Переменная в bash — это строка с именем. Оболочка подставляет её значение везде, где встречает $имя, а затем (если нет кавычек) режет результат на слова и раскрывает шаблоны вроде *. Поэтому кавычки здесь важнее, чем кажется.
Синтаксис
имя=значение # без пробелов вокруг =
echo "$имя" # подстановка значения
echo "${имя}_suffix" # фигурные скобки отделяют имя от текста
export имя # сделать переменную видимой дочерним процессам
unset имя # удалить переменную
Имя — латинские буквы, цифры и _, не может начинаться с цифры. Регистр важен: Name и NAME — разные переменные.
Кавычки
| Запись | Что происходит |
|---|
"…" | подстановки $var, $(…), $((…)) работают, но результат не режется на слова и не раскрывается как шаблон |
'…' | всё буквально, никаких подстановок; одинарную кавычку внутри не поставить |
$'…' | буквально, но понимает escape-последовательности: $'a\tb', $'\n' |
\символ | экранировать один символ: \$, \", \\, \ |
| без кавычек | значение режется по $IFS (пробел, табуляция, перевод строки) и раскрывается как glob |
Объявление: declare и компания
| Команда | Что делает |
|---|
export var=x / declare -x var | переменная попадёт в окружение дочерних процессов; export -n var — убрать оттуда |
local var=x | переменная видна только внутри функции |
readonly var=x / declare -r | константа: переприсвоить нельзя (в скрипте это ошибка и выход) |
declare -i n | целое число: n=5+3 сохранит 8 |
declare -l / declare -u | при присваивании переводить в нижний / верхний регистр |
declare -a arr / declare -A map | обычный / ассоциативный массив |
declare -p var | показать, как объявлена переменная, с типом и значением |
VAR=x команда | переменная только для этой одной команды |
Подстановки параметров
| Форма | Что даёт |
|---|
${var:-x} | значение var, а если она пуста или не задана — x |
${var-x} | x, только если var не задана вовсе (пустая строка считается значением) |
${var:=x} | как :-, но ещё и присваивает x переменной |
${var:+x} | x, если var непуста, иначе пустая строка |
${var:?текст} | ошибка с текстом и выход из скрипта, если var пуста |
${#var} | длина строки в символах |
${var#шаблон} / ${var##шаблон} | срезать с начала самое короткое / самое длинное совпадение |
${var%шаблон} / ${var%%шаблон} | срезать с конца самое короткое / самое длинное совпадение |
${var/a/b} / ${var//a/b} | заменить первое / все вхождения a на b; ${var//a/} — удалить |
${var/#a/b} / ${var/%a/b} | заменить a, только если строка с него начинается / им заканчивается |
${var:pos} / ${var:pos:len} | подстрока с позиции pos (с нуля); ${var: -3} — последние 3 символа (пробел перед - обязателен) |
${var^} / ${var^^} | первую букву / всю строку в верхний регистр |
${var,} / ${var,,} | первую букву / всю строку в нижний регистр |
${!var} | косвенная ссылка: значение переменной, имя которой лежит в var |
${var@Q} | значение в кавычках, пригодное для повторного ввода в оболочку |
Шаблоны в #, %, / — это glob (*, ?, […]), а не регулярные выражения.
Подстановка команд и арифметика
| Форма | Что даёт |
|---|
$(команда) | stdout команды; завершающие переводы строк срезаются. Старая форма — `команда` |
$((выражение)) | результат целочисленного выражения |
((выражение)) | вычислить без подстановки; код возврата 0, если результат не ноль |
let "z=2*3" | то же, что ((z=2*3)) |
Операторы в $((…)): + - * / % **, ++ --, += -= *= /=, сравнения < <= > >= == !=, логика && || !, побитовые & | ^ ~ << >>, тернарный a ? b : c. Внутри $((…)) знак $ перед именами можно не писать. Числа: 0x1F — шестнадцатеричное, 010 — восьмеричное, 2#101 — двоичное, 10#08 — явно десятичное.
Массивы
| Запись | Что даёт |
|---|
arr=(a b "c d") | создать массив из трёх элементов |
arr[5]=x | присвоить по индексу (дырки допустимы) |
arr+=(e f) | добавить элементы в конец |
${arr[0]} / ${arr[-1]} | первый / последний элемент |
"${arr[@]}" | все элементы, каждый отдельным словом |
"${arr[*]}" | все элементы одной строкой через первый символ $IFS |
${#arr[@]} | число элементов |
${!arr[@]} | список индексов (или ключей ассоциативного массива) |
"${arr[@]:1:2}" | срез: 2 элемента начиная с индекса 1 |
unset 'arr[1]' | удалить элемент (индексы не сдвигаются) |
declare -A m; m[key]=v | ассоциативный массив: ключи — строки |
mapfile -t arr < файл | прочитать файл в массив, по строке на элемент |
IFS=, read -ra arr <<< "a,b,c" | разрезать строку по разделителю в массив |
Специальные переменные
| Переменная | Значение |
|---|
$0 | имя скрипта |
$1 … $9, ${10} | позиционные аргументы; shift сдвигает их влево |
$# | число аргументов |
"$@" | все аргументы, каждый отдельным словом — почти всегда нужен именно он |
"$*" | все аргументы одной строкой |
$? | код возврата последней команды |
$$ | PID текущей оболочки |
$! | PID последнего фонового процесса |
$_ | последний аргумент предыдущей команды |
$PIPESTATUS | массив кодов возврата частей последнего конвейера |
$BASH_REMATCH | группы последнего совпадения [[ … =~ … ]] |
$IFS | разделители слов (по умолчанию пробел, табуляция, перевод строки) |
$PWD, $OLDPWD, $HOME, $UID | текущий и предыдущий каталог, домашний каталог, числовой UID |
$RANDOM, $SECONDS, $LINENO | случайное число 0–32767, секунды с запуска оболочки, номер строки |
Примеры
name=world
echo "hello $name" # hello world
echo 'hello $name' # hello $name — в одинарных кавычках подстановок нет
# значение по умолчанию
echo "${PORT:-8080}"
# обязательный параметр скрипта: без него — ошибка и выход
: "${1:?укажи имя файла}"
# разобрать путь без basename/dirname
p=/var/log/nginx/access.log.gz
echo "${p##*/}" # access.log.gz — имя файла
echo "${p%/*}" # /var/log/nginx — каталог
echo "${p%.gz}" # /var/log/nginx/access.log — без расширения
echo "${p##*.}" # gz — только расширение
echo "${p%%.*}" # /var/log/nginx/access — до первой точки
# замена и регистр
s="a-b-c"
echo "${s//-/_}" # a_b_c
echo "${s^^}" # A-B-C
# вывод команды в переменную
today=$(date +%F)
# арифметика
echo $((7 / 2)) $((7 % 2)) $((2 ** 10)) # 3 1 1024
i=5; ((i++)); ((i += 10)); echo "$i" # 16
# массивы
arr=(one two "three four")
echo "${#arr[@]}" # 3
arr+=(five)
for x in "${arr[@]}"; do echo "<$x>"; done
# ассоциативный массив
declare -A port=([http]=80 [https]=443)
for k in "${!port[@]}"; do echo "$k=${port[$k]}"; done
# форматировать число в переменную без подоболочки
printf -v id '%05d' 42 # id=00042
# переменная только для одной команды
LC_ALL=C sort file.txt
Подводные камни
- Пробелы вокруг
= ломают присваивание. a = 1 — это запуск команды a с аргументами = и 1 (command not found).
- Переменная без кавычек режется на слова. Если
f="my file.txt", то ls $f ищет два файла my и file.txt. Пиши "$f" почти всегда.
"${arr[@]}" и ${arr[@]} — разные вещи. Без кавычек элемент "three four" развалится на два. То же с "$@" против $*.
- Арифметика только целочисленная.
$((0.5 + 1)) — синтаксическая ошибка, $((7/2)) — это 3. Дроби считай в awk.
- Ведущий ноль — восьмеричное число.
$((010)) равно 8, а $((08)) — ошибка «value too great for base». Для чисел вроде 08 из даты пиши $((10#08)).
- Без
export дочерний процесс переменную не видит. x=1; bash -c 'echo $x' печатает пустую строку.
$(…) срезает завершающие переводы строк, но не ведущие пробелы. А local x=$(команда) теряет код возврата команды: $? будет кодом local.
${var: -1} и ${var:-1} — разные вещи. Без пробела это «значение по умолчанию 1», а не «последний символ».
- Изменения переменных внутри конвейера теряются: каждая часть
| работает в подоболочке.
См. также
conditions-loops · redirection · exit-codes · shellcheck