Переменные в bash

Базовый синтаксис

Присваивание значения переменной:

VAR=something

Для получения значения переменной в bash нужно объявить знак доллара ($) и имя переменной. Такой вариант получения переменной удобен в работе со строками, а bash ориентирован именно на это, поэтому в нем используется именно такой подход.
Наглядно:

MSG="Error: $FILE not found"
# Переменные в bash

Юзай двойные кавычки

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

Пробелы не нужны

В операции присваивания пробелы вокруг ”=” не допускаются, ибо синтаксически вся конструкция присваивания должна быть одним “словом”.

В строках, где сложно определить, где заканчивается имя переменной, лучше использовать полный синтаксис - фигурные скобки вокруг имени переменной: ${FILE}.


Подстановки/правки для возвращаемых значений переменной

Длина значения переменной

Фигурные скобки используются во многих синтаксических конструкциях, например, решетка (#) перед именем переменной ${#FILE} возвращает длину значения (в символах). Наглядно:

FILE=some_file
echo "${#FILE}"
# 9

Таким же образом работают и остальные синтаксические конструкции. Можно задать правила подстановки/правки, которые будут влиять на возвращаемое значение, но не на само значение переменной (есть исключение: Изменение значения).
Следующие несколько секций об этих правилах.

Short вариант команды basename или удаление пути/префикса

Как ты знаешь скрипт можно вызывать разными способами, юзая:

  • ./scriptnameиз current dir
  • полный путь /home/autistic/scripts/scriptname
    вызов скрипта из директорий в PATH считается использованием абсолютного пути
  • относительный путь.

$0 будет хранить путь, который и использовался для вызова скрипта. А для идентификации скрипта, к примеру, в сообщении о порядке его использования (usage message) достаточно базового имени (тот же basename) без пути к нему:

echo "usage: ${0##*/} inputfile [outputfile]"

Это пример удаления символов в начале/слева (префикса) строки. То есть, чтобы удалить префикс, нужно добавить в конструкцию ${VAR} решетку (#) и шаблон для удаления префикса.
${VAR#abc} удалит символы abc, если c них начинается значение $VAR
${VAR#*abc} удалит всё до символов abc ВКЛЮЧИТЕЛЬНО, кратчайшее совпадение (#)
${VAR##*abc} тоже самое, но выбирается самое длинное совпадение ()
Наглядно:

VAR="shit_abc_abc.png"
echo "${VAR#*abc}"   # _abc.png
echo "${VAR##*abc}"  # .png

Удаление пути к файлу:

filename="./file"; echo "${filename#*/}"         # file
filename="./file"; echo "${filename##*/}"        # file
 
filename="../dir/file"; echo "${filename#*/}"    # dir/file
filename="../dir/file"; echo "${filename##*/}"   # file
 
filename="/usr/bin/env"; echo "${filename#*/}"   # usr/bin/env
filename="/usr/bin/env"; echo "${filename##*/}"  # env

Short вариант команды dirname или удаление суффикса

Подобно #, удаляющему префикс, знак % удаляет суффикс (символы справа).
И так же, двойной знак %% (вместе с *) удаляет всё после самого длинного совпадения.

Наглядно:

filename="img.123.jpg"; echo "${filename%.*}"             # img.123
filename="img.123.jpg"; echo "${filename%%.*}"            # img
 
filename="./pics/as.png"; echo "${filename%/*}"           # ./pics
filename="./pics/as.png"; echo "${filename%%/*}"          # .
 
filename="/home/user/pics/as.png"; echo "${filename%/*}"  # /home/user/pics
filename="/home/user/pics/as.png"; echo "${filename%/*}"  # 

Эти выражения не полностью эквивалентны команде dirname: применительно к /file последнее выражение вернет пустую строку, а dirname - / (косую черту).
Но если нужна абсолютная схожесть с dirname в этих случаях, можно просто добавить слэш к выражению - ${0%/*}/ - и результат будет заканчиваться слэшем.

"Запоминалка" для символов

Чтобы проще запомнить, что # удаляет префикс, а % - суффикс, в книге предлагается аналогия с клавишами, где символ # (Shift+3) находится слева от % (Shift+5).

Модификаторы для преобразования в верхний/нижний регистры

С помощью ^ или ^^ можно преобразовать первый или все символы в верхний регистр, а с помощью , или ,, - в нижний регистр.
Наглядно:

# ^ и ^^
TXT="silly little things"; echo "${TXT^}"   # Silly little things
TXT="silly little things"; echo "${TXT^^}"  # SILLY LITTLE THINGS
 
# , и ,,
TXT="Dumb Shit"; echo "${TXT,}"             # dumb Shit
TXT="CLOWN ASS CAR"; echo "${TXT,,}"        # clown ass car

Info

А можно сразу объявить переменные в верхнем/нижнем регистрах:

declare -u var  # UPPER
declare -l var  # lower

Значения этих переменных будут всегда преобразовываться в указанный в параметрах declare регистр.

Модификатор замены / (слэш)

Выполняет люблю замену в любом месте строки (как в sed, например).
Нужно указать после / или // (для замены первого или всех совпадений, соответственно) искомый шаблон и через еще один слэш - строку замены.
НЕ ТРЕБУЕТСЯ завершающий слэш.

Наглядно:

FN="name with spaces.txt"; echo "${FN/ /_}"   # name_with spaces.txt
FN="name with spaces.txt"; echo "${FN// /_}"  # name_with_spaces.txt
FN="name with spaces.txt"; echo "${FN// /}"   # namewithspaces.txt
 
FN="/usr/bin/env"; echo "${FN/\//}"           # usr/bin/env
FN="/usr/bin/env"; echo "${FN//\//}"          # usrbinenv

Модификатор извлечения подстроки (substring)

Нужно добавить : (двоеточие) после имени переменной, указать порядковый номер символа (отсчет идет от 0) (начало подстроки), поставить еще одно : (двоеточие) и указать длину извлекаемой подстроки.

Наглядно:

FN="/usr/bin/env"; echo "${FN:0:1}"  # /
FN="/usr/bin/env"; echo "${FN:1:1}"  # u
FN="/usr/bin/env"; echo "${FN:5:2}"  # bi
FN="/usr/bin/env"; echo "${FN:8:6}"  # /env
FN="/usr/bin/env"; echo "${FN:9}"    # env

Условные подстановки

Есть и условные подстановки, выполняющиеся при определенных условиях. Их особенность - : (двоеточие), после которого идет другой спец. символ: - (минус), + (плюс)
или = (знак равенства). Они проверяют, была ли создана переменная и имеет ли она значение. Несозданной (неустановленной) считается переменная, которой еще не было присвоено значение или которая была удалена с помощью команды unset. Позиционный параметр ($1, $2, …) считается несозданным, если пользователь не передал такой параметр.
Если из этих условных подстановок убрать двоеточие они выполнятся, если переменная не создана; для созданных переменных вернутся их значения (даже если это пустая строка).

Дефолтные значения переменных

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

LEN="${1:-5}"
some_command | head -n "$LEN"
# или
LEN="${1:-$LEN}"

Выражение выше присвоит переменной LEN значение $1, либо 5, если последний не был указан.

Идиома соединения

Условная подстановка со знаком + проверяет, присвоено ли переменной какое-то значение (непустое) если присвоено, возвращает указанное значение.
Встает вопрос: зачем возвращать какое-то другое значение, если переменная и так имеет свое??
С помощью такой подстановки можно создать, например, список значений, разделенных запятыми, без использования if для недопущения лишних запятых в начале/конце списка.

Идиома соединения наглядно (на примере создания списка значений с запятыми):

for filename in * ; do
	SEP="${LIST:+,}"  # SEP - разделитель
	# Разделитель равен ",", если $LIST не пустой
	LIST="${LIST}${SEP}${filename}"
	# В список добавляются значения $filename через запятую ($SEP)
done

Изменение значения

Это то самое исключение, которое может менять значение переменной, в отличие от других подстановок. Это выражение ${VAR:=value}, которая действует так же, как и ${VAR:-value}, но ПРИСВОИТ переменной указанное значение и вернет это значение, если переменная не была еще создана или имела пустое значение (в то время, как ${VAR:-value} просто возвращает указанное значение, не присваивая его).
Об этом исключении написал просто справедливости ради 🤓👆, ибо на деле используют эту идиому редко, потому что она не работает с позиционными параметрами ($1, $2, …).


Переменная $RANDOM

Из man bash:
При каждом обращении к этой переменной генерируется случайное число от 0 до 32767. Присвоение значения этой переменной запускает генератор случайных чисел.

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

Наглядно на примере выбора случайного элемента из списка:

declare -a some_list
some_list=(foo bar baz one two "three four")
 
range="${#some_list[@]}"
random=$(( $RANDOM % $range ))  # от 0 до числа длины списка
 
echo "range = $range, random = $random, choice = ${some_list[$random]}"
# range = 6, random = 2, choice = baz
 
 
# более короткий, но не сильно читаемый вариант
echo "choice = ${some_list[$(( $RANDOM % ${#some_list[@]} ))]}"
# choice = three four

Можно еще такой код встретить:

TEMP_DIR="$TMP/tmp_dir.$RANDOM"
[[ -d "$TEMP_DIR" ]] || mkdir "$TEMP_DIR"

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

$RANDOM недоступна в dash

В некоторых дистрибутивах ссылка /bin/sh указывает на dash, где не работает переменная $RANDOM. Актуальные версии Debian/Ubuntu используют dash, ибо он позволяет быстрее загружаться за счет меньшего объема и скорости по сравнению с bash.


Пару слов о подстановке команд

Команды внутри $( ) работают в подоболочке.

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

for arg in $(cat /some/file)
for arg in $(< /some/file)    # FASTER

Первый вариант - по сути, cat abuse.

Вложенная подстановка команд

Использование “ при вложенной подстановке команд (да и при обычной подстановке 🤢) выглядит особенно уродливо и чревато ошибками из-за сложного синтаксиса.

### Просто работает
echo $(echo $(echo $(echo inside)))
# inside
 
### Ошибка
echo `echo `echo `echo inside```
# echo inside
 
### Работает, но 🤢🤢
echo `echo \`echo \\\`echo inside\\\`\``
# inside

Соус: Книга “Идиомы Bash Глава 4. Язык переменных

bash