Обязательно посмотрите shell-скрипт в репозитории — он чудо как хорош. Раскрашивает выходные данные, надёжный как скала… настоящий мастер-класс по созданию shell-скриптов.

Спасибо Гуннару Морлингу за добрые слова!

В январе 2024 года меня, вместе с несколькими дюжинами других гиков, повёрнутых на производительности, что называется, «заснайпили», заинтересовав участием в конкурсе Гуннара One Billion Row Challenge (1BRC).

Гуннара, оценивающего результаты работы конкурсантов (бесплатно), очень быстро буквально завалил непрерывный поток решений. Я рискнул помочь ему автоматизировать оценку работ и написал Shell‑скрипт, за что и получил от него вышеприведённый хвалебный отзыв. Он это сказал в выступлении на конференции JavaZone (# 1BRC‑Nerd Sniping the Java Community — Gunnar Morling). Посмотрите это выступление, если хотите узнать о подходах к повышению производительности кода, применённых на конкурсе.

Здесь я расскажу о 6 приёмах, которые я использовал в Shell-скрипте для конкурса. Эти приёмы позволили сделать скрипт надёжным, безопасным и приятным в использовании для Гуннара.

1. Всеобъемлющая обработка ошибок и проверка входных данных

Я уверен в том, что чёткие и понятные сообщения об ошибках — это краеугольный камень хорошего UX. Именно поэтому я и реализовал в скрипте качественные механизмы, обеспечивающие обработку ошибок и проверку ввода. Например:

if [ -z "$1" ] 
  then 
    echo "Usage: evaluate.sh <fork name> (<fork name 2> ...)" 
    echo " for each fork, there must be a 'calculate_average_<fork name>.sh' script and an optional 'prepare_<fork name>.sh'." 
    exit 1 
fi

Применение такого подхода помогает пользователям быстро выявлять и устранять проблемы, не тратя на это лишние время и нервы.

2. Понятные выходные данные, оформляемые с использованием разных цветов

Для того чтобы пользователю было бы удобнее и приятнее читать данные, которые выводит скрипт, я использовал ANSI-коды цветов, выделяя важную информацию, тексты предупреждений и ошибок. Например:

BOLD_RED='\033[1;31m'
RESET='\033[0m'
echo -e "${BOLD_RED}ERROR${RESET}: ./calculate_average_$fork.sh does not exist." >&2

Визуальные различия между текстами помогают пользователю быстро уловить характер сообщения.

3. Подробные сведения о ходе работы скрипта

Мне хотелось, чтобы пользователи точно знали о том, чем именно скрипт занимается на каждом этапе его работы. Для того чтобы этого достичь, я реализовал функцию, которая, перед выполнением команд, выводила сведения о них.

function print_and_execute() {
  echo "+ $@" >&2 
  "$@" 
}

Это напоминает то, как работает встроенная опция Bash set -x, которая позволяет включить трассировку выполнения скрипта. Но мой подход даёт автору скрипта возможность более точно управлять тем, что именно будет выводиться.

Такой уровень прозрачности программы не только снабжает пользователя сведениями о том, что в ней происходит, но и, в том случае, если что-то пойдёт не так, помогает в отладке.

4. Стратегический подход к обработке ошибок с помощью set -e и set +e

Мне хотелось сделать так, чтобы скрипт немедленно завершал бы работу в том случае, если в нём самом произошла бы ошибка. Но при этом я стремился к тому, чтобы позволить ему продолжить работу в том случае, если проблемы возникали в одном из форков. Для того чтобы этого достичь, я стратегически разместил в нужных местах скрипта опции Bash set -e и set +e. Вот как я это сделал:

# В начале скрипта
set -eo pipefail

# Перед запуском тестов и бенчмарков для каждого из форков
for fork in "$@"; do
  set +e # нам не нужно, чтобы prepare.sh, test.sh или hyperfine, давшие сбой на 1 форке, преждевременно останавливали бы скрипт

  # Запуск подготовительного скрипта (упрощено)
  print_and_execute source "./prepare_$fork.sh"

  # Запуск набора тестов (упрощено)
  print_and_execute $TIMEOUT ./test.sh $fork

  # ... (другие операции, специфичные для форка)
done
set -e  # Снова включить возможность выхода при возникновении ошибки, сделав это после выполнения операций, специфичных для форка

Такой подход даёт автору скрипта полный контроль над тем, какие ошибки приводят к остановке скрипта, а какие могут быть обработаны другими способами.

5. Адаптация скрипта к различным платформам

Зная о том, что пользователи могут запускать этот скрипт из разных операционных систем, я добавил в него логику, позволяющую определить используемую ОС и соответствующим образом откорректировать поведение скрипта:

if [ "$(uname -s)" == "Linux" ]; then 
  TIMEOUT="timeout -v $RUN_TIME_LIMIT" 
else # Исходим из предположения о том, что это — MacOS 
  if [ -x "$(command -v gtimeout)" ]; then 
    TIMEOUT="gtimeout -v $RUN_TIME_LIMIT"
  else 
    echo -e "${BOLD_YELLOW}WARNING${RESET} gtimeout not available, install with `brew install coreutils` or benchmark runs may take indefinitely long." 
  fi
fi

Благодаря этому приёму пользователи, работающие в разных окружениях, оказываются в равных условиях. Так, например, многие участники конкурса #1BRC программировали на MacOS, а оценочный сервер работал под Linux.

6. Использование временных меток при сохранении в файлах результатов множества вызовов скрипта

Для того, чтобы при многократных запусках скрипта не перезаписывались бы результаты его работы, полученные ранее, я реализовал систему формирования выходных файлов с использованием временных меток. Это позволяет пользователям запускать скрипт по много раз и не терять при этом историю результатов. Вот как я это сделал:

filetimestamp=$(date +"%Y%m%d%H%M%S")

# ... (в цикле, для каждого из форков)
HYPERFINE_OPTS="--warmup 0 --runs $RUNS --export-json $fork-$filetimestamp-timing.json --output ./$fork-$filetimestamp.out"

# ... (после работы бенчмарков)
echo "Raw results saved to file(s):"
for fork in "$@"; do
  if [ -f "$fork-$filetimestamp-timing.json" ]; then
      cat $fork-$filetimestamp-timing.json >> $fork-$filetimestamp.out
      rm $fork-$filetimestamp-timing.json
  fi

  if [ -f "$fork-$filetimestamp.out" ]; then
    echo "  $fork-$filetimestamp.out"
  fi
done

Итоги

Вот — полный код скрипта evaluate.sh, используемого для оценки заданий участников конкурса #1BRC, хранящийся в его репозитории.

Применяя вышеописанные приёмы, я стремился к созданию надёжного и информативного Shell-скрипта, с которым приятно работать, который сослужит хорошую службу пользователям, запускающим и анализирующим бенчмарки. Надеюсь, мои идеи вдохновят вас на улучшение UX ваших собственных Shell-скриптов. Здесь и здесь вы можете присоединиться к обсуждению этой статьи и поделиться своими мыслями о создании качественных Shell-скриптов.

Дополнение от 19 марта 2025 года: после того, как читатели обсудили эту статью, я написал продолжение, в котором приведены 6 приёмов написания Shell-скриптов от пользователей Hacker News.

О, а приходите к нам работать? ? ?

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Комментарии (1)


  1. RumataEstora
    29.07.2025 13:20

    print_and_execute

    Сомнительно. Чем вам пара set -x/set +x не угодила? И название длинное.

    xtrace() {
    	set -x
    	"$@"
    	set +x
    }

    Конечно же будет выводится лишняя отладочная строка + set +x. Но это мелочи. Зато можно так писать, а отладку включать/выключать комметированием первой строки:

    xtrace \
    какая-то команда с кучей --разных --ключей и разных параметров