14 примеров нетривиального использования bash из рабочей практики

Введение

Bash — самая популярная командная оболочка в GNU/Linux системах, доступна также на BSD системах, macOS и Windows и широко используется как для выполнения отдельных команд, так и в решениях автоматизации разной сложности. Я работаю в CloudLinux в роли SDET Automation Engineer над средством обеспечения безопасности веб-серверов Imunify360 и в нашей кодовой базе достаточно много кода на bash. Для этой статьи я выбрал 14 интересных примеров использования bash у нас.

Важное примечание

Здесь не будет описания базового синтаксиса bash, включая переменные, условный оператор, циклы, функции, массивы и т. д. Статья не претендует на роль введения в использование bash и подразумевает наличие у читателя определённых базовых знаний этой командной оболочки.

Также здесь не будет разбора базовых команд UNIX систем (ls, grep, find и прочих) и более специализированных, возможны только небольшие комментарии об их опциях, где это необходимо для описания конкретной возможности bash.

Статья публикуется без претензий на полноту описания всех доступных интересных возможностей bash или на новизну для 100% читателей. Среди приведённых примеров есть и относительно простые и более сложные, достаточно распространённые и весьма редкие. В общем, главная цель статьи — показать нетривиальные примеры использования bash в реальной рабочей практике и смотивировать по полной использовать его широкие возможности.

Примеры команд и кода намеренно синтетические и/или упрощённые для большей наглядности, но сами конструкции взяты из реального кода.

Короткий синтаксис перенаправления каналов вывода и ошибок

Начнём с очень простого. У каждой программы есть стандартный канал ввода, вывода и ошибок. В командной оболочке доступна возможность их перенаправления в указанный файл, и в bash в дополнение к, пожалуй, знакомым всем опциям > для вывода, 2> для ошибок и < для ввода есть более продвинутые.

Например, &> используется для перенаправления и стандартного канала вывода, и стандартного канала ошибок в один и тот же файл и позволяет использовать some-command &> some-file вместо дублирующих конструкций вида some-command > some-file 2> some-file или some-command > some-file 2>&1.

Близкая ей по смыслу опция для использования в конвейерах — |&. Она позволяет вместо some-command 2>&1 | some-other-command просто написать some-command |& some-other-command. Реализуемый этой опцией функционал необходим для передачи по конвейеру содержимого канала вывода вместе с каналом ошибок, так как по умолчанию по конвейеру передаётся только стандартный вывод.

Here-строка, here-документ и here-процесс

Кроме упомянутой ранее возможности перенаправления стандартного канала ввода из файла доступны более продвинутые возможности настройки входных данных для команды.

Here-строка используется для указания входных данных для команды непосредственно внутри командной строки. Простейший пример позволяет избежать необходимости создания файла и написать просто:

$ cat <<< "hello world"
hello world

Да, конкретно приведённый пример ничем не отличается от `echo “hello world”`. Но если надо передать конкретную небольшую строку в команду, работающую только со стандартным вводом, то это очень удобный способ. Сама конструкция очевидно идентична `echo SOME_STRING | other-command`, просто немного короче.

Чуть более сложный вариант — here-документ, он позволяет ввести содержимое многострочного файла непосредственно в скрипте без необходимости использования интерактивных редакторов. Вот пример использования этой конструкции создания тела для запроса к API.

$ cat <<EOF > data.json
{
    “name”: “Data Soong”,
    “type”: "android”
}
EOF
$ curl --header "Content-Type: application/json" --request POST --data @data.json https://some-host.com/api/v1/user

И последний пример варианта указания входных данных для команды — here-процесс (термин не является общеупотребительным и использован просто по аналогии с предыдущими вариантами). Этот вариант позволяет избежать необходимости в промежуточном шаге для сохранения вывода команды для использования в последующей и представляет скорее альтернативный вариант конвейеру из команд:

$ cat < <(echo "hello world")
hello world

Обратите внимание на пробел между двумя < — это не опечатка, он необходим.

Отлов ошибок в конвейерах

Как вы наверняка знаете, по умолчанию bash-скрипт продолжает выполняться даже при возврате ошибки одной из команд в скрипте. Это можно решить опцией `set -e`, прекращающей выполнение скрипта в случае ошибки. Но если в скрипте есть конвейеры — их кодом завершения является код последней команды, и поэтому критичные для выполнения скрипта ошибки могут быть проигнорированы таким образом и привести к некорректному поведению скрипта. Рассмотрим четыре варианта скрипта: без опций обработки ошибок, два с set -e и с set -o pipefail.

Первый выполнится до конца:

succeeded-command-1
failed-command
succeeded-command-2

Второй прервётся после failed-command

set -e
succeeded-command-1
failed-command
succeeded-command-2

но продолжится, если к failed-command через | добавлена команда с успешным выполнением как в третьем:

set -e
succeeded-command-1
failed-command | succeeded-command-3
succeeded-command-2

Указание опции pipefail позволит избежать этого и прервать выполнение скрипта после неуспешного выполнения хотя бы одной команды из конвейера, то есть после failed-command | succeeded-command-3 здесь:

set -e
set -o pipefail
succeeded-command-1
failed-command | succeeded-command-3
succeeded-command-2

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

Объединить упомянутые опции можно таким «эпиграфом» в начале файла:

set -euo pipefail

Раскрытие параметра со значением по умолчанию

Если в скрипте нужно использовать какой-то параметр из переменных окружения, но нет уверенности в его существовании или в непустом его значении, можно задать значение по умолчанию, например так:

TIMEOUT=${TIMEOUT:-60}

Если переменная окружения `TIMEOUT` существует в окружении и имеет непустое значение, будет использовано её значение, иначе будет использовано значение по умолчанию `60`.

И ещё одна конструкция полезна для одноимённых переменных, как в примерах выше, она при использовании и установит TIMEOUT в 60 и вернёт это значение, если TIMEOUT не была объявлена ранее или пуста, иначе вернёт значение переменной:

echo ${TIMEOUT:=60}

Но эта конструкция синтаксически верна только там, где ожидается возвращаемое значение, просто для присваивания она не работает. То есть только ${TIMEOUT:=60} в строке не сработает.

Группировка команд

Если необходимо сгруппировать вывод нескольких команд и передать результат для дальнейшей обработки — можно использовать фигурные скобки:

$ { echo value-1; echo value-2; echo foo; } | grep value
value-1
value-2

Как видно из примера, `grep` применён к объединённому выводу всех трёх команд группы.

Подобная конструкция — с обычными скобками, её отличие в создании subshell, отдельного процесса оболочки для выполнения команд внутри скобок, это например позволяет отделить переменные, текущую директорию и прочую специфичную для сеанса оболочки информацию, например (обратите внимание, что ; после последней команды в скобках не нужна):

$ pwd; (cd /tmp; pwd); pwd
/etc
/tmp
/etc

Как видим, текущая директория, изменённая внутри subshell, не повлияла на родительскую.

Срез

Возможно, Python срезы списков были вдохновлены таковыми в bash (кто-то может ещё вспомнить Fortran 🙂). Вот так можно например получить один символ строки из переменной FOO начиная с нулевой позиции:

$ FOO=bar
$ echo "${FOO:0:1}"
b

Короткая запись преобразования строки

Продолжаем тему обработки строк. С помощью конструкции ${VARIABLE#PATTERN} например можно отфильтровать из переменной VARIABLE часть строки PATTERN:

$ ASSIGNMENT=variable=value
$ echo ${ASSIGNMENT#*=}
value

Важное уточнение — удаляется кратчайшее соответствие шаблону от начала строки. Для удаления наибольшего соответствия # нужно заменить на ##. Также доступна обратная операция — удаление шаблона с конца строки:

$ echo ${ASSIGNMENT%=*}
variable

И она тоже работает для кратчайшего совпадения, а для наибольшего — аналогично нужны два %.

Такой разный if

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

Вот такие возможности есть в bash у оператора if:

  1. Без скобок — проверка кода возврата команды
  2. test или [] — для простых условий, конструкция является частью стандарта POSIX и доступна не только в bash, но и во многих других оболочках
  3. [[]] — более продвинутый оператор сравнения
  4. (()) — для математических операций

Вот пример скрипта, где все условия вычисляются в истину:

NUM_VAR=4
STR_VAR="a b"
if ls | grep sh > /dev/null
then
    echo "Shell scripts found"
fi
if test $NUM_VAR = 4
then
    echo "Num variable matches"
fi
if [ "$STR_VAR" = "a b" ]
then
    echo "String variable also matches"
fi
if [[ $STR_VAR == "a b" ]]
then
    echo "String variable again matches"
fi
if (( NUM_VAR % 2 == 0 ))
then
    echo "Num variable again good"
fi

Соответствие регулярному выражению

В прошлом пункте было указано про расширенные возможности двойных квадратных скобок в if операторе. Остановимся на этом чуть подробнее.

Проверка соответствия регулярной строке может быть осуществлена конструкцией

[[ $VARIABLE =~ PATTERN ]]

Например:

$ FOO=bar
$ [[ $FOO =~ ba. ]] && echo true || echo false
true

Но нужно учитывать, что в bash реализован стандарт Extended Regular Expressions (ERE), более ограниченный чем например Perl Compatible Regular Expressions (PCRE), и вот такая конструкция с использованием класса символа уже не будет работать:

$ [[ $FOO =~ ba\w ]] && echo true || echo false
false

В таких случаях нас спасёт grep:

$ ( echo $FOO | grep -Pq "ba\w" ) && echo true || echo false
true

-P здесь нужна для включения режима PCRE, а -q для подавления вывода.

Корректный вывод многострочной переменной

Если в переменную сохраняется вывод команды, где есть много строк, то простой вывод переменной подавляет символы новой строки:

$ SHELL_SCRIPTS=$(ls -1 | grep sh)
$ echo $SHELL_SCRIPTS 
script-1.sh script-2.sh

Это легко решается добавлением кавычек:

$ echo "$SHELL_SCRIPTS"
script-1.sh
script-2.sh

Контроль времени

Три простых команды, связанных со временем выполнения команд

  1. sleep — приостановить выполнение скрипта на указанное время, доступны секунды, минуты, часы и даже дни
  2. time — вывод времени выполнения команды, причём как реальное календарное, так и непосредственно “активное” время затраченное процессором для выполнения в пользовательском режиме и режиме ядра
  3. timeout — ограничение времени выполнение команды заданных временем, также доступны разные единицы как и в `sleep`, очень полезно для сетевых команд

Сокращённое указание диапазона для перебора

В UNIX есть команда для вывода последовательности чисел с разными параметрами и это удобно для циклов:

$ for i in `seq 1 3`; do echo $i; done
1
2
3

Но в bash доступно сокращение для подобного функционала:

$ for i in {1..3}; do echo $i; done
1
2
3

Специальные переменные

В bash много всего полезного кодируется короткими языковыми конструкциями, вот примеры:

  • $? — код возврата последней команды
  • $# — число аргументов, переданных функции или самому скрипту
  • $* и $@ — аргументы, переданные функции или самому скрипту, одним словом или отдельными соответственно
  • $! — PID последней команды, запущенной в фоновом режиме
  • $$ — PID самой оболочки командной строки

Вот простейший пример файла с ними (вместо function foo доступен синтаксис foo()):

function foo
{
    echo $#
    echo $*
    echo $@
    return 1
}
foo a b c
echo $?
ls > /dev/null &
echo $!
echo $$

И вот его вывод:

3
a b c
a b c
1
454082
454081

Lock-файл

И последнее, пожалуй, наиболее интересное. Бывает, что нужно ограничить число разрешённых экземпляров какой-либо программы до одного, чтобы избежать конфликтов работы с разделяемыми ресурсами, например. Вот упрощённый пример такой программы, где полезной нагрузки нет, только ожидание:

echo trying to acquire lock for process $$
(
    flock -w 20 9
    echo begin operation in process $$
    sleep 10
    echo end operation in process $$
) 9>/tmp/foo.lock

9 здесь — не какое-то магическое число, а просто код файлового дескриптора, так же как 1 — код стандартного вывода. 20 — время ожидания доступности файла блокировки, а 10 — просто длительность имитации работы, меньшая чем время ожидания, чтобы второй экземпляр скрипта из примера ниже тоже успел поработать.

И вот что происходит при одновременном запуске двух экземпляров этого скрипта:

$ bash flock.sh &
[1] 454845
trying to acquire lock for process 454845
begin operation in process 454845
$ bash flock.sh &
[2] 454851
trying to acquire lock for process 454851
end operation in process 454845
begin operation in process 454851
end operation in process 454851

Как видим, сначала первый процесс 454845 успешно захватывает lock-файл и начинает работу (то есть спит 😀), второй 454851 пытается занять блокировку, но получается это только после завершения работы первым скриптом.

Заключение

Возможности bash очень широки за счёт множества сильных синтаксических конструкций и доступа к огромному базовому набору команд, расширяемых установкой новых пакетов до покрытия многих и многих потребностей. Однако всё-таки это не язык программирования, и бывают такие сценарии использования или такое развитие первых версий функционала, когда лучше переключиться на язык программирования типа Python или на решение автоматизации как Ansible, иначе поддержка и развитие становятся очень и очень трудозатратными.

Полезные ссылки для дополнительного изучения

  1. Очень обширное руководство «Искусство программирования на языке сценариев командной оболочки» в переводе Андрея Киселева (в оригинале «Advanced Bash-Scripting Guide» от Mendel Cooper) https://www.opennet.ru/docs/RUS/bash_scripting_guide/
  2. Шпаргалка по bash, больше про синтаксические конструкции самого bash https://devhints.io/bash (на английском)
  3. Ещё одна шпаргалка по bash, здесь синтаксические конструкции bash демонстрируются на примерах с командами оболочки https://github.com/RehanSaeed/Bash-Cheat-Sheet (на английском)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *