ServerAID
Найти гайд, команду, тег… ⌘ K
Shell и скрипты

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.

Похожие материалы

Shell и скрипты

sed на Linux: замена строк, правка in-place и регулярки

sed (stream editor) — стандартная команда Linux для построчной обработки текста: замена, удаление, печать строк по шаблону. Работает с потоком (pipe) или файлами, поддерживает in-place редактирование. Базовый инструмент шелл-скриптов после `grep` и `awk`.

Редакция
Shell и скрипты

wget на Linux: скачивание файлов, дозакачка и зеркало сайта

wget — стандартная утилита Linux для скачивания файлов по HTTP, HTTPS и FTP. Умеет дозакачивать прерванное, ходить рекурсивно по ссылкам, ограничивать скорость и работать без интерактива — поэтому это базовый инструмент скриптов установки, скачивания дистрибутивов и зеркалирования сайтов.

Редакция
Shell и скрипты

bash if else elif fi: условия в скриптах с примерами

Условие в bash — это `if [[ … ]]; then … fi`. Разбираем разницу между `[[ ]]` и `[ ]`, сравнение строк и чисел, проверки файлов через `-f`/`-d`/`-x`, цепочки `elif`, связку через `&&`/`||` и боевые примеры из реальных скриптов: проверка root, проверка существования файла, проверка наличия команды.

Редакция
Безопасность

Let's Encrypt и certbot на Ubuntu: бесплатный SSL для nginx

Let's Encrypt — бесплатные SSL/TLS-сертификаты для любого домена. На Ubuntu выпускаются через утилиту certbot одной командой; автообновление работает само через systemd-таймер. Разбираем установку, выпуск для nginx, DNS-challenge для wildcard-сертификатов и грабли с rate-limit.

Редакция