Практическое написание сценариев командной оболочки Bash/Bash подстановки

← Эмуляция ссылочной адресации Глава Команды →
Bash подстановки


Простой подстановкой (expansion) в этой книге мы называем замену обращения к переменной ее значением. Простые подстановки это что-то похожее на обращение к переменной в других языках программирования, но в Bash эта операция отличается следующим:

  • Заменяемая переменная может быть не объявлена: в этом случае мы говорим, что подстановка раскрывается в пустоту. Пустота есть пустота: место в сценарии, в котором прописано раскрытие останется пустым, как будто само раскрытие не было записано в коде сценария.
  • Переменная может быть объявлена, но проинициализирована пустотой. Следует отличать понятия пустоты и пустой строки (т.е. строки с нулевым числом символов): пустая строка в отличие от пустоты может быть проверена на длину; над пустой строкой можно выполнять строковые операции, а над пустотой нельзя.

У простой подстановки есть две формы:

  • Упрощенная. В упрощенной форме достаточно после знака доллара написать имя переменной, например $VARIABLE_NAME.
  • Строгая. В строгой форме имя переменной нужно поместить в фигурные скобки, например ${VARIABLE_NAME}. В таком виде строгая форма ничем не отличается от упрощенной, тем не менее только строгая форма разрешает вам пользоваться встроенными подстановочными операциями — мощным инструментом командной оболочки. Строгая форма, помимо всего прочего, определяет где заканчивается имя подставляемой переменной, так как его границы определены фигурными скобками. Например, сравните:
    # Положим у нас есть переменные
    VAR="Good "
    VAR_="Bad "
    
    # Значение какой переменной будет подставлено?
    echo "$VAR_day"
    
    # В данном случае никакой из существующих (пустая подстановка). Интерпретатор будет пытаться искать
    # переменную VAR_day, которая не определена.
    # Чтобы разрешить неоднозначтность, нужно использовать строгую форму, чтобы обозначить границы имени переменной
    
    echo "${VAR}day" # "Good day"
    echo "${VAR_}day" # "Bad day"
    
    # Строгая форма должна всегда использоваться, когда за подстановкой нет разделителя и идет буквенно-цифровой символ или символ
    # нижнего подчеркивания (т.е. за подстановкой идет такой символ, который может использоваться в именах переменных).
    
    echo "$VAR_$VAR" # "Bad Good ". Неоднозначности нет, потому что символ '$' нельзя использовать в имени переменной
    echo "$VAR$VAR_" # "Good Bad ". Аналогично
    
    # У этого правила есть исключение: специальные подстановки (встроенные в оболочку) всегда разрешаются правильно:
    echo "$-_day"
    echo "$$_day"
    echo "$@_day"
    echo "$*_day"
    echo "$1_day"
    echo "$2_day"
    echo "$#_day"
    
    # Но и здесь есть исключение
    
    echo "${_}_day"
    
    # Так же неоднозначности нет и здесь
    echo "$VAR_%cost" # "Bad %cost"
    echo "$VAR&cost"  # "Good &cost" 
    echo "$VAR+cost"  # "Good +cost"
    # потому что символы %, & и + в данных примерах не могут использоваться в именах переменных.
    
    # Тем не менее, использование строгой формы улучшает читаемость кода.
    

Давайте рассмотрим следующий код.

$EMPTYNESS # Подстановка не объявленной переменной всегда раскрывается в пустоту.
[[ -z $EMPTYNESS ]] && echo "Emptyness expansion" # Опция -z команды test позволяет определять пустую подстановку.

EMPTYNESS= # Если после равно нет какого-то значения, например в форме строки или 'голого слова' (bareword), то она остается пустой.
EMPTYNESS="" # Теперь переменная проинициализирована пустой строкой.

# Для того, чтобы сделать переменную пустой, служит команда unset
unset EMPTYNESS
# или
unset -v EMPTYNESS

Кроме простой подстановки, Bash позволяет делать некоторые более продвинутые, встроенные в интерпретатор:

  • Арифметические подстановки. Позволяет на ходу делать вычисления и подставлять результат.
  • Подстановка с подстановочной операцией.
  • Подстановка результата из подоболочки.
  • Подстановка результата процесса через автоматический файл.
  • Скобочные подстановки. Позволяют быстро генерировать списки, используя комбинаторные методы.
  • Тильда-подстановки. Позволяют делать подстановки относительно залогинившегося пользователя.
  • Globbing-подстановки. Позволяет через маскирующие символы подставлять имена файлов.

Далее мы рассмотрим каждый вид подстановки и ситуации, когда они применяются.

Арифметические подстановки править

Для выполнения арифметических вычислений в командной оболочке служат две команды: let и expr. Тем не менее, Bash позволяет выполнять вычисления и без их применения с помощью арифметической подстановки:

$((<выражение>))

Примеры

N1=2
N2=3
N3=26
N4=7
N5=-8

# Математические вычисления
echo "$N1 + $N2 = $((N1 + N2))"   # сложение
echo "$N1 - $N2 = $((N1 - N2))"   # вычитание
echo "$N1 * $N2 = $((N1 * N2))"   # умножение
echo "$N1 ** $N2 = $((N1 ** N2))" # возведение в степень
echo "$N3 / $N4 = $((N3 / N4))"   # деление
echo "$N3 % $N4 = $((N3 % N4))"   # остаток от деления

# Унарные операторы
echo "-($N5) = $((-N5))"  # унарный минус
echo "+($N5) = $((+N5))"  # унарный плюс

TRUE=0
FALSE=1

# Булевы операторы
echo "!$TRUE = $((!TRUE))"     # отрицание
echo "!$FALSE = $((!FALSE))"
echo "!$FALSE = $((!FALSE))"
echo "$TRUE && $FALSE = $((TRUE && FALSE))"  # конъюнкция
echo "$TRUE || $FALSE = $((TRUE || FALSE))"  # дизъюнкция

NN1=222
NN2=141
# Поразрядные операции
echo "$(echo "obase=2;$NN1" | bc) & $(echo "obase=2;$NN2" | bc) = $(echo "obase=2; $((NN1 & NN2))" | bc)" # поразрядное И
echo "$(echo "obase=2;$NN1" | bc) | $(echo "obase=2;$NN2" | bc) = $(echo "obase=2; $((NN1 | NN2))" | bc)" # поразрядное ИЛИ
echo "$(echo "obase=2;$NN1" | bc) ^ $(echo "obase=2;$NN2" | bc) = $(echo "obase=2; $((NN1 ^ NN2))" | bc)" # поразрядное исключающее ИЛИ
echo "~15 = $((~15))" # поразрядное отрицание

NN1=1
# Поразрядные сдвиги
echo "$(printf "%04d" $(echo "obase=2;$NN1" | bc)) << 3 = $(echo "obase=2; $((NN1 << 3))" | bc)"  # поразрядный сдвиг влево
NN1=8
echo "$(printf "%04d" $(echo "obase=2;$NN1" | bc)) >> 2 = $(printf "%04d" $(echo "obase=2; $((NN1 >> 2))" | bc))" # поразрядный сдвиг вправо

# Инкремент
echo "N1 = $N1"
echo "N1 = ++$N1 = $((++N1))" # префиксный инкремент: увеличить на единицу, присвоить и вывести
echo "N1 = $N1"
echo "N1 = $N1++ = $((N1++))" # постфиксный инкремент: вывести, увеличить на единицу и присвоить
echo "N1 = $N1"

# Декремент
echo "N1 = --$N1 = $((--N1))" # префиксный декремент: уменьшить на единицу, присвоить и вывести
echo "N1 = $N1"
echo "N1 = $N1-- = $((N1--))" # постфиксный декремент: вывести, уменьшить на единицу и присвоить
echo "N1 = $N1"

# Сравнивание чисел
echo "$N1 > $N2 = $((N1 > N2))"
echo "$N1 >= $N2 = $((N1 >= N2))"
echo "$N1 < $N2 = $((N1 <= N2))"
echo "$N1 == $N2 = $((N1 == N2))"
echo "$N1 != $N2 = $((N1 != N2))"

# Тернарная операция
echo "N1 = $N1, N2 = $N2"
echo "$((N1 > N2 ? N1 + N2 : N1 - N2))"

# Несколько выражений в одном
echo "$((N1 = 1 , N += 8, N *= 2, N--))"

# Следующие операторы также имеют форму с присваиванием
# *= /= %= += -= <<= >>= &= ^= |=
echo "N1 = $N1"
echo $(( N1 *= 2 ))
echo $(( N1 /= 3 ))
echo $(( N1 %= 10 ))
echo $(( N1 += 25 ))
echo $(( N1 -= 6 ))
echo $(( N1 <<= 2 ))
echo $(( N1 >>= 3 ))
echo $(( N1 &= 255 ))
echo $(( N1 ^= 255 ))
echo $(( N1 |= 255 ))

Результаты

2 + 3 = 5 
2 - 3 = -1
2 * 3 = 6 
2 ** 3 = 8
26 / 7 = 3
26 % 7 = 5
-(-8) = 8 
+(-8) = -8
!0 = 1    
!1 = 0    
!1 = 0    
0 && 1 = 0
0 || 1 = 1
11011110 & 10001101 = 10001100
11011110 | 10001101 = 11011111
11011110 ^ 10001101 = 1010011
~15 = -16
0001 << 3 = 1000
1000 >> 2 = 0010
N1 = 2
N1 = ++2 = 3
N1 = 3
N1 = 3++ = 3
N1 = 4
N1 = --4 = 3
N1 = 3
N1 = 3-- = 3
N1 = 2
2 > 3 = 0
2 >= 3 = 0
2 < 3 = 1
2 == 3 = 0
2 != 3 = 1
N1 = 2, N2 = 3
-1
16
N1 = 1
2
0
0
25
19
76
9
9
246
255

Подстановочные операции править

Подстановочные операции (Shell Parameter Expansion) позволяют преобразовать значение переменной перед подстановкой, при этом не перезаписывая оригинальное значение. Эти операции реализует командная оболочка, поэтому эта техника потенциально не переносима. Раньше разработчикам приходилось вызывать различные утилиты, чтобы некоторым образом преобразовать строку. Со временем большую часть таких рутинных операций перенесли в командную оболочку через подстановки.

Мы рассмотрим часто используемые ситуации использования подстановочных операций.

Операции, связанные с присваиванием
echo ${EMPTYNESS-"NULL"} # Если переменная EMPTYNESS раскрывается в пустоту, то вместо нее будет подставлено значение справа от тире.
                    # Результат: "NULL"
EMPTYNESS=""
echo ${EMPTYNESS:-"EMPTY"} # Если переменная EMPTYNESS раскрывается в пустоту или в строку нулевой длины,
                      # то вместо нее будет подставлено значение справа от :-.
                      # Результат: "EMPTY"

unset -v EMPTYNESS
echo ${EMPTYNESS="DEFAULT_VALUE"} # Если переменная EMPTYNESS раскрывается в пустоту, то вместо нее будет подставлено и ПРИСВОЕНО значение справа от равно.
                                  # Результат: "DEFAULT_VALUE"

unset -v EMPTYNESS
echo ${EMPTYNESS:="DEFAULT_VALUE"} # Если переменная EMPTYNESS раскрывается в пустоту или в строку нулевой длины,
                                   # то вместо нее будет подставлено и ПРИСВОЕНО значение справа от равно.
                                   # Результат: "DEFAULT_VALUE"

EMPTYNESS=""
echo "${EMPTYNESS+"Actually, EMPTYNESS is not NULL."}" # Если переменная не раскрывается в пустоту, то будет подставлена строка справа от +.
                                                       # Результат: "Actually, EMPTYNESS is not NULL."

EMPTYNESS="some text"
echo "${EMPTYNESS:+"Actually, EMPTYNESS is not empty."}" # Если переменная не раскрывается в пустоту, либо в строку нулевой длины,
                                                         # то будет подставлена строка справа от :+.
                                                         # Результат: "Actually, EMPTYNESS is not empty."

unset -v EMPTYNESS
# Следующие две подстановочные операции используются обычно для технологических проверок инициализации переменных.
: ${EMPTYNESS?"is NULL"} # Сценарий будет аварийно прерван с сообщением, указанным после знака вопроса, если переменная раскрывается в пустоту.

: ${EMPTYNESS:?"is NULL"} # Сценарий будет аварийно прерван с сообщением, указанным после :?, если переменная раскрывается в пустоту или в строку нулевой длины.
Работа с регистром символов
TEST_STR="sOmE_Text@123"
echo "(Original test)" ${TEST_STR}

echo "(To upper case)" ${TEST_STR^^}                     # Перевести все символы в верхний регистр
echo "(To upper case for first letter)" ${TEST_STR^}     # Перевести в верхний регистр только первый символ строки

echo "(To lower case)" ${TEST_STR,,}                     # Перевести все символы в нижний регистр
TEST_STR_1=${TEST_STR^^}
echo "(To lower case for first letter)" ${TEST_STR_1,}   # Перевести в нижний регистр только первый символ

echo "(Reverse case for every letter)" ${TEST_STR~~}     # Инвертировать регистр каждого символа в строке
echo "(Reverse case for first letter)" ${TEST_STR~}      # Инвертировать регистр только первого символа

# Вы не ограничены в использовании только простых переменных. Вы можете редактировать также списки, сформированные, например, из массива.
declare -a array=(val1 Val2 vAL3 VAL4 VaL5 vAl6)
echo "${array[@]} original"
echo "${array[@]^^} ^^"
echo "${array[@]^} ^"
echo "${array[@],,} ,,"
echo "${array[@],} ,"
echo "${array[@]~~} ~~"
echo "${array[@]~} ~"
Работа со строками и выделение подстрок
echo "Number of letters in TEST_STR: ${#TEST_STR}" # Возвращает число символов в строке
echo "Substring (from 0 symbol get 5 symbols): ${TEST_STR:0:5}" # Выделяет подстроку из 5 символов, начиная с символа с индексом 0 (т.е. с первого)
echo "Display string from 4th symbol: ${TEST_STR:3}" # Выделяет подстроку, начиная с символа с индексом 3

# Вы можете использовать отрицательные числа, чтобы проходить строку с правого края.
echo "Cut off 3 symbols from right edge: ${TEST_STR:0:-3}" # Отсечь три символа справа

# В следующем примере мы захватываем три последних символа через отступ на три
# символа от правого края. Обратите внимание, что пробел перед минусом обязательный.
echo "Display last 3 symbols: ${TEST_STR: -3:3}"  # Первый способ
echo "Display last 3 symbols: ${TEST_STR:(-3):3}" # Второй способ

# Вы можете работать похожим образом и с элементами массива.
echo "Array size ${#array[@]}" # Выводит размер массива
echo "Size of the second element of the array ${#array[1]}" # Выводит число символов в элементе массива с индексом 1

# Удаление части начальных и конечных символов строки по маске. Очень часто такая возможность используется, чтобы
# выделять части абсолютного пути.
SOME_PATH="/very/long/path/to/archive.tar.gz"
echo "(Original path)  $SOME_PATH"

echo "(Full path to archive) ${SOME_PATH%/*}"              # Результат: "/very/long/path/to"
echo "(File name) ${SOME_PATH##*/}"                        # Результат: "archive.tar.gz"
echo "(Full path without extensions) ${SOME_PATH%%.*}"     # Результат: "/very/long/path/to/archive"
echo "(Full path without gz-extension) ${SOME_PATH%.*}"    # Результат: "/very/long/path/to/archive.tar"
echo "(Most outer extension) ${SOME_PATH##*.}"             # Результат: "gz"
echo "(All extensions) ${SOME_PATH#*.}"                    # Результат: "tar.gz"
echo "(Replace gz by bz2) ${SOME_PATH%.gz}.bz2"            # Результат: "/very/long/path/to/archive.tar.bz2"
Замена подстрок
TEST_STR="Very long long string with spaces."
echo "(Original string) $TEST_STR"

echo "(Replace 'long' by 'short') ${TEST_STR//long/short}"  # Заменить все вхождения 'long' на 'short'
echo "(Replace 'long' by 'short') ${TEST_STR/long/short}"   # Заменить только первое вхождение 'long' на 'short'
echo "(Remove 'long' from the text) ${TEST_STR//long }"     # Удалить все вхождения 'long'. Фактически мы заменяем их строкой нулевой длины
echo "(Remove 'long' from the text) ${TEST_STR/long }"      # Удалить только первое вхождение 'long'
Другие
# Следующие подстановки можно использовать в технологических целях

# Вывести имена всех переменных, которые начинаются на 'TEST'
echo "Display all variables with PREFIX='TEST': ${!TEST*}" # Первый способ
echo "Display all variables with PREFIX='TEST': ${!TEST@}" # Второй способ

Подстановка результата из подоболочки править

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

$(<командный список>)

# Со времен Bourne Shell поддерживается также вариант через наклонные апострофы
`командный список`
# Но мы не рекомендуем его использовать в новых сценариях из-за проблем с читаемостью такого синтаксиса,
# а также сложностями вкладывания одних подстановок в другие.

На практике этот прием очень часто используется, чтобы проинициализировать переменную выводом другой команды, либо получить некоторый список (например список файлов через команду find).

Примеры

# Следующий пример вызывает echo в подоболочке, вывод которой будет подставлен и исполнен в текущей оболочке.
$(echo "echo "Hello, world!"")

# Передача результата подоболочки функции аргументами
# В этом примере функция printer просто напечатает свои аргументы, которые формируются
# через вызов команды ls в подоболочке. В данном примере будут выведены все файлы
# в текущем рабочем каталоге.
printer() {
	echo ">>> Called $FUNCNAME"
	local i=0
	for arg; do
		echo "$((++i)) $arg"
	done
}

printer $(ls)

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

$($($(third-level-command)))
# или
$( # Первый уровень
   $( # Второй уровень
      $(third-level-command)
    )
)
# Здесь сначала исполнится команда третьего уровня подоболочки, чей вывод будет подставлен
# во второй уровень. Вывод третьего уровня должен быть по меньшей мере командой, чтобы второй
# уровень подоболочки смог его исполнить. Затем вывод второго уровня подставится в первый уровень подоболочки.
# Наконец, первый уровень подоболочки попытается исполнить команду, которая вернулась из второго уровня
# и вернуть весь вывод в корневую командную оболочку.

Обычно вкладывание используется редко, потому что такие конструкции сложно читать.

Обратите внимание, что если вы используете двойные кавычки, чтобы предотвратить разбиение (splitting), то каждый уровень вложенности должен сопровождаться своей парой кавычек. Другими словами, внешняя пара кавычек по отношению к вложенной подстановке, за эту подстановку не отвечает. Это легко увидеть на следующем примере

echo "$(printf $(printf "one two"))"
# Результат: "one"

В предыдущем примере, вместо ожидаемой строки "one two", мы получаем только "one". Здесь накладываются следующие факторы:

  • Особенность работы команды printf, которая может принимать много аргументов: в первом аргументе должна быть форматная строка, а в каждом последующем должна быть подстановка для формата в форматной строке. Таким образом, любые строки, передаваемые в первом аргументе printf всегда должны заключаться в кавычки, если они имеют пробелы или символы поля IFS.
  • Забытые кавычки вокруг второго уровня. Команда printf на втором уровне выполнится правильно и вернет на первый уровень строку "one two". Но, так как самые внешние кавычки не отвечают за первый уровень вложенности, команда printf получит два аргумента из-за разбиения одной строки на два слова: форматную строку one и подстановку для формата two. Так как в форматной строке нет ни одного формата, printf игнорирует все аргументы, начиная со второго.

Обратите внимание, что эту ошибку можно исправить двумя путями:

# Достаточно поставить кавычки вокруг второго уровня вложенности
echo "$(printf "$(printf "one two")")"
# Теперь, когда исполнится второй уровень вложенности, результат будет такой
#
#    "$(printf "$(printf "one two")")" --> "$(printf "one two")"
#              ^-------------------^                 ^-------^
#                        \-------------------------------/
#                               Одни и те же кавычки
#
# Результат: "one two"

# Второй способ заключается в явном указании форматной строки для команды printf первого уровня вложенности
echo "$(printf "%s %s"  $(printf "one two"))"
# В этом случае второй и третий аргумент, которые возвращаются со второго уровня вложенности, будут просто
# подставлены в формат.
#
# Результат: "one two"

Вы уже наверное догадались, что результат подстановки должен быть верным в контексте, в который он подставляется. Команда в подстановке может быть сложной, в том числе, это может быть целая процедура.

#!/bin/bash

declare -ar SUBCOMMANDS=("algo1" "algo2" "algo3")

# В этом примере мы формируем описание одной из опций гипотетической утилиты в функции usage().
usage() {
    printf "
    $0      Some utility.

    Options:
        -r $(
        for s in ${SUBCOMMANDS[*]}; do printf "$s|"; done
    )
            <${SUBCOMMANDS[0]}>
                Description of the algorithm 1
            <${SUBCOMMANDS[1]}>
                Description of the algorithm 2
            <${SUBCOMMANDS[2]}>
                Description of the algorithm 3

"
}
usage
# Вывод:
#
#    ./dummy.sh      Some utility.
#
#    Options:
#        -r algo1|algo2|algo3|
#            <algo1>
#                Description of the algorithm 1     
#            <algo2>
#                Description of the algorithm 2     
#            <algo3>
#                Description of the algorithm 3     
#

Из-за плохой производительности вложенных конструкций, их следует использовать для фрагментов кода, которые вызываются относительно редко.

Подстановка результата процесса через автоматический файл править

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

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

  • Если команда производит данные, то она создает и пишет в некоторый файл, указываемый разработчиком явно. Файл можно создать, если перенаправить стандартный поток вывода команды в файл.
  • Если команда потребляет данные из файла, то разработчик связывает стандартный поток ввода этой команды с дескриптором, ведущим на файл производителя.

Например

#!/bin/bash

PRODUCER_FILE=$(mktemp -p /tmp tmp.XXXXXXXX)

# Для удаления временного файла после завершения сценария
trap "rm -f $PRODUCER_FILE" EXIT SIGKILL

producer() {
    for arg; do
        echo $arg
    done
} >$PRODUCER_FILE

consumer() {
    while read; do
        echo $FUNCNAME: $REPLY
    done
} <$PRODUCER_FILE

producer "alpha" "beta" "gamma"
echo "========================="
cat $PRODUCER_FILE
echo "========================="
consumer
# Результат:
# =========================
# alpha
# beta
# gamma
# =========================
# consumer: alpha
# consumer: beta
# consumer: gamma

Основным неудобством этого способа является необходимость иметь файл-посредник между двумя процессами: этот файл создаете вы и вы же отвечаете за его удаление.

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

#!/bin/bash

producer() {
    for arg; do
        echo $arg
    done
}

consumer() {
    while read; do
        echo $FUNCNAME: $REPLY
    done
}

producer "omicron" "kappa" | consumer
# Результат:
# consumer: omicron
# consumer: kappa

Вроде бы все хорошо, но теперь представьте, что данный код не оформлен через функции: есть только код, производящий данные, и код — потребляющий данные. В этом случае в Bash предусмотрен третий метод — через подстановку.

#!/bin/bash
(
    for arg in "alpha" "beta" "gamma"; do
        echo $arg
    done
) > >(
    while read; do
        echo Anonimous consumer: $REPLY
    done
)
# Результат:
# Anonimous consumer: alpha
# Anonimous consumer: beta
# Anonimous consumer: gamma

Данная конструкция кажется сложной, но поймете ли вы ее смысл, если мы запишем ее в гибридном варианте:

producer "alpha" "beta" "gamma" > >(
    while read; do
        echo Anonimous consumer: $REPLY
    done
)
# Результат:
# Anonimous consumer: alpha
# Anonimous consumer: beta
# Anonimous consumer: gamma
#
# или так

exec 3<><(
    for arg in "alpha" "beta" "gamma"; do
        echo $arg
    done
)

consumer() {
    # Мы используем таймаут, чтобы программа не подвесилась, потому что читаемый файл бесконечный.
    while read -t 1; do
        echo $FUNCNAME: $REPLY
    done
}

consumer 0<&3

# Результат:
# consumer: alpha
# consumer: beta
# consumer: gamma

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

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

$ echo >(true) <(true)
/dev/fd/63 /dev/fd/62

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

Приведем еще один пример

(echo "World")> >(read str; echo "Hello, ${str}")> >(read str1; echo "${str1}! How are you?")
# Результат:
# Hello, World! How are you?
#
# Следующая схема показывает, как такой вывод получается
# echo > [fd 1] > (read; echo) > [fd 2] > (read; echo) > STDOUT

Скобочные подстановки править

Скобочные подстановки (Brace Expansion) обычно используются для генерации списков, в которых есть комбинаторные закономерности. Обычно это полезно в циклах for и при заполнении массивов некоторыми данными.

Скобочная подстановка так называется потому, что она оформляется через фигурные скобки.

Примеры

{element1,element2,element3,element4} # Подставит список "element1 element2 element3 element4"
{0..5} # "0 1 2 3 4 5"
{00..05} # "00 01 02 03 04 05"
{a..z} # Список из всех букв латинского алфавита
1.{0..5} # "1.0 1.1 1.2 1.3 1.4 1.5"
{0..20..2} # Можно генерировать ряд чисел с шагом "0 2 4 6 8 ... 20"
{0000..20..2} # "0000 0002 0004 ... 0020"

# Скобочные подстановки можно вкладывать
{{A..Z},{a..z},{0..9}} # "A B ... Z a b ... z 0 1 ... 9"

# Комбинаторный пример: генерация имен файлов
{john,bill}{0..3}.tar.{bz2,gz}
# john0.tar.bz2
# john0.tar.gz
# john1.tar.bz2
# john1.tar.gz
# ....
# bill0.tar.bz2
# bill0.tar.gz
# ....
# bill3.tar.gz

Тильда-подстановки править

Тильда подстановки (Tilde Expansion) позволяют делать подстановки относительно залогинившегося пользователя. Эти подстановки так называются, потому что они начинаются с символа ~.

echo "My home directory is " ~ # Вместо тильды будет подставлен рабочий каталог текущего пользователя.
echo "Current workdir is" ~+ # Будет подставлен текущий рабочий каталог.
echo "Previous workdir is" ~- # Будет подставлен предыдущий рабочий каталог.

# Вообще после '+' и '-' можно указать число. Тогда подставится каталог,
# получаемый командой 'dirs':
#     ~+2    то же что и    dirs +2
#     ~-2    то же что и    dirs -2
# По умолчанию это число равно 0.

echo "Root workdir is" ~root # Заменится на путь к рабочему каталогу пользователя root.
echo "Path to 'my_docs' dir is" ~/my_docs # Заменит подстановку на путь к каталогу my_docs относительно рабочего каталога
                                          # текущего пользователя.
# Каталог my_docs может не существовать в действительности.

Globbing-подстановки править

Globbing-подстановки (маскирующие подстановки) позволяют вам подставлять списки из имен файлов, используя маски. Обычно этой возможностью пользователи пользуются в интерактивном режиме, но в сценарии эта возможность также доступна. Как и в интерактивном режиме, эта подстановка разрешается относительно текущего рабочего каталога сценария, если вы не указываете абсолютный/относительный путь в подстановке.

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

  • ?. Маскирует ноль или один литерал маски.
  • *. Маскирует ноль или серию литералов маски.
  • []. Напоминает группу в регулярных выражениях, т.е. сопоставляет один из символов в группе в позиции входящей строки.
  • [^] или [!]. Напоминает инверсную группу в регулярных выражениях.

Рассмотрим примеры.

# Пусть в текущем каталоге лежат файлы: a.txt, b.txt, script.sh и c.tar.gz
echo [a-z].???  # Сформирует список "a.txt b.txt"
echo [a-z]*.??  # "c.tar.gz script.sh"
echo [^a].*     # "b.txt c.tar.gz"
echo /???       # Сформирует список из всех каталогов с трехсимвольным именем в корневом каталоге, например "/bin /cmd /dev /etc /tmp /usr"

Особенности интерпретации подстановок править

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

Давайте начнем с общих правил. Интерпретатор, разбирая сценарий построчно, разбивает каждую строку на отдельные слова, используя пробельные символы (word splitting). Для интерпретатора важно понять, что из результата является командой, а что ее параметрами. В общем случае мы называем такие слова «голыми» (bare words).

# Данная команда состоит из 4 голых слов. Из них крайнее левое слово всегда интерпретируется как имя команды,
# а все последующие - это ее параметры. Причем количество пробелов между голыми словами не имеет значения.
command param1 param2 param3

Из-за того что голые слова разделяются пробелами, их нельзя нигде ставить при присваивании:

# ОШИБКА
VARIABLE = 1234
# С точки зрения интерпретатора, вы ввели команду VARIABLE и передали ей два аргумента: '=' и '1234'

# ОШИБКА
VARIABLE=A very long string
# Так тоже делать нельзя. С точки зрения интерпретатора, вы вызвали команду 'very' и передали ей
# экспортированную переменную VARIABLE со значением 'A', а также параметры 'long' и 'string'.

Для преодоления таких проблем, когда пробелы нужно интерпретировать не как разделители голых слов, в интерпретаторе предусмотрены символы кавычек двух типов. Первый тип — это двойные кавычки, которые не дают интерпретатору использовать пробелы как разделители голых слов. При этом внутри двойных кавычек интерпретатор может разрешать встречающиеся ему простые подстановки.

VARIABLE="A very long string"
# Двойные кавычки не позволяют интерпретатору разбить правую часть от равно на голые слова.

# ДОПУСТИМО
VARIABLE_1=$VARIABLE
# Несмотря на то что в оригинальной переменной есть пробелы, процедура присваивания не будет принимать их во внимание после подстановки, т.е.
# кавычки ставить не обязательно.

echo "Value: $VARIABLE_1"
# Интерпретатор разрешает подстановки внутри двойных кавычек.

Второй тип — это одинарные кавычки. Одинарные кавычки выполняют ту же задачу что и двойные, а также запрещают разрешать подстановки.

echo 'Cost: $10'
echo 'Total cost: 25 000 $'
# Так как знак '$' несет особый смысл (интерпретатор определяет так начало подстановки), мы используем одинарные кавычки, чтобы запретить его интерпретацию.
# В этом случае мы говорим, что знак '$' понимается буквально.

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

echo "Cost: \$10"
echo "Total cost: 25 000 \$"

# В этом примере мы используем двойные кавычки, но экранируем символы '$'.

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

arg_printer() {
    echo "Arg counter: $#"
    local -i counter
    for arg; do
        echo "$((counter++)): '$arg'"
    done
}

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

LONG_STRING="A Very Long String"
arg_printer $LONG_STRING    # Подстановка будет разбита на 4 голых слова, т.е. команде будет передано 4 параметра
# Результат:
# Arg counter: 4
# 0: 'A'
# 1: 'Very'
# 2: 'Long'
# 3: 'String'

# Если мы передаем подстановку в двойных кавычках, то мы запретим разбивать по пробелам на голые слова.
arg_printer "$LONG_STRING"
# Результат:
# Arg counter: 1
# 0: 'A Very Long String'

# В следующем примере мы склеиваем несколько строк вместе. Обратите внимание, что каждый пробел в склеиваемых
# строках будет участвовать в разделении на голые слова, если он не закавычен.
ADDITION_STRING="with some additions"
SPACE=' '
arg_printer "$LONG_STRING"$SPACE$ADDITION_STRING
# Результат:
# Arg counter: 4
# 0: 'A Very Long String'
# 1: 'with'
# 2: 'some'
# 3: 'additions'

Для некоторых выражений в Bash очень важно правильно разбивать строки на голые слова. Одним из таких примеров является цикл for.

LIST="Apple Pear Potato Tomato"

# НЕПРАВИЛЬНО
for item in "$LIST"; do
   echo "$item"
done
# Результат:
# Apple Pear Potato Tomato
#
# В данном случае ставить двойные кавычки неправильно, так как они мешают сформировать список и склеивают
# все элементы в одну большую строку.
#
# ПРАВИЛЬНО
for item in $LIST; do
   echo "$item"
done
# Результат:
# Apple
# Pear
# Potato
# Tomato

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

# Пусть у нас есть пути к файлам, в именах которых есть пробелы.
ITEM_1="/home/john/files/My document.txt"
ITEM_2="/home/alice/music collection/jazz/Louis Armstrong"

# Тогда писать так было бы НЕПРАВИЛЬНЫМ
for item in $ITEM_1 $ITEM_2; do
   echo "$item"
done
# Результат:
# /home/john/files/My       
# document.txt
# /home/alice/music
# collection/jazz/Louis
# Armstrong
#
# Вместо двух вхождений мы получили 5 из-за разбиения на голые слова. Чтобы пресечь это, мы должны
# закавычить подстановки.
# ПРАВИЛЬНО (1)
for item in "$ITEM_1" "$ITEM_2"; do
   echo "$item"
done
# Результат:
# /home/john/files/My document.txt
# /home/alice/music collection/jazz/Louis Armstrong

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

ARRAY+=("$ITEM_1") # Кавычки ставить важно, чтобы не разбить строку по пробелам и не получить мнимые элементы. 
ARRAY+=("$ITEM_2")
# При подстановке массива как списка, важно использовать подстановку с '@' и использовать двойные кавычки,
# чтобы пресечь разбиение элементов по пробелам.
# ПРАВИЛЬНО (2)
for item in "${ARRAY[@]}"; do
    echo "$item"
done
# Результат
# /home/john/files/My document.txt
# /home/alice/music collection/jazz/Louis Armstrong

# Также обратим внимание, что, когда мы передаем сформированный из массива список в функцию, то простановка
# кавычек также обязательна.
# ПРАВИЛЬНО
arg_printer "${ARRAY[@]}"
# Результат:
# Arg counter: 2
# 0: '/home/john/files/My document.txt'
# 1: '/home/alice/music collection/jazz/Louis Armstrong'

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

PATH="/music collection/jazz/Louis Armstrong/Dream A Little Dream Of Me"
echo "$(echo ~)""$PATH."{wav,mp3}
# Результат:
# /home/john/music collection/jazz/Louis Armstrong/Dream A Little Dream Of Me.wav /home/john/music collection/jazz/Louis Armstrong/Dream A Little Dream Of Me.mp3

Отложенная интерпретация подстановок править

Используя обратный слеш \ можно отложить интерпретацию подстановки для позднего ее разрешения. Обычно все сводится к тому, что на некоторый слой из вызовов вы передаете экранированную подстановку, причем число экранирующих слешей рассчитывается под этот слой. Лучше всего такой прием демонстрируется в рекурсивных вызовах.

# Следующая функция вызывается рекурсивно. Она ожидает любое число параметров, причем
# последний параметр это шаблон с отложенными подстановками, а все предыдущие от него параметры
# это значения для отложенных подстановок в шаблоне.
recurse() {
    if [[ $# -gt 1 ]]; then
        shift
        recurse $(eval echo "$@")
    else
        echo "Result: $@"
    fi
}

# Теперь, если вызвать функцию например так
recurse "a" "One" "Two" "Three" "Four" "\$1_\\\$1_\\\\\\\$1_\\\\\\\\\\\\\\\$1"
# мы получим такой ответ
# Result: One_Two_Three_Four

# Такой же ответ мы получим, если вызовем функцию так
recurse "a" "One" "Two" "Three" "Four" '$1_\$1_\\\$1_\\\\\\\$1'
# Result: One_Two_Three_Four

Весь фокус в том, что функция рекурсивно вызывается 5 раз (с учетом входа в рекурсию). На первом слое мы разрешаем крайнюю левую подстановку в шаблоне, т.е. крайняя левая $1 заменяется словом One. Далее полученный после первой подстановки результат углубляется на новый уровень вместе с оставшимися аргументами, получаемыми через левый сдвиг, и операция повторяется. Операция будет повторяться до тех пор, пока в результате рекурсии не останется полностью разрешенный шаблон (если говорить точно, пока не останется последний аргумент).

В этой реализации мы вынуждены использовать мнимый аргумент (в этом примере он имеет значение a) для входа в рекурсию, так как нам не нужно, чтобы крайняя подстановка разрешилась на уровне первого вызова функции. Чтобы интерпретировать подстановку на нужном слое, мы используем символ экранирования нужное число раз. Обратите внимание, что нужное число слешей рассчитывается по-разному для двойных и одинарных кавычек. В случае двойных кавычек мы всегда должны держать в уме то, что слеш тоже интерпретируется внутри кавычек, поэтому после первого слоя каждый последующий слеш должен экранироваться дополнительно. Таким образом, число слешей для двойных кавычек рассчитывается как  , где   – номер слоя. В случае одинарных кавычек формула остается такой же, но слои начинают отсчитываться с нуля, а не с единицы.

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

# $1    => Число подстановок в шаблоне.
# $2... => Значения для подстановок в количестве, указанном в $1.
# После всех значений идет шаблон в произвольном виде.
recurse1() {
    local -i remain=$1
    shift
    if [[ $remain -ne 0 ]]; then
        ((__SHIFT+=1))
        recurse1 $((remain-1)) $(eval echo "$@")
    else
        shift $__SHIFT
        unset __SHIFT
        echo "Result: $@"
    fi
}

# В этой реализации вызовы будут такими
recurse1 "4" "One" "Two" "Three" "Four" "\$1 \\\$2 \\\\\\\$3 \\\\\\\\\\\\\\\$4"   # Теперь в шаблоне можно использовать пробелы
# Result: One Two Three Four

# Или так
recurse1 "4" "One" "Two" "Three" "Four" '$1 \$2 \\\$3 \\\\\\\$4'
# Result: One Two Three Four

Отложенная интерпретация на практике используется очень редко из-за большого числа нюансов в ее использовании и в сложности отладки. Тем не менее, возможность выстраивать слои дает некоторые возможности для абстрактного программирования.

Взяв за основу предыдущий код, можно реализовать что-то похожее на функцию printf, где в произвольном формате мы подставляем значения, при этом число аргументов для функции не оговорено заранее.

formatter() {
    __resolve() {
        local -i remain=$1
        shift
        if [[ $remain -ne 0 ]]; then
            __resolve $((remain-1)) $(eval echo "$@")
        else
            while true; do
                case "$1" in
                    --) shift; break ;;
                    *) shift ;;
                esac
            done
            echo "$@"
        fi
    }
    local args
    local format 
    local remain
    args=$(getopt -o '-f:' -- "$@") || return
    eval "set -- $args"
    while true; do
        case "$1" in
            -f)
                format=$format$2; shift 2
            ;;
            --)
                shift; break
            ;;
            *)
                break
            ;;
        esac
    done
    remain=("$@")
    __resolve ${#remain[@]} "${remain[@]}" -- $format
}

# Например такой вызов
formatter -f '$1 [\$2]:\\\$3: \\\\\\\\\\\\\\$4' -- "$(date +%T)" "info" "0" "Hello World! How are you?"

Результат работы функции будет такой:

12:11:32 [info]:0: Hello World! How are you?



← Эмуляция ссылочной адресации Команды →