Массивы: indexed и associative
Bash долго был языком одиночных переменных. До версии 2.0 у тебя были только строки. Сейчас в bash есть массивы (indexed) и ассоциативные массивы (associative, появились в bash 4.0, 2009). Это не Python list/dict, но 80% типичных DE-задач они решают: список файлов для обработки, счётчики по категориям, маппинги “имя -> путь”, дедупликация.
Без массивов Junior пишет циклы через $IFS-разбиение строк, и каждый скрипт превращается в минное поле. С массивами — код становится читаемым, надёжным и быстрым (массивы в памяти, в отличие от cat файлов).
В этом уроке: indexed array, associative array, идиомы для DE — group-by counter, дедуп, lookup table.
Indexed arrays: основы
Indexed array — это массив с ключами-числами, начиная с 0.
# Создание:
fruits=(apple banana cherry)
# Доступ к элементу:
echo "${fruits[0]}" # apple
echo "${fruits[1]}" # banana
# Все элементы:
echo "${fruits[@]}" # apple banana cherry
# Длина:
echo "${#fruits[@]}" # 3
ВСЕГДА используй "${arr[@]}" в двойных кавычках — это сохраняет элементы как отдельные слова. Без кавычек bash сделает word splitting и apple banana cherry превратятся в три отдельных слова (а если в элементе есть пробел — разобьётся на части). Это правило номер один при работе с массивами.
Доступ к индексам
Создание разными способами
# Литерал:
arr=(one two three)
# По индексу (можно с пропусками):
arr[0]="zero"
arr[10]="ten"
echo "${#arr[@]}" # 2 (только определённые элементы)
echo "${!arr[@]}" # 0 10
# Из вывода команды:
files=( $(ls /tmp/*.csv) ) # ОПАСНО: word splitting на пробелы
mapfile -t files < <(find /tmp -name '*.csv') # БЕЗОПАСНО
# readarray (синоним mapfile, bash 4+):
readarray -t lines < input.txt # каждая строка файла — элемент
mapfile -t (или readarray -t) — золотой стандарт для чтения построчного вывода в массив. -t убирает trailing newline у каждой строки. Безопаснее, чем arr=( $(cmd) ), потому что не подвержен word splitting.
Append (добавление элементов)
arr=(one two)
arr+=(three) # arr = (one two three)
arr+=(four five) # arr = (one two three four five)
arr[${#arr[@]}]="six" # явный append через индекс длины
+= — главный оператор для построения массивов в цикле:
errors=()
for log in /var/log/app/*.log; do
if grep -q ERROR "$log"; then
errors+=("$log")
fi
done
echo "Found ${#errors[@]} files with errors"
printf '%s\n' "${errors[@]}"
Iteration по массивам
fruits=(apple banana cherry)
# По значениям:
for f in "${fruits[@]}"; do
echo "Processing: $f"
done
# По индексам:
for i in "${!fruits[@]}"; do
echo "$i: ${fruits[$i]}"
done
# C-style:
for ((i=0; i<${#fruits[@]}; i++)); do
echo "$i: ${fruits[$i]}"
done
Реальный DE-кейс: list of files to process
Задача: обработать все CSV-файлы за вчерашний день из директории, безопасно по отношению к пробелам в именах.
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
# Найти все вчерашние CSV:
mapfile -t files < <(find /data/incoming -name '*.csv' -mtime -1 -type f)
echo "Found ${#files[@]} files to process"
# Если пусто — выйти:
if [ "${#files[@]}" -eq 0 ]; then
echo "Nothing to process, exiting"
exit 0
fi
# Обработать каждый:
for f in "${files[@]}"; do
echo "Processing: $f"
python3 /opt/etl/process.py "$f"
done
echo "All ${#files[@]} files processed"
Почему это правильно:
mapfileкорректно читает имена с пробелами, не подвержен IFS-разбиению."${files[@]}"в кавычках сохраняет элементы как отдельные слова.- Проверка
${#files[@]} -eq 0— graceful exit на пустой вход. - Передача
"$f"в python — экранирование на случай пробелов.
Associative arrays: хэш-мапы
С bash 4.0+ есть associative arrays — словари key->value. Создаются через declare -A:
declare -A user_email
user_email["alice"]="[email protected]"
user_email["bob"]="[email protected]"
user_email["carol"]="[email protected]"
echo "${user_email[alice]}" # [email protected]
echo "${user_email[bob]}" # [email protected]
# Все ключи:
echo "${!user_email[@]}" # alice bob carol (порядок НЕ гарантирован)
# Все значения:
echo "${user_email[@]}"
# Количество ключей:
echo "${#user_email[@]}" # 3
Обязательно объявить через declare -A ПЕРЕД использованием. Без declare bash будет считать arr[key] обращением к indexed-массиву с ключом 0 (arithmetic evaluation). Это самая частая ошибка с associative arrays.
Проверка существования ключа
declare -A config
config[host]="localhost"
config[port]="5432"
# Проверка через -v (bash 4.3+):
if [ -v config[host] ]; then
echo "host is set: ${config[host]}"
fi
# Альтернатива: проверить через :+ оператор
if [ -n "${config[host]:+x}" ]; then
echo "host is set"
fi
Удаление ключа
unset 'config[port]' # одинарные кавычки чтобы не было globbing
DE-паттерн 1: group-by counter
Самая частая задача: посчитать количество вхождений каждого значения. Без associative arrays — мучение через sort | uniq -c. С ними — просто.
Пример: посчитать HTTP status codes из nginx-логов.
#!/bin/bash
set -euo pipefail
declare -A status_counts
while IFS= read -r line; do
# Извлечь статус-код (9-й столбец access.log)
status=$(echo "$line" | awk '{print $9}')
# Увеличить счётчик:
status_counts[$status]=$(( ${status_counts[$status]:-0} + 1 ))
done < /var/log/nginx/access.log
# Распечатать результаты:
for status in "${!status_counts[@]}"; do
echo "$status: ${status_counts[$status]}"
done
Bash-эквивалент Pandas groupby().count(). За один проход по файлу.
Ключевой трюк: ${status_counts[$status]:-0} — взять текущее значение или 0 если ключа нет. Без :-0 при первом обращении к новому ключу с set -u будет ошибка.
Для больших файлов awk быстрее: awk '{counts[$9]++} END {for (s in counts) print s, counts[s]}' access.log. Bash-цикл с read работает ~1000 строк/сек, awk — миллионы строк/сек. Bash подходит для маленьких объёмов или когда логика сложная. О выборе bash vs awk vs python — в уроке 04 и в capstone.
DE-паттерн 2: дедуп через associative array
Удалить дубликаты, сохраняя порядок первого появления:
declare -A seen
unique=()
while IFS= read -r line; do
if [ ! -v seen[$line] ]; then
seen[$line]=1
unique+=("$line")
fi
done < input.txt
printf '%s\n' "${unique[@]}" > deduplicated.txt
Альтернатива через awk: awk '!seen[$0]++' input.txt > deduplicated.txt — короче и быстрее. Но если логика сложнее (например, дедуп по конкретному полю с pre-processing) — bash с associative array читаем.
DE-паттерн 3: lookup table
Маппинг env -> bucket name для multi-tenant pipeline:
declare -A bucket_for_env=(
[dev]="acme-data-dev"
[staging]="acme-data-staging"
[prod]="acme-data-prod-eu-central-1"
)
ENV="${1:-dev}"
if [ ! -v bucket_for_env[$ENV] ]; then
echo "Unknown env: $ENV. Valid: ${!bucket_for_env[@]}" >&2
exit 1
fi
BUCKET="${bucket_for_env[$ENV]}"
echo "Using bucket: $BUCKET"
aws s3 cp data.json "s3://$BUCKET/raw/data.json"
Заметь синтаксис инициализации associative array: declare -A name=([k1]=v1 [k2]=v2). Удобно для статических конфигов.
Различия indexed vs associative
macOS gotcha: системный /bin/bash на старых macOS — версия 3.2 (Apple не обновляет из-за GPLv3). Associative arrays там не работают. Используй #!/usr/bin/env bash (берёт bash из PATH, обычно brew-версия 5.3+) или установи bash через brew install bash + переключи путь.
Передача массива в функцию
В bash нельзя передать массив как аргумент напрямую — функции получают строки. Два паттерна:
# 1. Развёрнутый массив (теряется группировка):
my_func() {
for x in "$@"; do
echo "Got: $x"
done
}
arr=(one "two with space" three)
my_func "${arr[@]}" # передаём все элементы как отдельные аргументы
# 2. Через nameref (bash 4.3+):
my_func() {
local -n arr_ref=$1 # nameref на переданное имя
for x in "${arr_ref[@]}"; do
echo "Got: $x"
done
}
myarr=(one "two with space" three)
my_func myarr # передаём ИМЯ массива
Nameref удобнее для associative arrays (где разворачивание теряет ключи).
Подводные камни
1. Обращение без кавычек
arr=("one two" "three")
for x in ${arr[@]}; do echo "$x"; done
# Output:
# one
# two
# three
# Разорвало "one two" на части!
for x in "${arr[@]}"; do echo "$x"; done
# Output:
# one two
# three
# Корректно
2. ${arr[*]} vs ${arr[@]}
${arr[*]} склеивает все элементы в одну строку через первый символ IFS. Полезно редко (например, для join). Не путай с ${arr[@]}.
arr=(a b c)
IFS=','
echo "${arr[*]}" # a,b,c (склеено)
echo "${arr[@]}" # a b c (отдельные слова)
3. Indexed array создаётся при первом присваивании
# Это ОК (создаёт indexed array):
arr[0]="zero"
arr[1]="one"
# Это создаст БЛОБ если до этого был declare -A arr:
declare -A arr
arr[0]="zero"
echo "${arr[0]}" # zero (но это associative со строковым ключом "0", не indexed!)
4. Удаление элемента не сдвигает индексы
arr=(a b c d)
unset 'arr[1]'
echo "${arr[@]}" # a c d
echo "${#arr[@]}" # 3 (длина)
echo "${!arr[@]}" # 0 2 3 (пропуск!)
# Если нужно компактировать:
arr=("${arr[@]}") # пересборка убирает пропуски
echo "${!arr[@]}" # 0 1 2
Bash 5.3: новая фича GLOBSORT
В bash 5.3 (июль 2025) добавили опцию GLOBSORT — управление сортировкой при глоб-разворачивании в массивы:
# Bash 5.3+:
GLOBSORT=mtime
files=(*.csv) # отсортированы по mtime (старые first)
GLOBSORT=numeric
files=(file*.csv) # числовая сортировка (file2 < file10)
GLOBSORT=name # default
Раньше нужно было городить find -printf '%T+ %p' | sort | cut. Теперь — встроено. Это удобно для DE-задач типа “обработать файлы в порядке их создания”.
Cross-links
- Урок 01 (preamble) — почему IFS критичен для quoting
"${arr[@]}". - Урок 03 (trap) — массивы для tracking временных ресурсов.
- Урок 04 (getopts) — массивы для накопления списочных опций (например,
--input file1 --input file2). - Модуль 07 (sed/awk) — awk-эквивалент группировки/дедупа часто быстрее.
Попробуй сам
- Создай массив дней недели, выведи длину, выведи все элементы.
days=(пн вт ср чт пт сб вс)
echo "${#days[@]}"
printf '%s\n' "${days[@]}"
- Прочитай содержимое
/etc/passwdв массив черезmapfile. Выведи первые 5 строк.
mapfile -t users < /etc/passwd
printf '%s\n' "${users[@]:0:5}"
- Посчитай распределение shells в
/etc/passwdчерез associative array:
declare -A shells
while IFS=: read -r _ _ _ _ _ _ shell; do
shells[$shell]=$(( ${shells[$shell]:-0} + 1 ))
done < /etc/passwd
for s in "${!shells[@]}"; do
echo "$s: ${shells[$s]}"
done | sort -t: -k2 -rn
-
Создай lookup table env -> backup-path, попробуй с тремя env и с несуществующим.
-
(bash 5.3+) Попробуй
GLOBSORT=mtime; arr=(/tmp/*)и проверь порядок черезprintf.