Списки и хеши в bash

Сложные структуры не для bash

В bash можно создавать многомерные структуры, но выглядеть они будут ужасно и такой усложненный код сложнее поддерживать; если нужны такие сложные структуры, то лучше реализовать их на другом ЯП.

Не все версии bash поддерживают хеши

Поддержка хешей появилась только в bash 4.0, после чего потребовалось еще пару релизов, чтобы отшливофать некоторые детали. К примеру, только в 4.3 в стало возможным юзать $list[-1] для ссылки на последний элемент вместо вот этого трэша:

$mylist[${#mylist[*]}-1]
# где ${#mylist[*] - кол-во элементов

Массивы не POSIX

Массивы (как списки, так и хеши) не стандартизированы в POSIX.
если парит переносимость кода за пределы bash - будь осторожен (ибо, например, синтаксис zsh отличается).

Случайное присваивание

Присваивание без указания нижнего индекса изменит нулевой элемент.
myarray=foo создаст/изменит $myarray[0] (даже если это хеш!).

и [*]

Индекс [@] или [*] возвращает все элементы.
Они различаются только когда ссылка на массив заключается в двойные кавычки ().
$name[*] развернется в одну строку
$name[@] развернется в коллекцию строк с отдельными элементами массива.

или [*] ?

Почти во всех случаях следует юзать [@].


Массивы (списки)

Объявление

Массивы (списки в bash так называются) могут объявляться w/:

declare -a mylist
local -a mylist
readonly -a mylist

или простым присваниванием:

mylist[0]=foo
mylist=()  # пустой список

Добавление элемента

После объявления списка можно добавлять элементы с помощью такого присваивания:

declare -a mylist
 
mylist+=(foo bar buz)
# или
mylist+=("abo bus")
# или
mylist+=(foo bar "abo bus" buz)

Длина списка или элемента

echo "The element count is: ${#mylist[@]} or ${#mylist[*]}"
# The element count is: 4 or 4
echo "The length of element [3] is ${#mylist[3]}"
# The length of element [3] is 7

Объединение элементов

function Join { local IFS="$1"; shift; echo "$*"; }  # односимвольный разделитель
 
echo -n "Join ',' \${mylist[@] = "; Join ',' "${mylist[@]}"
# Join ',' ${mylist[@] = foo,bar,buz,abo bus

Обход значений

for element in "${mylist[@]}"; do
	echo "$element"
done
Обход значений с индексами элементов

Для этого нужен восклицательный знак (!) перед названием списка.

for element in "${!mylist[@]}"; do
	echo -e "\tElement: $element; value: ${mylist[$element]}"
done
 
# 	Element: 0; value: foo
#	Element: 1; value: bar
#	Element: 2; value: abo bus
#	Element: 3; value: buz

Операции со срезами (slices)

Вывод
printf "%q|" "${mylist[@]:3:1}"
# buz|
Присваивание
# Срез, начинающийся с первого элемент, кавычки обязательны
mylist=("${mylist[@]:1}")
# Срез, начинающийся с элемента #count
mylist=("$mylist[@]:$count")
Удаление (pop) последнего элемента
unset -v 'mylist[-1]'  # В bash 4.3+
 
# unset -v 'mylist[${#mylist[*]}-1]'  # В старых версиях
Удаление срезов
unset -v 'mylist[2]'  # Delete element 2
Удаление всего списка
unset -v 'mylist'

Осторожнее с unset

Если в файловой системе будет файл с именем, совпадающим с именем переменной, то поддержка подстановки имен файлов в командной оболочке может удалить этот файл.
Чтобы избежать этого, нужно:

  • заключить переменную в кавычки
  • юзать ключ -v, чтобы unset рассматривал аргумент как переменную.

Хеши (словари)

Объявление

Хеши (словари) обязательно должны объявляться w/ :

declare -A myhash
local -A myhash
readonly -A myhash

Присваивание

declare -A myhash
 
myhash[a]='foo'
myhash[b]='bar'
myhash[c]='aasasa ada asasas'
# или
myhash=([a]=foo [b]=bar [c]="aasasa ada asasas")

Вывод некоторых деталей и контента

echo "The key count is: ${#myhash[@]} or ${#myhash[*]}"
# The key count is: 3 or 3
echo "The length of the value of key [b] is: ${#myhash[b]}"
# The length of the value of key [b] is: 3
 
declare -p myhash
echo -n "\${myhash[@]} = " ; printf "%q|" ${myhash[@]}
# ${myhash[@]} = aasasa|ada|asasas|bar|foo|

Объединение значений

function Join { local IFS="$1"; shift; echo "$*"; }  # односимвольный разделитель
 
echo -n "Join ',' \${myhash[@] = "; Join ',' "${myhash[@]}"
# Join ',' ${myhash[@] = aasasa ada asasas,bar,foo

Операции со срезами

Но работа со срезами выглядит странно, ибо индексы не порядковые номера.

Вывод такой же, как у списков.
Присваивание тоже.

Удаление ключей
unset -v 'myhash[c]'
Удаление всего хеша
unset -v 'myhash'

Подсчет слов

Самое распространенное применение хешей - подсчет слов и/или “уникализация” элементов.

#!/bin/bash -
 
WORD_FILE='/tmp/words.txt'
> "$WORD_FILE"  # Создание файла
trap "rm -f $WORD_FILE" ABRT EXIT HUP INT QUIT TERM
 
declare -A MY_HASH
 
# Создание списка слов
MY_LIST=(foo bar baz one two three four)
 
# Выбор случайных элементов из списка в цикле
range="${#MY_LIST[@]}"
for ((i=0; i<35; i++)); do
	random_element="$(( $RANDOM % $range ))"
	echo "${MY_LIST[$random_element]}" >> $WORD_FILE
done
 
# Запись слов из списка в хеш
while read line; do
	# увеличение значения счетчика для уже встречающегося слова
	(( MY_HASH[$line]++ ))
done < "$WORD_FILE"
 
 
# Обход ключей для вывода списка слов
# без повторений и без использования внешней команды uniq
echo -e "\nUnique words from: $WORD_FILE"
for key in "${!MY_HASH[@]}"; do
	echo "$key"
done | sort
 
# Повторный обход ключей для отображения значений счетчиков слов
echo -e "\nWord counts, ordered by word, from: $WORD_FILE"
for key in "${!MY_HASH[@]}"; do
	printf "%s\t%d\n" $key ${MY_HASH[$key]}
done | sort
 
# Последний обход ключей для отображения счетчиков,
# но на этот раз с числовой сортировкой по второму полю (sort -k2,2n)
echo -e "\nWord counts, ordered by count, from: $WORD_FILE"
for key in "${!MY_HASH[@]}"; do
	printf "%s\t%d\n" $key ${MY_HASH[$key]}
done | sort -k2,2n
Результат выполнения кода
Unique words from: /tmp/words.txt
bar
baz
foo
four
one
three
two

Word counts, ordered by word, from: /tmp/words.txt
bar	7
baz	4
foo	5
four	6
one	2
three	4
two	7

Word counts, ordered by count, from: /tmp/words.txt
one	2
baz	4
three	4
foo	5
four	6
bar	7
two	7

Другие сценарии использования

Перенаправление результата выполнения команды в массив

declare -a SOME_LIST
mapfile -t SOME_LIST <<< "$(
	some_cmd \
	  | other_cmd \
	  | another_cmd
)"

Выбор рандомных элементов из списка

mylist=(foo bar baz one two three four)
 
range"${#mylist[@]}"
for ((i=0; i<35; i++)); do
	random_element="$(( $RANDOM % $range ))"
	echo "${mylist[$random_element]}"
done

Запись из списка/файла в хеш

while read line; do
	# увеличение значения счетчика для уже встречающегося слова
	(( myhash[$line]++ ))
done < $WORD_FILE  # или $mylist

Соус: Книга “Идиомы Bash Глава 7. “Списки и хэши

bash