Глава 25. Массивы


Новейшие версии Bash поддерживают одномерные массивы. Инициализация элементов массива может быть произведена в виде: variable[xx]. Можно явно объявить массив в сценарии, с помощью директивы declare: declare -a variable. Обращаться к отдельным элементам массива можно с помощью фигурных скобок, т.е.: ${variable[xx]}.

Пример 25-1. Простой массив

  1. #!/bin/bash
  2. area[11]=23
  3. area[13]=37
  4. area[51]=UFOs
  5. # Массивы не требуют, чтобы последовательность элементов в массиве была непрерывной.
  6. # Некоторые элементы массива могут оставаться неинициализированными.
  7. # "Дыркм" в массиве не являются ошибкой.
  8. echo -n "area[11] = "
  9. echo ${area[11]}    #  необходимы {фигурные скобки}
  10. echo -n "area[13] = "
  11. echo ${area[13]}
  12. echo "содержимое area[51] = ${area[51]}."
  13. # Обращение к неинициализированным элементам дает пустую строку.
  14. echo -n "area[43] = "
  15. echo ${area[43]}
  16. echo "(элемент area[43] — неинициализирован)"
  17. echo
  18. # Сумма двух элементов массива, записанная в третий элемент
  19. area[5]=`expr ${area[11]} + ${area[13]}`
  20. echo "area[5] = area[11] + area[13]"
  21. echo -n "area[5] = "
  22. echo ${area[5]}
  23. area[6]=`expr ${area[11]} + ${area[51]}`
  24. echo "area[6] = area[11] + area[51]"
  25. echo -n "area[6] = "
  26. echo ${area[6]}
  27. # Эта попытка закончится неудачей, поскольку сложение целого числа со строкой не допускается.
  28. echo; echo; echo
  29. # -----------------------------------------------------------------
  30. # Другой массив, "area2".
  31. # И другой способ инициализации массива...
  32. # array_name=( XXX YYY ZZZ ... )
  33. area2=( ноль один два три четыре )
  34. echo -n "area2[0] = "
  35. echo ${area2[0]}
  36. # Ага, индексация начинается с нуля (первый элемент массива имеет индекс [0], а не [1]).
  37. echo -n "area2[1] = "
  38. echo ${area2[1]}    # [1] — второй элемент массива.
  39. # -----------------------------------------------------------------
  40. echo; echo; echo
  41. # -----------------------------------------------
  42. # Еще один массив, "area3".
  43. # И еще один способ инициализации...
  44. # array_name=([xx]=XXX [yy]=YYY ...)
  45. area3=([17]=семнадцать [21]=двадцать_один)
  46. echo -n "area3[17] = "
  47. echo ${area3[17]}
  48. echo -n "area3[21] = "
  49. echo ${area3[21]}
  50. # -----------------------------------------------
  51. exit 0
Note

Bash позволяет оперировать переменными, как массивами, даже если они не были явно объявлены таковыми.

  1. string=abcABC123ABCabc
  2. echo ${string[@]}               # abcABC123ABCabc
  3. echo ${string[*]}               # abcABC123ABCabc
  4. echo ${string[0]}               # abcABC123ABCabc
  5. echo ${string[1]}               # Ничего не выводится!
  6.                                 # Почему?
  7. echo ${#string[@]}              # 1
  8.                                 # Количество элементов в массиве.
  9. # Спасибо Michael Zick за этот пример.
Эти примеры еще раз подтверждают отсутствие контроля типов в Bash.

Пример 25-2. Форматирование стихотворения

  1. #!/bin/bash
  2. # poem.sh
  3. # Строки из стихотворения (одна строфа).
  4. Line[1]="Мой дядя самых честных правил,"
  5. Line[2]="Когда не в шутку занемог;"
  6. Line[3]="Он уважать себя заставил,"
  7. Line[4]="И лучше выдумать не мог."
  8. Line[5]="Его пример другим наука..."
  9. # Атрибуты.
  10. Attrib[1]=" А.С. Пушкин"
  11. Attrib[2]="\"Евгений Онегин\""
  12. for index in 1 2 3 4 5    # Пять строк.
  13. do
  14.   printf "     %s\n" "${Line[index]}"
  15. done
  16. for index in 1 2          # Две строки дополнительных атрибутов.
  17. do
  18.   printf "          %s\n" "${Attrib[index]}"
  19. done
  20. exit 0

При работе с отдельными элементами массива можно использовать специфический синтаксис, даже стандартные команды и операторы Bash адаптированы для работы с массивами.

Пример 25-3. Различные операции над массивами

  1. #!/bin/bash
  2. # array-ops.sh: Операции над массивами.
  3. array=( ноль один два три четыре пять )
  4. echo ${array[0]}       #  ноль
  5. echo ${array:0}        #  ноль
  6.                        #  Подстановка параметра - первый элемент,
  7.                        #+ начиная с позиции 0 (с 1-го символа).
  8. echo ${array:1}        #  оль
  9.                        #  Подстановка параметра - первый элемент,
  10.                        #+ начиная с позиции 1 (со 2-го символа).
  11. echo "--------------"
  12. echo ${#array[0]}      #  4
  13.                        #  Длина первого элемента массива.
  14. echo ${#array}         #  4
  15.                        #  Длина первого элемента массива.
  16.                        #  (Альтернативный вариант)
  17. echo ${#array[1]}      #  4
  18.                        #  Длина второго элемента массива.
  19.                        #  Индексация массивов в Bash начинается с нуля.
  20. echo ${#array[*]}      #  6
  21.                        #  Число элементов в массиве.
  22. echo ${#array[@]}      #  6
  23.                        #  Число элементов в массиве.
  24. echo "--------------"
  25. array2=( [0]="первый элемент" [1]="второй элемент" [3]="четвертый элемент" )
  26. echo ${array2[0]}      # первый элемент
  27. echo ${array2[1]}      # второй элемент
  28. echo ${array2[2]}      #
  29.                        # Не был проинициализирован, поэтому null.
  30. echo ${array2[3]}      # четвертый элемент
  31. exit 0

Большинство стандартных операций над строками применимы к массивам.

Пример 25-4. Строковые операции и массивы

  1. #!/bin/bash
  2. # array-strops.sh: Строковые операции и массивы.
  3. # Автор: Michael Zick.
  4. # Используется с его разрешения.
  5. #  Как правило, строковые операции, в нотации ${name ... }
  6. #+ могут использоваться для работы со строковыми элементами массивов
  7. #+ в виде: ${name[@] ... } или ${name[*] ...} .
  8. arrayZ=( one two three four five five )
  9. echo
  10. # Извлечение части строки
  11. echo ${arrayZ[@]:0}     # one two three four five five
  12.                         # Все элементы массива.
  13. echo ${arrayZ[@]:1}     # two three four five five
  14.                         # Все эелементы массива, начиная со 2-го.
  15. echo ${arrayZ[@]:1:2}   # two three
  16.                         # Два элемента массива, начиная со 2-го.
  17. echo "-----------------------"
  18. #  Удаление части строки
  19. #  Удаляет наименьшую из подстрок, найденых по шаблону (поиск ведется с начала строки)
  20. #+ где шаблон — это регулярное выражение.
  21. echo ${arrayZ[@]#f*r}   # one two three five five
  22.                         # Находит подстроку "four" и удаляет ее.
  23.                         # Поиск ведется по всем элементам массива
  24. # Удаляет наибольшую подстроку, из найденых по шаблону
  25. echo ${arrayZ[@]##t*e}  # one two four five five
  26.                         # Находит подстроку "three" и удаляет ее.
  27.                         # Поиск ведется по всем элементам массива
  28. #  Удаляет наименьшую из подстрок, найденых по шаблону (поиск ведется с конца строки)
  29. echo ${arrayZ[@]%h*e}   # one two t four five five
  30.                         # Находит подстроку "hree" и удаляет ее.
  31.                         # Поиск ведется по всем элементам массива
  32. # Удаляет наибольшую из подстрок, найденых по шаблону (поиск ведется с конца строки)
  33. echo ${arrayZ[@]%%t*e}  # one two four five five
  34.                         # Находит подстроку "three" и удаляет ее.
  35.                         # Поиск ведется по всем элементам массива
  36. echo "-----------------------"
  37. # Замена части строки
  38. # Заменяет первую найденую подстроку заданой строкой
  39. echo ${arrayZ[@]/fiv/XYZ}   # one two three four XYZe XYZe
  40.                             # Поиск ведется по всем элементам массива
  41. # Заменяет все найденные подстроки
  42. echo ${arrayZ[@]//iv/YY}    # one two three four fYYe fYYe
  43.                             # Поиск ведется по всем элементам массива
  44. # Удаляет все найденные подстроки
  45. # Если замещающая строка не задана, то это означает "удаление"
  46. echo ${arrayZ[@]//fi/}      # one two three four ve ve
  47.                             # Поиск ведется по всем элементам массива
  48. # Заменяет первую найденную подстроку (поиск ведется с начала строки)
  49. echo ${arrayZ[@]/#fi/XY}    # one two three four XYve XYve
  50.                             # Поиск ведется по всем элементам массива
  51. # Заменяет первую найденную подстроку (поиск ведется с конца строки)
  52. echo ${arrayZ[@]/%ve/ZZ}    # one two three four fiZZ fiZZ
  53.                             # Поиск ведется по всем элементам массива
  54. echo ${arrayZ[@]/%o/XX}     # one twXX three four five five
  55.                             # Почему?
  56. echo "-----------------------"
  57. # Before reaching for awk (or anything else)
  58. # Вспомним:
  59. #   $( ... ) — вызов функции.
  60. #   Функция запускается как подпроцесс.
  61. #   Функции выводят на stdout.
  62. #   Присваивание производится со stdout функции.
  63. #   Запись name[@] — означает операцию "for-each".
  64. newstr() {
  65.     echo -n "!!!"
  66. }
  67. echo ${arrayZ[@]/%e/$(newstr)}
  68. # on!!! two thre!!! four fiv!!! fiv!!!
  69. # Q.E.D: Замена — суть есть "присваивание".
  70. #  Доступ "For-Each" — ко всем элементам массива
  71. echo ${arrayZ[@]//*/$(newstr optional_arguments)}
  72. #  Now, if Bash would just pass the matched string as $0
  73. #+ to the function being called . . .
  74. echo
  75. exit 0

Command substitution can construct the individual elements of an array.

Пример 25-5. Загрузка исходного текста сценария в массив

  1. #!/bin/bash
  2. # script-array.sh: Сценарий загружает себя в массив.
  3. # Идею подал Chris Martin (спасибо!).
  4. script_contents=( $(cat "$0") )  #  Записать содержимое этого сценария ($0)
  5.                                  #+ в массив.
  6. for element in $(seq 0 $((${#script_contents[@]} - 1)))
  7.   do                #  ${#script_contents[@]}
  8.                     #+ дает число элементов массива.
  9.                     #
  10.                     #  Вопрос:
  11.                     #  Для чего необходима команда seq 0  ?
  12.                     #  Попробуйте заменить ее на seq 1.
  13.   echo -n "${script_contents[$element]}"
  14.                     # Вывести элементы массива в одну строку,
  15.   echo -n " — "    # разделяя их комбинацией " — " .
  16. done
  17. echo
  18. exit 0
  19. # Упражнение:
  20. # --------
  21. #  Попробуйте изменить сценарий таким образом,
  22. #+ чтобы он выводил себя на экран в первоначальном виде,
  23. #+ со всеми пробелами, переводами строк и т.п.

При работе с массивами, некоторые встроенные команды Bash имеют несколько иной смысл. Например, unset — удаляет отдельные элементы массива, или даже массив целиком.

Пример 25-6. Некоторые специфичные особенности массивов

  1. #!/bin/bash
  2. declare -a colors
  3. # Допускается объявление массива без указания его размера.
  4. echo "Введите ваши любимые цвета (разделяя их пробелами)."
  5. read -a colors    # Введите хотя бы 3 цвета для демонстрации некоторых свойств массивов.
  6. #  Специфический ключ команды 'read',
  7. #+ позволяющий вводить несколько элементов массива.
  8. echo
  9. element_count=${#colors[@]}
  10. # Получение количества элементов в массиве.
  11. #     element_count=${#colors[*]} — дает тот же результат.
  12. #
  13. #  Переменная "@" позволяет "разбивать" строку в кавычках на отдельные слова
  14. #+ (выделяются слова, разделенные пробелами).
  15. index=0
  16. while [ "$index" -lt "$element_count" ]
  17. do    # Список всех элементов в массиве.
  18.   echo ${colors[$index]}
  19.   let "index = $index + 1"
  20. done
  21. # Каждый элемент массива выводится в отдельной строке.
  22. # Если этого не требуется, то используйте  echo -n "${colors[$index]} "
  23. #
  24. # Эквивалентный цикл "for":
  25. #   for i in "${colors[@]}"
  26. #   do
  27. #     echo "$i"
  28. #   done
  29. # (Спасибо S.C.)
  30. echo
  31. # Еще один, более элегантный, способ вывода списка всех элементов массива.
  32.   echo ${colors[@]}          # ${colors[*]} дает тот же результат.
  33. echo
  34. # Команда "unset" удаляет элементы из массива, или даже массив целиком.
  35. unset colors[1]              # Удаление 2-го элемента массива.
  36.                              # Тот же эффект дает команда   colors[1]=
  37. echo  ${colors[@]}           # Список всех элементов массива — 2-й элемент отсутствует.
  38. unset colors                 # Удаление всего массива.
  39.                              #  Тот же эффект имеют команды unset colors[*]
  40.                              #+ и unset colors[@].
  41. echo; echo -n "Массив цветов опустошен."
  42. echo ${colors[@]}            # Список элементов массива пуст.
  43. exit 0

Как видно из предыдущего примера, обращение к ${array_name[@]} или ${array_name[*]} относится ко всем элементам массива. Чтобы получить количество элементов массива, можно обратиться к ${#array_name[@]} или к ${#array_name[*]}. ${#array_name} — это длина (количество символов) первого элемента массива, т.е. ${array_name[0]}.

Пример 25-7. Пустые массивы и пустые элементы

  1. #!/bin/bash
  2. # empty-array.sh
  3. #  Выражаю свою благодарность Stephane Chazelas за этот пример,
  4. #+ и Michael Zick за его доработку.
  5. # Пустой массив — это не то же самое, что массив с пустыми элементами.
  6. array0=( первый второй третий )
  7. array1=( '' )   # "array1" имеет один пустой элемент.
  8. array2=( )      # Массив "array2" не имеет ни одного элемента, т.е. пуст.
  9. echo
  10. ListArray()
  11. {
  12. echo
  13. echo "Элементы массива array0:  ${array0[@]}"
  14. echo "Элементы массива array1:  ${array1[@]}"
  15. echo "Элементы массива array2:  ${array2[@]}"
  16. echo
  17. echo "Длина первого элемента массива array0 = ${#array0}"
  18. echo "Длина первого элемента массива array1 = ${#array1}"
  19. echo "Длина первого элемента массива array2 = ${#array2}"
  20. echo
  21. echo "Число элементов в массиве array0 = ${#array0[*]}"  # 3
  22. echo "Число элементов в массиве array1 = ${#array1[*]}"  # 1  (сюрприз!)
  23. echo "Число элементов в массиве array2 = ${#array2[*]}"  # 0
  24. }
  25. # ===================================================================
  26. ListArray
  27. # Попробуем добавить новые элементы в массивы
  28. # Добавление новых элементов в массивы.
  29. array0=( "${array0[@]}" "новый1" )
  30. array1=( "${array1[@]}" "новый1" )
  31. array2=( "${array2[@]}" "новый1" )
  32. ListArray
  33. # или
  34. array0[${#array0[*]}]="новый2"
  35. array1[${#array1[*]}]="новый2"
  36. array2[${#array2[*]}]="новый2"
  37. ListArray
  38. # Теперь представим каждый массив как 'стек' ('stack')
  39. # Команды выше, можно считать командами 'push' — добавление нового значения на вершину стека
  40. # 'Глубина' стека:
  41. height=${#array2[@]}
  42. echo
  43. echo "Глубина стека array2 = $height"
  44. # Команда 'pop' — выталкивание элемента стека, находящегося на вершине:
  45. unset array2[${#array2[@]}-1]   # Индексация массивов начинается с нуля
  46. height=${#array2[@]}
  47. echo
  48. echo "POP"
  49. echo "Глубина стека array2, после выталкивания = $height"
  50. ListArray
  51. # Вывести только 2-й и 3-й элементы массива array0
  52. from=1          # Индексация массивов начинается с нуля
  53. to=2              #
  54. declare -a array3=( ${array0[@]:1:2} )
  55. echo
  56. echo "Элементы массива array3:  ${array3[@]}"
  57. # Замена элементов по шаблону
  58. declare -a array4=( ${array0[@]/второй/2-й} )
  59. echo
  60. echo "Элементы массива array4:  ${array4[@]}"
  61. # Замена строк по шаблону
  62. declare -a array5=( ${array0[@]//новый?/старый} )
  63. echo
  64. echo "Элементы массива array5:  ${array5[@]}"
  65. # Надо лишь привыкнуть к такой записи...
  66. declare -a array6=( ${array0[@]#*новый} )
  67. echo # Это может вас несколько удивить
  68. echo "Элементы массива array6:  ${array6[@]}"
  69. declare -a array7=( ${array0[@]#новый1} )
  70. echo # Теперь это вас уже не должно удивлять
  71. echo "Элементы массива array7:  ${array7[@]}"
  72. # Выглядить очень похоже на предыдущий вариант...
  73. declare -a array8=( ${array0[@]/новый1/} )
  74. echo
  75. echo "Элементы массива array8:  ${array8[@]}"
  76. #  Итак, что вы можете сказать обо всем этом?
  77. #  Строковые операции выполняются последовательно, над каждым элементом
  78. #+ в массиве var[@].
  79. #  Таким образом, BASH поддерживает векторные операции
  80. #  Если в результате операции получается пустая строка, то
  81. #+ элемент массива "исчезает".
  82. #  Вопрос: это относится к строкам в "строгих" или "мягких" кавычках?
  83. zap='новый*'
  84. declare -a array9=( ${array0[@]/$zap/} )
  85. echo
  86. echo "Элементы массива array9:  ${array9[@]}"
  87. # "...А с платформы говорят: "Это город Ленинград!"..."
  88. declare -a array10=( ${array0[@]#$zap} )
  89. echo
  90. echo "Элементы массива array10:  ${array10[@]}"
  91. # Сравните массивы array7 и array10
  92. # Сравните массивы array8 и array9
  93. # Ответ: в "мягких" кавычках.
  94. exit 0

Разница между ${array_name[@]} и ${array_name[*]} такая же, как между $@ и $*. Эти свойства массивов широко применяются на практике.

  1. # Копирование массивов.
  2. array2=( "${array1[@]}" )
  3. # или
  4. array2="${array1[@]}"
  5. # Добавить элемент.
  6. array=( "${array[@]}" "новый элемент" )
  7. # или
  8. array[${#array[*]}]="новый элемент"
  9. # Спасибо S.C.


Tip

Операция подстановки командarray=( element1 element2 ... elementN ), позволяет загружать содержимое текстовых файлов в массивы.

  1. #!/bin/bash
  2. filename=sample_file
  3. #            cat sample_file
  4. #
  5. #            1 a b c
  6. #            2 d e fg
  7. declare -a array1
  8. array1=( `cat "$filename"`)  # Загрузка содержимого файла
  9.                              # $filename в массив array1.
  10. #         Вывод на stdout.
  11. #
  12. #  array1=( `cat "$filename" | tr '\n' ' '`)
  13. #                         с заменой символов перевода строки на пробелы.
  14. #  Впрочем, в этом нет необходимости, поскольку Bash
  15. #+ выполняет разбивку по словам заменяя символы перевода строки
  16. #+ на пробелы автоматически.
  17. echo ${array1[@]}            # список элементов массива.
  18. #                              1 a b c 2 d e fg
  19. #
  20. #  Каждое "слово", в текстовом файле, отделяемое от других пробелами
  21. #+ заносится в отдельный элемент массива.
  22. element_count=${#array1[*]}
  23. echo $element_count          # 8


Пример 25-8. Инициализация массивов

  1. #! /bin/bash
  2. # array-assign.bash
  3. #  Поскольку здесь рассматриваются операции, специфичные для Bash,
  4. #+ файл сценария имеет расширение ".bash".
  5. # Copyright (c) Michael S. Zick, 2003, All rights reserved.
  6. # Лицензионное соглашение: Допускается использование сценария
  7. # в любом виде без каких либо ограничений.
  8. # Версия: $ID$
  9. #  Основан на примере, предоставленом Stephane Chazelas,
  10. #+ который включен в состав книги: Advanced Bash Scripting Guide.
  11. # Формат вывода команды 'times':
  12. # User CPU <space> System CPU
  13. # User CPU of dead children <space> System CPU of dead children
  14. #  Bash предоставляет два способа записи всех элементов
  15. #+ одного массива в другой.
  16. #  В Bash, версий 2.04, 2.05a и 2.05b,
  17. #+ оба они пропускают 'пустые' элементы
  18. #  В более новых версиях добавлена возможность присваивания
  19. #+ в виде [индекс]=значение.
  20. declare -a bigOne=( /dev/* )
  21. echo
  22. echo 'Условия проверки: Отсутствие кавычек, IFS по-умолчанию, Все-Элементы'
  23. echo "Количество элементов в массиве: ${#bigOne[@]}"
  24. # set -vx
  25. echo
  26. echo '- - проверяется: =( ${array[@]} ) - -'
  27. times
  28. declare -a bigTwo=( ${bigOne[@]} )
  29. #                 ^              ^
  30. times
  31. echo
  32. echo '- - проверяется: =${array[@]} - -'
  33. times
  34. declare -a bigThree=${bigOne[@]}
  35. # Обратите внимание: круглые скобки отсутствуют.
  36. times
  37. #  Сравнение временных показателей свидетельствует о том, что вторая форма записи,
  38. #+ как заметил Stephane Chazelas, работает в 3-4 раза быстрее.
  39. #  Тем не менее, в своих примерах, я буду продолжать использовать первую форму записи
  40. #+ потому что, на мой взгляд, она более показательна.
  41. #  Однако, в отдельных случаях, я буду использовать вторую форму записи,
  42. #+ с целью увеличения скорости исполнения сценариев.
  43. # MSZ: Прошу прощения, что не предупредил об этом заранее!
  44. exit 0
Note

Явное объявление массива с помощью конструкции declare -a может повысить скорость работы с этим массивом в последующих операциях.

Пример 25-9. Копирование и конкатенация массивов

  1. #! /bin/bash
  2. # CopyArray.sh
  3. #
  4. # Автор: Michael Zick.
  5. # Используется с его разрешения.
  6. #  "Принять из массива с заданным именем записать в массив с заданным именем"
  7. #+ или "собственный Оператор Присваивания".
  8. CpArray_Mac() {
  9. # Оператор Присваивания
  10.     echo -n 'eval '
  11.     echo -n "$2"                    # Имя массива-результата
  12.     echo -n '=( ${'
  13.     echo -n "$1"                    # Имя исходного массива
  14.     echo -n '[@]} )'
  15. # Все это могло бы быть объединено в одну команду.
  16. # Это лишь вопрос стиля.
  17. }
  18. declare -f CopyArray                # "Указатель" на функцию
  19. CopyArray=CpArray_Mac               # Оператор Присваивания
  20. Hype()
  21. {
  22. # Исходный массив с именем в $1.
  23. # (Слить с массивом, содержащим "-- Настоящий Рок-н-Ролл".)
  24. # Вернуть результат в массиве с именем $2.
  25.     local -a TMP
  26.     local -a hype=( — Настоящий Рок-н-Ролл )
  27.     $($CopyArray $1 TMP)
  28.     TMP=( ${TMP[@]} ${hype[@]} )
  29.     $($CopyArray TMP $2)
  30. }
  31. declare -a before=( Advanced Bash Scripting )
  32. declare -a after
  33. echo "Массив before = ${before[@]}"
  34. Hype before after
  35. echo "Массив after  = ${after[@]}"
  36. # Еще?
  37. echo "Что такое ${after[@]:4:2}?"
  38. declare -a modest=( ${after[@]:2:1} ${after[@]:3:3} )
  39. #                    ---- выделение подстроки ----
  40. echo "Массив Modest = ${modest[@]}"
  41. # А что в массиве 'before' ?
  42. echo "Массив Before = ${before[@]}"
  43. exit 0

Пример 25-10. Еще пример на конкатенацию массивов

  1. #! /bin/bash
  2. # array-append.bash
  3. # Copyright (c) Michael S. Zick, 2003, All rights reserved.
  4. # Лицензионное соглашение: Допускается использование сценария
  5. # в любом виде без каких либо ограничений.
  6. # Версия: $ID$
  7. #
  8. # С небольшими изменениями, внесенными автором книги.
  9. # Действия над массивами являются специфичными для Bash.
  10. # Эквиваленты в /bin/sh отсутствуют!
  11. #  Чтобы избежать скроллинга выводимой информации за пределы терминала,
  12. #+ передайте вывод от сценария, через конвейер, команде 'more'.
  13. # Упакованный массив.
  14. declare -a array1=( zero1 one1 two1 )
  15. # Разреженный массив (элемент [1] — не определен).
  16. declare -a array2=( [0]=zero2 [2]=two2 [3]=three2 )
  17. echo
  18. echo '- Проверка того, что массив получился разреженным. -'
  19. echo "Число элементов: 4"        # Жестко "зашито", в демонстрационных целях.
  20. for (( i = 0 ; i < 4 ; i++ ))
  21. do
  22.     echo "Элемент [$i]: ${array2[$i]}"
  23. done
  24. # См. так же пример basics-reviewed.bash.
  25. declare -a dest
  26. # Конкатенация двух массивов.
  27. echo
  28. echo 'Условия: Отсутствие кавычек, IFS по-умолчанию, Все-Элементы'
  29. echo '- Неопределенные элементы не передаются. -'
  30. # # На самом деле неопределенные элемены отсутствуют в массиве.
  31. dest=( ${array1[@]} ${array2[@]} )
  32. # dest=${array1[@]}${array2[@]}     # Странный результат, возможно ошибка.
  33. # Теперь выведем результат.
  34. echo
  35. echo '- - Проверка конкатенации массивов - -'
  36. cnt=${#dest[@]}
  37. echo "Число элементов: $cnt"
  38. for (( i = 0 ; i < cnt ; i++ ))
  39. do
  40.     echo "Элемент [$i]: ${dest[$i]}"
  41. done
  42. # Записать массив в элемент другого массива (дважды).
  43. dest[0]=${array1[@]}
  44. dest[1]=${array2[@]}
  45. # Вывести результат.
  46. echo
  47. echo '- - Проверка записи содержимого одного массива в элемент другого массива - -'
  48. cnt=${#dest[@]}
  49. echo "Число элементов: $cnt"
  50. for (( i = 0 ; i < cnt ; i++ ))
  51. do
  52.     echo "Элемент [$i]: ${dest[$i]}"
  53. done
  54. # Рассмотрение содержимого второго элемента.
  55. echo
  56. echo '- - Запись содержимого второго элемента и вывод результата - -'
  57. declare -a subArray=${dest[1]}
  58. cnt=${#subArray[@]}
  59. echo "Число элементов: $cnt"
  60. for (( i = 0 ; i < cnt ; i++ ))
  61. do
  62.     echo "Элемент [$i]: ${subArray[$i]}"
  63. done
  64. #  Запись содержимого целого массива в элемент другого массива,
  65. #+ с помощью конструкции '=${ ... }',
  66. #+ приводит к преобразованию содержимого первого массива в строку,
  67. #+ в которой отдельные жлементы первого массива разделены пробелом
  68. #+ (первый символ из переменной IFS).
  69. # If the original elements didn't contain whitespace . . .
  70. # If the original array isn't subscript sparse . . .
  71. # Then we could get the original array structure back again.
  72. # Restore from the modified second element.
  73. echo
  74. echo '- - Listing restored element - -'
  75. declare -a subArray=( ${dest[1]} )
  76. cnt=${#subArray[@]}
  77. echo "Number of elements: $cnt"
  78. for (( i = 0 ; i < cnt ; i++ ))
  79. do
  80.     echo "Element [$i]: ${subArray[$i]}"
  81. done
  82. echo '- - Do not depend on this behavior. - -'
  83. echo '- - This behavior is subject to change - -'
  84. echo '- - in versions of Bash newer than version 2.05b - -'
  85. # MSZ: Sorry about any earlier confusion folks.
  86. exit 0

--

Массивы допускают перенос хорошо известных алгоритмов в сценарии на языке командной оболочки. Хорошо ли это — решать вам.

Пример 25-11. Старая, добрая: "Пузырьковая" сортировка

  1. #!/bin/bash
  2. # bubble.sh: "Пузырьковая" сортировка.
  3. #  На каждом проходе по сортируемому массиву,
  4. #+ сравниваются два смежных элемента, и, если необходимо, они меняются местами.
  5. #  В конце первого прохода, самый "тяжелый" элемент "опускается" в конец массива.
  6. #  В конце второго прохода, следующий по "тяжести" элемент занимает второе место снизу.
  7. #  И так далее.
  8. #  Каждый последующий проход требует на одно сравнение меньше предыдущего.
  9. #  Поэтому вы должны заметить ускорение работы сценария на последних проходах.
  10. exchange()
  11. {
  12.   # Поменять местами два элемента массива.
  13.   local temp=${Countries[$1]} #  Временная переменная
  14.   Countries[$1]=${Countries[$2]}
  15.   Countries[$2]=$temp
  16.   return
  17. }
  18. declare -a Countries  #  Объявление массива,
  19.                       #+ необязательно, поскольку он явно инициализируется ниже.
  20. #  Допустимо ли выполнять инициализацию массива в нескольки строках?
  21. #  ДА!
  22. Countries=(Нидерланды Украина Заир Турция Россия Йемен Сирия \
  23. Бразилия Аргентина Никарагуа Япония Мексика Венесуэла Греция Англия \
  24. Израиль Перу Канада Оман Дания Уэльс Франция Кения \
  25. Занаду Катар Лихтенштейн Венгрия)
  26. # "Занаду" — это мифическое государство, где, согласно Coleridge,
  27. #+ Kubla Khan построил величественный дворец.
  28. clear                      # Очистка экрана.
  29. echo "0: ${Countries[*]}"  # Список элементов несортированного массива.
  30. number_of_elements=${#Countries[@]}
  31. let "comparisons = $number_of_elements - 1"
  32. count=1 # Номер прохода.
  33. while [ "$comparisons" -gt 0 ]          # Начало внешнего цикла
  34. do
  35.   index=0  # Сбросить индекс перед началом каждого прохода.
  36.   while [ "$index" -lt "$comparisons" ] # Начало внутреннего цикла
  37.   do
  38.     if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
  39.     #  Если элементы стоят не по порядку...
  40.     #  Оператор \> выполняет сравнение ASCII-строк
  41.     #+ внутри одиночных квадратных скобок.
  42.     #  if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]]
  43.     #+ дает тот же результат.
  44.     then
  45.       exchange $index `expr $index + 1`  # Поменять местами.
  46.     fi
  47.     let "index += 1"
  48.   done # Конец внутреннего цикла
  49. let "comparisons -= 1" #  Поскольку самый "тяжелый" элемент уже "опустился" на дно,
  50.                        #+ то на каждом последующем проходе нужно выполнять на одно сравнение меньше.
  51. echo
  52. echo "$count: ${Countries[@]}"  # Вывести содержимое массива после каждого прохода.
  53. echo
  54. let "count += 1"                # Увеличить счетчик проходов.
  55. done                            # Конец внешнего цикла
  56. exit 0

--

Можно ли вложить один массив в другой?

  1. #!/bin/bash
  2. # "Вложенный" массив.
  3. # Автор: Michael Zick.
  4. #+ незначительные изменения и комментарии добавил William Park.
  5. AnArray=( $(ls --inode --ignore-backups --almost-all \
  6.   --directory --full-time --color=none --time=status \
  7.   --sort=time -l ${PWD} ) )  # Команды и опции.
  8. # Пробелы важны . . .
  9. SubArray=( ${AnArray[@]:11:1}  ${AnArray[@]:6:5} )
  10. #  Этот массив содержит шесть элементов:
  11. #+     SubArray=( [0]=${AnArray[11]} [1]=${AnArray[6]} [2]=${AnArray[7]}
  12. #      [3]=${AnArray[8]} [4]=${AnArray[9]} [5]=${AnArray[10]} )
  13. #
  14. #  Массивы в Bash оформляются в виде связанных (циклических) списков
  15. #+ где каждый элемент списка имеет тип string (char *).
  16. #  Таким образом, вложенные массивы фактически таковыми не являются,
  17. #+ хотя функционально очень похожи на них.
  18. echo "Текущий каталог и дата последнего изменения:"
  19. echo "${SubArray[@]}"
  20. exit 0


--

Вложенные массивы, в комбинации с косвенными ссылками, предоставляют в распоряжение программиста ряд замечательных возможностей

Пример 25-12. Вложенные массивы и косвенные ссылки

  1. #!/bin/bash
  2. # embedded-arrays.sh
  3. # Вложенные массивы и косвенные ссылки.
  4. # Автор: Dennis Leeuw.
  5. # Используется с его разрешения.
  6. # Дополнен автором документа.
  7. ARRAY1=(
  8.         VAR1_1=value11
  9.         VAR1_2=value12
  10.         VAR1_3=value13
  11. )
  12. ARRAY2=(
  13.         VARIABLE="test"
  14.         STRING="VAR1=value1 VAR2=value2 VAR3=value3"
  15.         ARRAY21=${ARRAY1[*]}
  16. )       # Вложение массива ARRAY1 в массив ARRAY2.
  17. function print () {
  18.         OLD_IFS="$IFS"
  19.         IFS=$'\n'       #  Вывод каждого элемента массива
  20.                         #+ в отдельной строке.
  21.         TEST1="ARRAY2[*]"
  22.         local ${!TEST1} # Посмотрите, что произойдет, если убрать эту строку.
  23.         #  Косвенная ссылка.
  24.         #  Позволяет получить доступ к компонентам $TEST1
  25.         #+ в этой функции.
  26.         #  Посмотрим, что получилось.
  27.         echo
  28.         echo "\$TEST1 = $TEST1"       #  Просто имя переменной.
  29.         echo; echo
  30.         echo "{\$TEST1} = ${!TEST1}"  #  Вывод на экран содержимого переменной.
  31.                                       #  Это то, что дает
  32.                                       #+ косвенная ссылка.
  33.         echo
  34.         echo "-------------------------------------------"; echo
  35.         echo
  36.         # Вывод переменной
  37.         echo "Переменная VARIABLE: $VARIABLE"
  38.         # Вывод элементов строки
  39.         IFS="$OLD_IFS"
  40.         TEST2="STRING[*]"
  41.         local ${!TEST2}      # Косвенная ссылка (то же, что и выше).
  42.         echo "Элемент VAR2: $VAR2 из строки STRING"
  43.         # Вывод элемента массива
  44.         TEST2="ARRAY21[*]"
  45.         local ${!TEST2}      # Косвенная ссылка.
  46.         echo "Элемент VAR1_1: $VAR1_1 из массива ARRAY21"
  47. }
  48. print
  49. echo
  50. exit 0

--

С помощью массивов, на языке командной оболочки, вполне возможно реализовать алгоритм Решета Эратосфена. Конечно же — это очень ресурсоемкая задача. В виде сценария она будет работать мучительно долго, так что лучше всего реализовать ее на каком либо другом, компилирующем, языке программирования, таком как C.

Пример 25-13. Пример реализации алгоритма Решето Эратосфена

  1. #!/bin/bash
  2. # sieve.sh
  3. # Решето Эратосфена
  4. # Очень старый алгоритм поиска простых чисел.
  5. # Этот сценарий выполняется во много раз медленнее
  6. # чем аналогичная программа на C.
  7. LOWER_LIMIT=1       # Начиная с 1.
  8. UPPER_LIMIT=1000    # До 1000.
  9. # (Вы можете установить верхний предел и выше...  если вам есть чем себя занять.)
  10. PRIME=1
  11. NON_PRIME=0
  12. declare -a Primes
  13. # Primes[] — массив.
  14. initialize ()
  15. {
  16. # Инициализация массива.
  17. i=$LOWER_LIMIT
  18. until [ "$i" -gt "$UPPER_LIMIT" ]
  19. do
  20.   Primes[i]=$PRIME
  21.   let "i += 1"
  22. done
  23. # Все числа в заданном диапазоне считать простыми,
  24. # пока не доказано обратное.
  25. }
  26. print_primes ()
  27. {
  28. # Вывод индексов элементов массива Primes[], которые признаны простыми.
  29. i=$LOWER_LIMIT
  30. until [ "$i" -gt "$UPPER_LIMIT" ]
  31. do
  32.   if [ "${Primes[i]}" -eq "$PRIME" ]
  33.   then
  34.     printf "%8d" $i
  35.     # 8 пробелов перед числом придают удобочитаемый табличный вывод на экран.
  36.   fi
  37.   let "i += 1"
  38. done
  39. }
  40. sift () # Отсеивание составных чисел.
  41. {
  42. let i=$LOWER_LIMIT+1
  43. # Нам известно, что 1 — это простое число, поэтому начнем с 2.
  44. until [ "$i" -gt "$UPPER_LIMIT" ]
  45. do
  46. if [ "${Primes[i]}" -eq "$PRIME" ]
  47. # Не следует проверять вторично числа, которые уже признаны составными.
  48. then
  49.   t=$i
  50.   while [ "$t" -le "$UPPER_LIMIT" ]
  51.   do
  52.     let "t += $i "
  53.     Primes[t]=$NON_PRIME
  54.     # Все числа, которые делятся на $t без остатка, пометить как составные.
  55.   done
  56. fi
  57.   let "i += 1"
  58. done
  59. }
  60. # Вызов функций.
  61. initialize
  62. sift
  63. print_primes
  64. # Это называется структурным программированием.
  65. echo
  66. exit 0
  67. # ----------------------------------------------- #
  68. # Код, приведенный ниже, не исполняется из-за команды exit, стоящей выше.
  69. # Улучшенная версия, предложенная Stephane Chazelas,
  70. # работает несколько быстрее.
  71. # Должен вызываться с аргументом командной строки, определяющем верхний предел.
  72. UPPER_LIMIT=$1                  # Из командной строки.
  73. let SPLIT=UPPER_LIMIT/2         # Рассматривать делители только до середины диапазона.
  74. Primes=( '' $(seq $UPPER_LIMIT) )
  75. i=1
  76. until (( ( i += 1 ) > SPLIT ))  # Числа из верхней половины диапазона могут не рассматриваться.
  77. do
  78.   if [[ -n $Primes[i] ]]
  79.   then
  80.     t=$i
  81.     until (( ( t += i ) > UPPER_LIMIT ))
  82.     do
  83.       Primes[t]=
  84.     done
  85.   fi
  86. done
  87. echo ${Primes[*]}
  88. exit 0

Сравните этот сценарий с генератором простых чисел, не использующим массивов, Пример A-18.

--

Массивы позволяют эмулировать некоторые структуры данных, поддержка которых в Bash не предусмотрена.

Пример 25-14. Эмуляция структуры "СТЕК" ("первый вошел — последний вышел")

  1. #!/bin/bash
  2. # stack.sh: Эмуляция структуры "СТЕК" ("первый вошел — последний вышел")
  3. #  Подобно стеку процессора, этот "стек" сохраняет и возвращает данные по принципу
  4. #+ "первый вошел — последний вышел".
  5. BP=100            # Базовый указатель на массив-стек.
  6.                   # Дно стека — 100-й элемент.
  7. SP=$BP            # Указатель вершины стека.
  8.                   # Изначально — стек пуст.
  9. Data=             #  Содержимое вершины стека.
  10.                   #  Следует использовать дополнительную переменную,
  11.                   #+ из-за ограничений на диапазон возвращаемых функциями значений.
  12. declare -a stack
  13. push()            # Поместить элемент на вершину стека.
  14. {
  15. if [ -z "$1" ]    # А вообще, есть что помещать на стек?
  16. then
  17.   return
  18. fi
  19. let "SP -= 1"     # Переместить указатель стека.
  20. stack[$SP]=$1
  21. return
  22. }
  23. pop()                    # Снять элемент с вершины стека.
  24. {
  25. Data=                    # Очистить переменную.
  26. if [ "$SP" -eq "$BP" ]   # Стек пуст?
  27. then
  28.   return
  29. fi                       #  Это предохраняет от выхода SP за границу стека — 100,
  30. Data=${stack[$SP]}
  31. let "SP += 1"            # Переместить указатель стека.
  32. return
  33. }
  34. status_report()          # Вывод вспомогательной информации.
  35. {
  36. echo "-------------------------------------"
  37. echo "ОТЧЕТ"
  38. echo "Указатель стека SP = $SP"
  39. echo "Со стека был снят элемент \""$Data"\""
  40. echo "-------------------------------------"
  41. echo
  42. }
  43. # =======================================================
  44. # А теперь позабавимся.
  45. echo
  46. # Попробуем вытолкнуть что-нибудь из пустого стека.
  47. pop
  48. status_report
  49. echo
  50. push garbage
  51. pop
  52. status_report     # Втолкнуть garbage, вытолкнуть garbage.
  53. value1=23; push $value1
  54. value2=skidoo; push $value2
  55. value3=FINAL; push $value3
  56. pop              # FINAL
  57. status_report
  58. pop              # skidoo
  59. status_report
  60. pop              # 23
  61. status_report    # Первый вошел — последний вышел!
  62. #  Обратите внимание как изменяется указатель стека на каждом вызове функций push и pop.
  63. echo
  64. # =======================================================
  65. # Упражнения:
  66. # -----------
  67. # 1)  Измените функцию "push()" таким образом,
  68. #   + чтобы она позволяла помещать на стек несколько значений за один вызов.
  69. # 2)  Измените функцию "pop()" таким образом,
  70. #   + чтобы она позволяла снимать со стека несколько значений за один вызов.
  71. # 3)  Попробуйте написать простейший калькулятор, выполняющий 4 арифметических действия?
  72. #   + используя этот пример.
  73. exit 0

--

Иногда, манипуляции с "индексами" массивов могут потребовать введения переменных для хранения промежуточных результатов. В таких случаях вам предоставляется лишний повод подумать о реализации проекта на более мощном языке программирования, например Perl или C.

Пример 25-15. Исследование математических последовательностей

  1. #!/bin/bash
  2. # Пресловутая "Q-последовательность" Дугласа Хольфштадтера *Douglas Hofstadter):
  3. # Q(1) = Q(2) = 1
  4. # Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), для n>2
  5. # Это "хаотическая" последовательность целых чисел с непредсказуемым поведением.
  6. # Первые 20 членов последовательности:
  7. # 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12
  8. # См. книгу Дугласа Хольфштадтера, "Goedel, Escher, Bach: An Eternal Golden Braid",
  9. # p. 137, ff.
  10. LIMIT=100     # Найти первые 100 членов последовательности
  11. LINEWIDTH=20  # Число членов последовательности, выводимых на экран в одной строке
  12. Q[1]=1        # Первые два члена последовательности равны 1.
  13. Q[2]=1
  14. echo
  15. echo "Q-последовательность [первые $LIMIT членов]:"
  16. echo -n "${Q[1]} "             # Вывести первые два члена последовательности.
  17. echo -n "${Q[2]} "
  18. for ((n=3; n <= $LIMIT; n++))  # C-подобное оформление цикла.
  19. do   # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]]  для n>2
  20. # Это выражение необходимо разбить на отдельные действия,
  21. # поскольку Bash не очень хорошо поддерживает сложные арифметические действия над элементами массивов.
  22.   let "n1 = $n - 1"        # n-1
  23.   let "n2 = $n - 2"        # n-2
  24.   t0=`expr $n - ${Q[n1]}`  # n - Q[n-1]
  25.   t1=`expr $n - ${Q[n2]}`  # n - Q[n-2]
  26.   T0=${Q[t0]}              # Q[n - Q[n-1]]
  27.   T1=${Q[t1]}              # Q[n - Q[n-2]]
  28. Q[n]=`expr $T0 + $T1`      # Q[n - Q[n-1]] + Q[n - Q[n-2]]
  29. echo -n "${Q[n]} "
  30. if [ `expr $n % $LINEWIDTH` -eq 0 ]    # Если выведено очередные 20 членов в строке.
  31. then   # то
  32.   echo # перейти на новую строку.
  33. fi
  34. done
  35. echo
  36. exit 0
  37. # Этот сценарий реализует итеративный алгоритм поиска членов Q-последовательности.
  38. # Рекурсивную реализацию, как более интуитивно понятную, оставляю вам, в качестве упражнения.
  39. # Внимание: рекурсивный поиск членов последовательности будет занимать *очень* продолжительное время.

--

Bash поддерживает только одномерные массивы, но, путем небольших ухищрений, можно эмулировать многомерные массивы.

Пример 25-16. Эмуляция массива с двумя измерениями

  1. #!/bin/bash
  2. # Эмуляция двумерного массива.
  3. # Второе измерение представлено как последовательность строк.
  4. Rows=5
  5. Columns=5
  6. declare -a alpha     # char alpha [Rows] [Columns];
  7.                      # Необязательное объявление массива.
  8. load_alpha ()
  9. {
  10. local rc=0
  11. local index
  12. for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
  13. do
  14.   local row=`expr $rc / $Columns`
  15.   local column=`expr $rc % $Rows`
  16.   let "index = $row * $Rows + $column"
  17.   alpha[$index]=$i   # alpha[$row][$column]
  18.   let "rc += 1"
  19. done
  20. # Более простой вариант
  21. #   declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
  22. # но при таком объявлении второе измерение массива завуалировано.
  23. }
  24. print_alpha ()
  25. {
  26. local row=0
  27. local index
  28. echo
  29. while [ "$row" -lt "$Rows" ]   # Вывод содержимого массива построчно
  30. do
  31.   local column=0
  32.   while [ "$column" -lt "$Columns" ]
  33.   do
  34.     let "index = $row * $Rows + $column"
  35.     echo -n "${alpha[index]} "  # alpha[$row][$column]
  36.     let "column += 1"
  37.   done
  38.   let "row += 1"
  39.   echo
  40. done
  41. # Более простой эквивалент:
  42. #   echo ${alpha[*]} | xargs -n $Columns
  43. echo
  44. }
  45. filter ()     # Отфильтровывание отрицательных индексов.
  46. {
  47. echo -n "  "
  48. if [[ "$1" -ge 0 &&  "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
  49. then
  50.     let "index = $1 * $Rows + $2"
  51.     echo -n " ${alpha[index]}"  # alpha[$row][$column]
  52. fi
  53. }
  54. rotate ()  # Поворот массива на 45 градусов
  55. {
  56. local row
  57. local column
  58. for (( row = Rows; row > -Rows; row-- ))  # В обратном порядке.
  59. do
  60.   for (( column = 0; column < Columns; column++ ))
  61.   do
  62.     if [ "$row" -ge 0 ]
  63.     then
  64.       let "t1 = $column - $row"
  65.       let "t2 = $column"
  66.     else
  67.       let "t1 = $column"
  68.       let "t2 = $column + $row"
  69.     fi
  70.     filter $t1 $t2   # Отфильтровать отрицательный индекс.
  71.   done
  72.   echo; echo
  73. done
  74. # Поворот массива выполнен на основе примеров (стр. 143-146)
  75. # из книги "Advanced C Programming on the IBM PC", автор Herbert Mayer
  76. # (см. библиографию).
  77. }
  78. #-----------------------------------------------------#
  79. load_alpha     # Инициализация массива.
  80. print_alpha    # Вывод на экран.
  81. rotate         # Повернуть на 45 градусов против часовой стрелки.
  82. #-----------------------------------------------------#
  83. # Упражнения:
  84. # -----------
  85. # 1)  Сделайте инициализацию и вывод массива на экран
  86. #   + более простым и элегантным способом.
  87. #
  88. # 2)  Объясните принцип работы функции rotate().
  89. exit 0

По существу, двумерный массив эквивалентен одномерному, с тем лишь различием, что для индексации отдельных элементов используются два индекса — "строка" и "столбец".

Более сложный пример эмуляции двумерного массива вы найдете в Пример A-11.