bash массивы: создание, перебор, ассоциативные и грабли с IFS
В bash есть два типа массивов: индексированные (нумерованные) и ассоциативные (хеши). Разбираем создание, чтение элементов, перебор, slicing, добавление/удаление, передачу в функцию и главные грабли — кавычки, `$@` vs `$*`, разделитель IFS. Конструкции на bash 5+, который идёт по умолчанию на Ubuntu 22.04 / 24.04.
bash массивы: индексированные и ассоциативные, рабочие примеры
В bash есть два типа массивов: индексированные (нумерованные) и ассоциативные (хеши). Разбираем создание, чтение элементов, перебор, slicing, добавление/удаление, передачу в функцию и главные грабли — кавычки, $@ vs $*, разделитель IFS. Конструкции на bash 5+, который идёт по умолчанию на Ubuntu 22.04 / 24.04.
Создаём индексированный массив
Самый простой синтаксис — присваивание со скобками:
fruits=("apple" "banana" "cherry")
Элементы разделены пробелами; кавычки нужны, если внутри элемента — пробел. Без кавычек ("hello world") стало бы массивом из двух элементов: hello и world.
Пустой массив:
empty=()
Создание по одному элементу:
flags=()
flags+=("--quiet")
flags+=("--no-color")
Можно сразу указать индекс:
sparse=([0]="zero" [10]="ten" [20]="twenty")
bash-индексы — целые числа от 0, как в C. С версии bash 4 индексированные массивы могут быть «разреженными»: индексы не обязаны идти подряд (как в sparse выше).
#!/bin/bash обязательно: на /bin/sh (это dash в Ubuntu) массивов нет.
Читаем элементы и длину
fruits=("apple" "banana" "cherry")
echo "${fruits[0]}" # apple
echo "${fruits[1]}" # banana
echo "${fruits[-1]}" # cherry — отрицательный индекс с bash 4.3
echo "${fruits[@]}" # apple banana cherry — все элементы
echo "${#fruits[@]}" # 3 — длина массива
echo "${#fruits[0]}" # 5 — длина первого элемента (строки "apple")
echo "${!fruits[@]}" # 0 1 2 — индексы (полезно для разреженных)
Грабли: без [@] обращение $fruits или ${fruits} вернёт только первый элемент. Это частая ошибка новичков:
echo "$fruits" # apple — только первый!
echo "${fruits[@]}" # apple banana cherry
Разница [@] vs [*]:
"${fruits[@]}"в кавычках — каждый элемент как отдельное слово. Стандартный выбор для перебора."${fruits[*]}"в кавычках — все элементы склеены в одну строку через первый символIFS(по умолчанию пробел).
Без кавычек оба варианта ведут себя одинаково (с разделением по пробелам) — но это редко то, что вы хотите.
Перебираем массив
Каноничный паттерн — for + [@]:
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "Берём $fruit"
done
Кавычки вокруг "${fruits[@]}" критичны: без них элементы с пробелами сломаются. for fruit in ${fruits[@]} без кавычек разделит «two words» на два отдельных элемента.
Перебор с индексом — через ${!array[@]}:
for i in "${!fruits[@]}"; do
echo "[$i] = ${fruits[$i]}"
done
Это нужно для разреженных массивов или когда вам важен сам индекс. Для плотных подойдёт классический C-style:
for ((i = 0; i < ${#fruits[@]}; i++)); do
echo "[$i] = ${fruits[$i]}"
done
Перебор с условием — через bash if/else:
for fruit in "${fruits[@]}"; do
if [[ "$fruit" == "banana" ]]; then
continue
fi
echo "$fruit"
done
Добавляем и удаляем элементы
Добавление в конец — через +=:
fruits+=("date")
fruits+=("elderberry" "fig") # сразу несколько
echo "${#fruits[@]}" # 5
Удаление по индексу — через unset:
fruits=("apple" "banana" "cherry")
unset 'fruits[1]'
echo "${fruits[@]}" # apple cherry
echo "${!fruits[@]}" # 0 2 — индекс 1 пропал, массив теперь разреженный
Если разреженность мешает — пересоберите:
fruits=("${fruits[@]}")
echo "${!fruits[@]}" # 0 1 — снова плотный
Полная очистка:
unset fruits # удалить переменную совсем
fruits=() # сделать пустой, но переменная остаётся
Slicing — срезы
Берём подмассив по индексу и длине:
nums=(zero one two three four five)
echo "${nums[@]:2}" # two three four five — с индекса 2 до конца
echo "${nums[@]:2:3}" # two three four — 3 элемента начиная с индекса 2
echo "${nums[@]: -2}" # four five — последние 2 (пробел перед минусом обязателен!)
echo "${nums[@]:0:3}" # zero one two — первые 3
Синтаксис ${array[@]:offset:length}. Если length опущен — до конца. Отрицательный offset — с конца, но между : и - нужен пробел, иначе bash примет за оператор :- (default value).
Ассоциативные массивы (хеши)
С bash 4 появились ассоциативные массивы — словари «ключ → значение». Объявляются через declare -A:
declare -A user
user["name"]="Alice"
user["email"]="alice@example.com"
user["age"]=30
echo "${user[name]}" # Alice
echo "${user[email]}" # alice@example.com
Объявление с инициализацией:
declare -A config=(
["port"]="8080"
["host"]="127.0.0.1"
["debug"]="true"
)
Ключи — строки (могут быть с пробелами в кавычках). Значения — тоже строки (bash не различает строки и числа).
Перебор:
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
Все ключи: ${!config[@]}. Все значения: ${config[@]}. Длина: ${#config[@]}.
Проверка наличия ключа:
if [[ -v config[port] ]]; then
echo "port задан: ${config[port]}"
fi
-v — bash 4.2+. На старых версиях используют [[ -n "${config[port]+x}" ]] — трюк через «значение или x».
Удаление пары:
unset 'config[debug]'
Передаём массив в функцию
Это самая болезненная тема в bash. Прямо передать массив нельзя — функция получает только строки.
Способ 1: через расширение "${array[@]}".
print_all() {
for item in "$@"; do
echo "→ $item"
done
}
fruits=("apple" "banana" "cherry pie")
print_all "${fruits[@]}"
"$@" внутри функции — все аргументы, включая разделение по словам. Главное — оборачивать в кавычки и при вызове, и при использовании.
Способ 2: через nameref (bash 4.3+).
modify_array() {
local -n ref=$1
ref+=("modified")
}
fruits=("apple" "banana")
modify_array fruits
echo "${fruits[@]}" # apple banana modified
local -n ref=$1 создаёт «ссылку» — изменения через ref отражаются на исходном массиве. Удобно, когда нужно модифицировать переданный массив.
Грабли: print_all $fruits без [@] передаст только первый элемент. print_all "$fruits" — то же самое, только в кавычках. Всегда "${fruits[@]}" при передаче.
Грабли с кавычками и IFS
Кавычки. Правило: всегда оборачивайте обращение к массиву в "...", иначе bash сделает word splitting:
files=("my photo.jpg" "report.pdf")
for f in ${files[@]}; do # БЕЗ кавычек — сломается!
echo "$f"
done
# my
# photo.jpg
# report.pdf
for f in "${files[@]}"; do # С кавычками — корректно
echo "$f"
done
# my photo.jpg
# report.pdf
IFS. Internal Field Separator — по умолчанию <пробел><таб><newline>. По нему bash разбивает строки на массивы. Удобный приём — split строки в массив:
csv="alice,bob,charlie"
IFS=',' read -ra names <<< "$csv"
echo "${names[@]}" # alice bob charlie
echo "${#names[@]}" # 3
read -r без a читает в переменную; с -a — в массив. -r отключает интерпретацию backslash. <<< — here-string, передаёт строку на stdin.
$@ vs $* для массивов. Аналогично позиционным аргументам в скриптах:
"${array[@]}"— каждый элемент как отдельный аргумент."${array[*]}"— все элементы как одна строка через первый символ IFS.
fruits=("apple" "banana cream" "cherry")
printf "[%s]\n" "${fruits[@]}"
# [apple]
# [banana cream]
# [cherry]
printf "[%s]\n" "${fruits[*]}"
# [apple banana cream cherry]
Для перебора почти всегда хотите [@].
Боевые примеры
Загрузка строк из вывода команды.
mapfile -t lines < <(ls /var/log/*.log)
echo "Найдено ${#lines[@]} логов"
for log in "${lines[@]}"; do
echo "Обрабатываем $log"
done
mapfile (или readarray) читает stdin построчно в массив; -t убирает trailing newline. < <(command) — process substitution, кладёт вывод команды на stdin без подоболочки.
Динамические флаги для команды.
opts=()
opts+=("-v")
[[ "$DEBUG" == "1" ]] && opts+=("--debug")
[[ -n "$LOGFILE" ]] && opts+=("--log" "$LOGFILE")
myapp "${opts[@]}"
Раскрытие "${opts[@]}" подставит флаги отдельными аргументами — без кавычек или со склейкой * оно бы сломалось на флагах со значениями.
Хеш кэшированных результатов.
declare -A user_cache
get_user() {
local id=$1
if [[ -v user_cache[$id] ]]; then
echo "${user_cache[$id]}"
return
fi
local result=$(curl -s "https://api.example.com/users/$id")
user_cache[$id]=$result
echo "$result"
}
Поиск файлов в массив.
mapfile -t -d '' configs < <(find /etc -name "*.conf" -print0 2>/dev/null)
echo "Нашли ${#configs[@]} конфигов"
-d '' указывает разделитель — нулевой байт (\0 от find -print0), безопасно для имён с пробелами и переводами строк. Подробнее про find — глоссарий find.
Частые вопросы
Как создать массив в bash
array=(элемент1 элемент2 элемент3). Элементы с пробелами оборачивайте в кавычки: array=("hello world" "second item"). Пустой массив: array=(). Добавление: array+=("new"). Шебанг скрипта обязательно #!/bin/bash — в /bin/sh (dash на Ubuntu) массивов нет.
Как получить длину массива в bash
${#array[@]} — количество элементов. ${#array[0]} — длина первого элемента (как строки). ${!array[@]} — список индексов. Перебор по индексам: for i in "${!array[@]}".
Как перебрать массив в bash
for item in "${array[@]}"; do ... done. Кавычки вокруг "${array[@]}" обязательны — без них элементы с пробелами разобьются. С индексами: for i in "${!array[@]}"; do echo "$i: ${array[$i]}"; done.
Что такое ассоциативные массивы в bash
Хеш-таблицы со строковыми ключами. Появились в bash 4. Объявляются через declare -A name, обращение — ${array[ключ]}. Перебор ключей: for key in "${!array[@]}". Удобны для конфигов, кэшей, словарей перевода.
Как добавить элемент в массив bash
Через array+=("новый"). Можно сразу несколько: array+=("a" "b" "c"). Можно по индексу: array[10]="по индексу". В ассоциативный массив — через ключ: assoc["key"]="value".
Как разделить строку на массив в bash
IFS=',' read -ra parts <<< "$string" — разделит $string по запятой на массив parts. Для разделения по строкам — mapfile -t lines <<< "$multiline". По произвольному разделителю — комбинация IFS=<разделитель> + read -ra.
Чем отличается ${array[@]} от ${array[*]}
Только в кавычках. "${array[@]}" — каждый элемент как отдельное слово (для перебора, передачи в команды). "${array[*]}" — все элементы склеены в одну строку через первый символ IFS. Без кавычек оба ведут себя одинаково. Для большинства задач берите [@].
Можно ли передать массив в функцию bash
Прямо — нет, но есть два пути. Через расширение: func "${array[@]}", внутри функции работайте с "$@". Через nameref (bash 4.3+): func arrayname, внутри local -n ref=$1 — будет ссылка на исходный массив с возможностью модификации.
Что запомнить
- Всегда оборачивайте
"${array[@]}"в кавычки — без них элементы с пробелами ломаются. - Без
[@]или[*]обращение к массиву даёт только первый элемент. Это не баг, это by design. - Длина —
${#array[@]}. Индексы —${!array[@]}. Все элементы —${array[@]}. - Ассоциативные массивы требуют
declare -A(bash 4+). Это словари «ключ → значение». - Передать массив в функцию — через
func "${array[@]}"+ работа с"$@". Для модификации — черезlocal -n ref=$1. - Split строки в массив —
IFS=',' read -ra parts <<< "$str". Чтение файла построчно —mapfile -t lines < file. #!/bin/bashобязательно. На/bin/sh(dash) массивов нет вовсе.
Обложка: фото Bernd Dittrich с Unsplash, лицензия Unsplash.