Перейти к основному содержанию
Рецепты Linux

Main navigation

  • Основы
  • Система
  • Команды
  • Программы
  • Дистро
  • Интерфейсы
  • Устройства
  • Доки
User account menu
  • Войти

Строка навигации

  1. Главная
  2. ABS Guide
  3. Часть 4. Материал повышенной сложности

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

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

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

#!/bin/bash


area[11]=23
area[13]=37
area[51]=UFOs

# Массивы не требуют, чтобы последовательность элементов в массиве была непрерывной.

# Некоторые элементы массива могут оставаться неинициализированными.
# "Дыркм" в массиве не являются ошибкой.


echo -n "area[11] = "
echo ${area[11]} # необходимы {фигурные скобки}

echo -n "area[13] = "
echo ${area[13]}

echo "содержимое area[51] = ${area[51]}."

# Обращение к неинициализированным элементам дает пустую строку.
echo -n "area[43] = "
echo ${area[43]}
echo "(элемент area[43] -- неинициализирован)"

echo

# Сумма двух элементов массива, записанная в третий элемент
area[5]=`expr ${area[11]} + ${area[13]}`
echo "area[5] = area[11] + area[13]"
echo -n "area[5] = "
echo ${area[5]}

area[6]=`expr ${area[11]} + ${area[51]}`
echo "area[6] = area[11] + area[51]"
echo -n "area[6] = "
echo ${area[6]}
# Эта попытка закончится неудачей, поскольку сложение целого числа со строкой не допускается.

echo; echo; echo

# -----------------------------------------------------------------
# Другой массив, "area2".
# И другой способ инициализации массива...
# array_name=( XXX YYY ZZZ ... )

area2=( ноль один два три четыре )

echo -n "area2[0] = "
echo ${area2[0]}
# Ага, индексация начинается с нуля (первый элемент массива имеет индекс [0], а не [1]).

echo -n "area2[1] = "
echo ${area2[1]} # [1] -- второй элемент массива.
# -----------------------------------------------------------------

echo; echo; echo

# -----------------------------------------------
# Еще один массив, "area3".
# И еще один способ инициализации...
# array_name=([xx]=XXX [yy]=YYY ...)

area3=([17]=семнадцать [21]=двадцать_один)

echo -n "area3[17] = "
echo ${area3[17]}

echo -n "area3[21] = "
echo ${area3[21]}
# -----------------------------------------------

exit 0
Note

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

string=abcABC123ABCabc
echo ${string[@]} # abcABC123ABCabc
echo ${string[*]} # abcABC123ABCabc
echo ${string[0]} # abcABC123ABCabc
echo ${string[1]} # Ничего не выводится!
 # Почему?
echo ${#string[@]} # 1
 # Количество элементов в массиве.

# Спасибо Michael Zick за этот пример.

Эти примеры еще раз подтверждают отсутствие контроля типов в Bash.

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

#!/bin/bash
# poem.sh

# Строки из стихотворения (одна строфа).
Line[1]="Мой дядя самых честных правил,"
Line[2]="Когда не в шутку занемог;"
Line[3]="Он уважать себя заставил,"
Line[4]="И лучше выдумать не мог."
Line[5]="Его пример другим наука..."

# Атрибуты.
Attrib[1]=" А.С. Пушкин"
Attrib[2]="\"Евгений Онегин\""

for index in 1 2 3 4 5 # Пять строк.
do
 printf " %s\n" "${Line[index]}"
done

for index in 1 2 # Две строки дополнительных атрибутов.
do
 printf " %s\n" "${Attrib[index]}"
done

exit 0

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

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

#!/bin/bash
# array-ops.sh: Операции над массивами.


array=( ноль один два три четыре пять )

echo ${array[0]} # ноль
echo ${array:0} # ноль
 # Подстановка параметра - первый элемент,
 #+ начиная с позиции 0 (с 1-го символа).
echo ${array:1} # оль
 # Подстановка параметра - первый элемент,
 #+ начиная с позиции 1 (со 2-го символа).

echo "--------------"

echo ${#array[0]} # 4
 # Длина первого элемента массива.
echo ${#array} # 4
 # Длина первого элемента массива.
 # (Альтернативный вариант)

echo ${#array[1]} # 4
 # Длина второго элемента массива.
 # Индексация массивов в Bash начинается с нуля.
		 
echo ${#array[*]} # 6
 # Число элементов в массиве.
echo ${#array[@]} # 6
 # Число элементов в массиве.
				 
echo "--------------"
								 
array2=( [0]="первый элемент" [1]="второй элемент" [3]="четвертый элемент" )
								 
echo ${array2[0]} # первый элемент
echo ${array2[1]} # второй элемент
echo ${array2[2]} #
 # Не был проинициализирован, поэтому null.
echo ${array2[3]} # четвертый элемент
											 
										 
exit 0

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

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

#!/bin/bash
# array-strops.sh: Строковые операции и массивы.
# Автор: Michael Zick.
# Используется с его разрешения.

# Как правило, строковые операции, в нотации ${name ... }
#+ могут использоваться для работы со строковыми элементами массивов
#+ в виде: ${name[@] ... } или ${name[*] ...} .


arrayZ=( one two three four five five )

echo

# Извлечение части строки
echo ${arrayZ[@]:0} # one two three four five five
 # Все элементы массива.

echo ${arrayZ[@]:1} # two three four five five
 # Все эелементы массива, начиная со 2-го.

echo ${arrayZ[@]:1:2} # two three
 # Два элемента массива, начиная со 2-го.

echo "-----------------------"

# Удаление части строки
# Удаляет наименьшую из подстрок, найденых по шаблону (поиск ведется с начала строки)
#+ где шаблон -- это регулярное выражение.

echo ${arrayZ[@]#f*r} # one two three five five
 # Находит подстроку "four" и удаляет ее.
 # Поиск ведется по всем элементам массива
						
# Удаляет наибольшую подстроку, из найденых по шаблону
echo ${arrayZ[@]##t*e} # one two four five five
 # Находит подстроку "three" и удаляет ее.
 # Поиск ведется по всем элементам массива
							
# Удаляет наименьшую из подстрок, найденых по шаблону (поиск ведется с конца строки)
echo ${arrayZ[@]%h*e} # one two t four five five
 # Находит подстроку "hree" и удаляет ее.
 # Поиск ведется по всем элементам массива
																		
# Удаляет наибольшую из подстрок, найденых по шаблону (поиск ведется с конца строки)
echo ${arrayZ[@]%%t*e} # one two four five five
 # Находит подстроку "three" и удаляет ее.
 # Поиск ведется по всем элементам массива

echo "-----------------------"
																
# Замена части строки
																								
# Заменяет первую найденую подстроку заданой строкой
echo ${arrayZ[@]/fiv/XYZ} # one two three four XYZe XYZe
 # Поиск ведется по всем элементам массива
																									 
# Заменяет все найденные подстроки
echo ${arrayZ[@]//iv/YY} # one two three four fYYe fYYe
 # Поиск ведется по всем элементам массива
																															
# Удаляет все найденные подстроки
# Если замещающая строка не задана, то это означает "удаление"
echo ${arrayZ[@]//fi/} # one two three four ve ve
 # Поиск ведется по всем элементам массива
																																		 
# Заменяет первую найденную подстроку (поиск ведется с начала строки)
echo ${arrayZ[@]/#fi/XY} # one two three four XYve XYve
 # Поиск ведется по всем элементам массива

# Заменяет первую найденную подстроку (поиск ведется с конца строки)
echo ${arrayZ[@]/%ve/ZZ} # one two three four fiZZ fiZZ
 # Поиск ведется по всем элементам массива

echo ${arrayZ[@]/%o/XX} # one twXX three four five five
 # Почему?
																																													
echo "-----------------------"
																																													
														
# Before reaching for awk (or anything else)
# Вспомним:
# $( ... ) -- вызов функции.
# Функция запускается как подпроцесс.
# Функции выводят на stdout.
# Присваивание производится со stdout функции.
# Запись name[@] -- означает операцию "for-each".
																																													
newstr() {
 echo -n "!!!"
}
																																													 
echo ${arrayZ[@]/%e/$(newstr)}
# on!!! two thre!!! four fiv!!! fiv!!!
# Q.E.D: Замена -- суть есть "присваивание".
																																													 
# Доступ "For-Each" -- ко всем элементам массива
echo ${arrayZ[@]//*/$(newstr optional_arguments)}
# Now, if Bash would just pass the matched string as $0
#+ to the function being called . . .
																																													 
echo
																																													 
exit 0

Command substitution can construct the individual elements of an array.

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

#!/bin/bash
# script-array.sh: Сценарий загружает себя в массив.
# Идею подал Chris Martin (спасибо!).

script_contents=( $(cat "$0") ) # Записать содержимое этого сценария ($0)
 #+ в массив.

for element in $(seq 0 $((${#script_contents[@]} - 1)))
 do # ${#script_contents[@]}
 #+ дает число элементов массива.
 #
 # Вопрос:
 # Для чего необходима команда seq 0 ?
 # Попробуйте заменить ее на seq 1.
 echo -n "${script_contents[$element]}"
 # Вывести элементы массива в одну строку,
 echo -n " -- " # разделяя их комбинацией " -- " .
done

echo

exit 0

# Упражнение:
# --------
# Попробуйте изменить сценарий таким образом,
#+ чтобы он выводил себя на экран в первоначальном виде,
#+ со всеми пробелами, переводами строк и т.п.

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

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

#!/bin/bash

declare -a colors
# Допускается объявление массива без указания его размера.

echo "Введите ваши любимые цвета (разделяя их пробелами)."

read -a colors # Введите хотя бы 3 цвета для демонстрации некоторых свойств массивов.
# Специфический ключ команды 'read',
#+ позволяющий вводить несколько элементов массива.

echo

element_count=${#colors[@]}

# Получение количества элементов в массиве.
# element_count=${#colors[*]} -- дает тот же результат.
#
# Переменная "@" позволяет "разбивать" строку в кавычках на отдельные слова
#+ (выделяются слова, разделенные пробелами).

index=0

while [ "$index" -lt "$element_count" ]
do # Список всех элементов в массиве.
 echo ${colors[$index]}
 let "index = $index + 1"
done
# Каждый элемент массива выводится в отдельной строке.
# Если этого не требуется, то используйте echo -n "${colors[$index]} "
#
# Эквивалентный цикл "for":
# for i in "${colors[@]}"
# do
# echo "$i"
# done
# (Спасибо S.C.)

echo

# Еще один, более элегантный, способ вывода списка всех элементов массива.
 echo ${colors[@]} # ${colors[*]} дает тот же результат.

echo

# Команда "unset" удаляет элементы из массива, или даже массив целиком.
unset colors[1] # Удаление 2-го элемента массива.
 # Тот же эффект дает команда colors[1]=
echo ${colors[@]} # Список всех элементов массива -- 2-й элемент отсутствует.

unset colors # Удаление всего массива.
 # Тот же эффект имеют команды unset colors[*]
 #+ и unset colors[@].
echo; echo -n "Массив цветов опустошен."
echo ${colors[@]} # Список элементов массива пуст.
							 
exit 0

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

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

#!/bin/bash
# empty-array.sh

# Выражаю свою благодарность Stephane Chazelas за этот пример,
#+ и Michael Zick за его доработку.


# Пустой массив -- это не то же самое, что массив с пустыми элементами.

array0=( первый второй третий )
array1=( '' ) # "array1" имеет один пустой элемент.
array2=( ) # Массив "array2" не имеет ни одного элемента, т.е. пуст.

echo
ListArray()
{
echo
echo "Элементы массива array0: ${array0[@]}"
echo "Элементы массива array1: ${array1[@]}"
echo "Элементы массива array2: ${array2[@]}"
echo
echo "Длина первого элемента массива array0 = ${#array0}"
echo "Длина первого элемента массива array1 = ${#array1}"
echo "Длина первого элемента массива array2 = ${#array2}"
echo
echo "Число элементов в массиве array0 = ${#array0[*]}" # 3
echo "Число элементов в массиве array1 = ${#array1[*]}" # 1 (сюрприз!)
echo "Число элементов в массиве array2 = ${#array2[*]}" # 0
}

# ===================================================================

ListArray

# Попробуем добавить новые элементы в массивы

# Добавление новых элементов в массивы.
array0=( "${array0[@]}" "новый1" )
array1=( "${array1[@]}" "новый1" )
array2=( "${array2[@]}" "новый1" )

ListArray

# или
array0[${#array0[*]}]="новый2"
array1[${#array1[*]}]="новый2"
array2[${#array2[*]}]="новый2"

ListArray

# Теперь представим каждый массив как 'стек' ('stack')
# Команды выше, можно считать командами 'push' -- добавление нового значения на вершину стека
# 'Глубина' стека:
height=${#array2[@]}
echo
echo "Глубина стека array2 = $height"

# Команда 'pop' -- выталкивание элемента стека, находящегося на вершине:
unset array2[${#array2[@]}-1] # Индексация массивов начинается с нуля
height=${#array2[@]}
echo
echo "POP"
echo "Глубина стека array2, после выталкивания = $height"

ListArray

# Вывести только 2-й и 3-й элементы массива array0
from=1 # Индексация массивов начинается с нуля
to=2 #
declare -a array3=( ${array0[@]:1:2} )
echo
echo "Элементы массива array3: ${array3[@]}"

# Замена элементов по шаблону
declare -a array4=( ${array0[@]/второй/2-й} )
echo
echo "Элементы массива array4: ${array4[@]}"

# Замена строк по шаблону
declare -a array5=( ${array0[@]//новый?/старый} )
echo
echo "Элементы массива array5: ${array5[@]}"

# Надо лишь привыкнуть к такой записи...
declare -a array6=( ${array0[@]#*новый} )
echo # Это может вас несколько удивить
echo "Элементы массива array6: ${array6[@]}"

declare -a array7=( ${array0[@]#новый1} )
echo # Теперь это вас уже не должно удивлять
echo "Элементы массива array7: ${array7[@]}"

# Выглядить очень похоже на предыдущий вариант...
declare -a array8=( ${array0[@]/новый1/} )
echo
echo "Элементы массива array8: ${array8[@]}"

# Итак, что вы можете сказать обо всем этом?

# Строковые операции выполняются последовательно, над каждым элементом
#+ в массиве var[@].
# Таким образом, BASH поддерживает векторные операции
# Если в результате операции получается пустая строка, то
#+ элемент массива "исчезает".

# Вопрос: это относится к строкам в "строгих" или "мягких" кавычках?

zap='новый*'
declare -a array9=( ${array0[@]/$zap/} )
echo
echo "Элементы массива array9: ${array9[@]}"

# "...А с платформы говорят: "Это город Ленинград!"..."
declare -a array10=( ${array0[@]#$zap} )
echo
echo "Элементы массива array10: ${array10[@]}"

# Сравните массивы array7 и array10
# Сравните массивы array8 и array9

# Ответ: в "мягких" кавычках.

exit 0

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

# Копирование массивов.
array2=( "${array1[@]}" )
# или
array2="${array1[@]}"

# Добавить элемент.
array=( "${array[@]}" "новый элемент" )
# или
array[${#array[*]}]="новый элемент"

# Спасибо S.C.
Tip

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

#!/bin/bash

filename=sample_file

# cat sample_file
#
# 1 a b c
# 2 d e fg


declare -a array1

array1=( `cat "$filename"`) # Загрузка содержимого файла
 # $filename в массив array1.
# Вывод на stdout.
#
# array1=( `cat "$filename" | tr '\n' ' '`)
# с заменой символов перевода строки на пробелы.
# Впрочем, в этом нет необходимости, поскольку Bash 
#+ выполняет разбивку по словам заменяя символы перевода строки
#+ на пробелы автоматически.

echo ${array1[@]} # список элементов массива.
# 1 a b c 2 d e fg
#
# Каждое "слово", в текстовом файле, отделяемое от других пробелами
#+ заносится в отдельный элемент массива.

element_count=${#array1[*]}
echo $element_count # 8

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

#! /bin/bash
# array-assign.bash

# Поскольку здесь рассматриваются операции, специфичные для Bash,
#+ файл сценария имеет расширение ".bash".

# Copyright (c) Michael S. Zick, 2003, All rights reserved.
# Лицензионное соглашение: Допускается использование сценария
# в любом виде без каких либо ограничений.
# Версия: $ID$

# Основан на примере, предоставленом Stephane Chazelas,
#+ который включен в состав книги: Advanced Bash Scripting Guide.

# Формат вывода команды 'times':
# User CPU  System CPU
# User CPU of dead children  System CPU of dead children

# Bash предоставляет два способа записи всех элементов
#+ одного массива в другой.
# В Bash, версий 2.04, 2.05a и 2.05b,
#+ оба они пропускают 'пустые' элементы
# В более новых версиях добавлена возможность присваивания
#+ в виде [индекс]=значение.

declare -a bigOne=( /dev/* )
echo
echo 'Условия проверки: Отсутствие кавычек, IFS по-умолчанию, Все-Элементы'
echo "Количество элементов в массиве: ${#bigOne[@]}"

# set -vx


echo
echo '- - проверяется: =( ${array[@]} ) - -'
times
declare -a bigTwo=( ${bigOne[@]} )
# ^ ^
times

echo
echo '- - проверяется: =${array[@]} - -'
times
declare -a bigThree=${bigOne[@]}
# Обратите внимание: круглые скобки отсутствуют.
times

# Сравнение временных показателей свидетельствует о том, что вторая форма записи,
#+ как заметил Stephane Chazelas, работает в 3-4 раза быстрее.

# Тем не менее, в своих примерах, я буду продолжать использовать первую форму записи
#+ потому что, на мой взгляд, она более показательна.

# Однако, в отдельных случаях, я буду использовать вторую форму записи,
#+ с целью увеличения скорости исполнения сценариев.

# MSZ: Прошу прощения, что не предупредил об этом заранее!


exit 0
Note

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

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

#! /bin/bash
# CopyArray.sh
#
# Автор: Michael Zick.
# Используется с его разрешения.

# "Принять из массива с заданным именем записать в массив с заданным именем"
#+ или "собственный Оператор Присваивания".


CpArray_Mac() {

# Оператор Присваивания

 echo -n 'eval '
 echo -n "$2" # Имя массива-результата
 echo -n '=( ${'
 echo -n "$1" # Имя исходного массива
 echo -n '[@]} )'

# Все это могло бы быть объединено в одну команду.
# Это лишь вопрос стиля.
}

declare -f CopyArray # "Указатель" на функцию
CopyArray=CpArray_Mac # Оператор Присваивания

Hype()
{

# Исходный массив с именем в $1.
# (Слить с массивом, содержащим "-- Настоящий Рок-н-Ролл".)
# Вернуть результат в массиве с именем $2.

 local -a TMP
 local -a hype=( -- Настоящий Рок-н-Ролл )

 $($CopyArray $1 TMP)
 TMP=( ${TMP[@]} ${hype[@]} )
 $($CopyArray TMP $2)
}

declare -a before=( Advanced Bash Scripting )
declare -a after

echo "Массив before = ${before[@]}"

Hype before after

echo "Массив after = ${after[@]}"

# Еще?

echo "Что такое ${after[@]:4:2}?"

declare -a modest=( ${after[@]:2:1} ${after[@]:3:3} )
# ---- выделение подстроки ----

echo "Массив Modest = ${modest[@]}"

# А что в массиве 'before' ?

echo "Массив Before = ${before[@]}"

exit 0

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

#! /bin/bash
# array-append.bash

# Copyright (c) Michael S. Zick, 2003, All rights reserved.
# Лицензионное соглашение: Допускается использование сценария
# в любом виде без каких либо ограничений.
# Версия: $ID$
#
# С небольшими изменениями, внесенными автором книги.


# Действия над массивами являются специфичными для Bash.
# Эквиваленты в /bin/sh отсутствуют!


# Чтобы избежать скроллинга выводимой информации за пределы терминала,
#+ передайте вывод от сценария, через конвейер, команде 'more'.


# Упакованный массив.
declare -a array1=( zero1 one1 two1 )
# Разреженный массив (элемент [1] -- не определен).
declare -a array2=( [0]=zero2 [2]=two2 [3]=three2 )

echo
echo '- Проверка того, что массив получился разреженным. -'
echo "Число элементов: 4" # Жестко "зашито", в демонстрационных целях.
for (( i = 0 ; i  4 ; i++ ))
do
 echo "Элемент [$i]: ${array2[$i]}"
done
# См. так же пример basics-reviewed.bash.
 
 
declare -a dest
 
# Конкатенация двух массивов.
echo
echo 'Условия: Отсутствие кавычек, IFS по-умолчанию, Все-Элементы'
echo '- Неопределенные элементы не передаются. -'
# # На самом деле неопределенные элемены отсутствуют в массиве.
 
dest=( ${array1[@]} ${array2[@]} )
# dest=${array1[@]}${array2[@]} # Странный результат, возможно ошибка.
 
# Теперь выведем результат.
echo
echo '- - Проверка конкатенации массивов - -'
cnt=${#dest[@]}
 
echo "Число элементов: $cnt"
for (( i = 0 ; i  cnt ; i++ ))
do
 echo "Элемент [$i]: ${dest[$i]}"
done
	
# Записать массив в элемент другого массива (дважды).
dest[0]=${array1[@]}
dest[1]=${array2[@]}
	
# Вывести результат.
echo
echo '- - Проверка записи содержимого одного массива в элемент другого массива - -'
cnt=${#dest[@]}
	
echo "Число элементов: $cnt"
for (( i = 0 ; i  cnt ; i++ ))
do
 echo "Элемент [$i]: ${dest[$i]}"
 done
	 
# Рассмотрение содержимого второго элемента.
echo
echo '- - Запись содержимого второго элемента и вывод результата - -'
	 
declare -a subArray=${dest[1]}
cnt=${#subArray[@]}
	 
echo "Число элементов: $cnt"
for (( i = 0 ; i  cnt ; i++ ))
do
 echo "Элемент [$i]: ${subArray[$i]}"
done
		
# Запись содержимого целого массива в элемент другого массива,
#+ с помощью конструкции '=${ ... }',
#+ приводит к преобразованию содержимого первого массива в строку,
#+ в которой отдельные жлементы первого массива разделены пробелом
#+ (первый символ из переменной IFS).
		
# If the original elements didn't contain whitespace . . .
# If the original array isn't subscript sparse . . .
# Then we could get the original array structure back again.
		
# Restore from the modified second element.
echo
echo '- - Listing restored element - -'
		
declare -a subArray=( ${dest[1]} )
cnt=${#subArray[@]}
		
echo "Number of elements: $cnt"
for (( i = 0 ; i  cnt ; i++ ))
do
 echo "Element [$i]: ${subArray[$i]}"
done
echo '- - Do not depend on this behavior. - -'
echo '- - This behavior is subject to change - -'
echo '- - in versions of Bash newer than version 2.05b - -'
		 
# MSZ: Sorry about any earlier confusion folks.
		 
exit 0

--

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

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

#!/bin/bash
# bubble.sh: "Пузырьковая" сортировка.

# На каждом проходе по сортируемому массиву,
#+ сравниваются два смежных элемента, и, если необходимо, они меняются местами.
# В конце первого прохода, самый "тяжелый" элемент "опускается" в конец массива.
# В конце второго прохода, следующий по "тяжести" элемент занимает второе место снизу.
# И так далее.
# Каждый последующий проход требует на одно сравнение меньше предыдущего.
# Поэтому вы должны заметить ускорение работы сценария на последних проходах.


exchange()
{
 # Поменять местами два элемента массива.
 local temp=${Countries[$1]} # Временная переменная
 Countries[$1]=${Countries[$2]}
 Countries[$2]=$temp

 return
}

declare -a Countries # Объявление массива,
 #+ необязательно, поскольку он явно инициализируется ниже.

# Допустимо ли выполнять инициализацию массива в нескольки строках?
# ДА!

Countries=(Нидерланды Украина Заир Турция Россия Йемен Сирия \
Бразилия Аргентина Никарагуа Япония Мексика Венесуэла Греция Англия \
Израиль Перу Канада Оман Дания Уэльс Франция Кения \
Занаду Катар Лихтенштейн Венгрия)

# "Занаду" -- это мифическое государство, где, согласно Coleridge,
#+ Kubla Khan построил величественный дворец.


clear # Очистка экрана.

echo "0: ${Countries[*]}" # Список элементов несортированного массива.

number_of_elements=${#Countries[@]}
let "comparisons = $number_of_elements - 1"

count=1 # Номер прохода.

while [ "$comparisons" -gt 0 ] # Начало внешнего цикла
do

 index=0 # Сбросить индекс перед началом каждого прохода.
 
 while [ "$index" -lt "$comparisons" ] # Начало внутреннего цикла
 do
 if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
 # Если элементы стоят не по порядку...
 # Оператор \> выполняет сравнение ASCII-строк
 #+ внутри одиночных квадратных скобок.
		 
 # if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]]
 #+ дает тот же результат.
 then
 exchange $index `expr $index + 1` # Поменять местами.
 fi
 let "index += 1"
 done # Конец внутреннего цикла
						 
						 
let "comparisons -= 1" # Поскольку самый "тяжелый" элемент уже "опустился" на дно,
 #+ то на каждом последующем проходе нужно выполнять на одно сравнение меньше.
									 
echo
echo "$count: ${Countries[@]}" # Вывести содержимое массива после каждого прохода.
echo
let "count += 1" # Увеличить счетчик проходов.
									 
done # Конец внешнего цикла
									 
exit 0

--

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

#!/bin/bash
# "Вложенный" массив.

# Автор: Michael Zick.
#+ незначительные изменения и комментарии добавил William Park.

AnArray=( $(ls --inode --ignore-backups --almost-all \
 --directory --full-time --color=none --time=status \
 --sort=time -l ${PWD} ) ) # Команды и опции.

# Пробелы важны . . .

SubArray=( ${AnArray[@]:11:1} ${AnArray[@]:6:5} )
# Этот массив содержит шесть элементов:
#+ SubArray=( [0]=${AnArray[11]} [1]=${AnArray[6]} [2]=${AnArray[7]}
# [3]=${AnArray[8]} [4]=${AnArray[9]} [5]=${AnArray[10]} )
#
# Массивы в Bash оформляются в виде связанных (циклических) списков
#+ где каждый элемент списка имеет тип string (char *).
# Таким образом, вложенные массивы фактически таковыми не являются,
#+ хотя функционально очень похожи на них.

echo "Текущий каталог и дата последнего изменения:"
echo "${SubArray[@]}"

exit 0

--

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

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

#!/bin/bash
# embedded-arrays.sh
# Вложенные массивы и косвенные ссылки.

# Автор: Dennis Leeuw.
# Используется с его разрешения.
# Дополнен автором документа.


ARRAY1=(
 VAR1_1=value11
 VAR1_2=value12
 VAR1_3=value13
)

ARRAY2=(
 VARIABLE="test"
 STRING="VAR1=value1 VAR2=value2 VAR3=value3"
 ARRAY21=${ARRAY1[*]}
) # Вложение массива ARRAY1 в массив ARRAY2.

function print () {
 OLD_IFS="$IFS"
 IFS=$'\n' # Вывод каждого элемента массива
 #+ в отдельной строке.
 TEST1="ARRAY2[*]"
 local ${!TEST1} # Посмотрите, что произойдет, если убрать эту строку.
 # Косвенная ссылка.
 # Позволяет получить доступ к компонентам $TEST1
 #+ в этой функции.


 # Посмотрим, что получилось.
 echo
 echo "\$TEST1 = $TEST1" # Просто имя переменной.
 echo; echo
 echo "{\$TEST1} = ${!TEST1}" # Вывод на экран содержимого переменной.
 # Это то, что дает
 #+ косвенная ссылка.
 echo
 echo "-------------------------------------------"; echo
 echo


 # Вывод переменной
 echo "Переменная VARIABLE: $VARIABLE"

 # Вывод элементов строки
 IFS="$OLD_IFS"
 TEST2="STRING[*]"
 local ${!TEST2} # Косвенная ссылка (то же, что и выше).
 echo "Элемент VAR2: $VAR2 из строки STRING"

 # Вывод элемента массива
 TEST2="ARRAY21[*]"
 local ${!TEST2} # Косвенная ссылка.
 echo "Элемент VAR1_1: $VAR1_1 из массива ARRAY21"
}
		
print
echo
		
exit 0

--

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

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

#!/bin/bash
# sieve.sh

# Решето Эратосфена
# Очень старый алгоритм поиска простых чисел.

# Этот сценарий выполняется во много раз медленнее
# чем аналогичная программа на C.

LOWER_LIMIT=1 # Начиная с 1.
UPPER_LIMIT=1000 # До 1000.
# (Вы можете установить верхний предел и выше... если вам есть чем себя занять.)

PRIME=1
NON_PRIME=0

declare -a Primes
# Primes[] -- массив.


initialize ()
{
# Инициализация массива.

i=$LOWER_LIMIT
until [ "$i" -gt "$UPPER_LIMIT" ]
do
 Primes[i]=$PRIME
 let "i += 1"
done
# Все числа в заданном диапазоне считать простыми,
# пока не доказано обратное.
}

print_primes ()
{
# Вывод индексов элементов массива Primes[], которые признаны простыми.

i=$LOWER_LIMIT

until [ "$i" -gt "$UPPER_LIMIT" ]
do

 if [ "${Primes[i]}" -eq "$PRIME" ]
 then
 printf "%8d" $i
 # 8 пробелов перед числом придают удобочитаемый табличный вывод на экран.
 fi

 let "i += 1"

done

}

sift () # Отсеивание составных чисел.
{

let i=$LOWER_LIMIT+1
# Нам известно, что 1 -- это простое число, поэтому начнем с 2.

until [ "$i" -gt "$UPPER_LIMIT" ]
do

if [ "${Primes[i]}" -eq "$PRIME" ]
# Не следует проверять вторично числа, которые уже признаны составными.
then

 t=$i

 while [ "$t" -le "$UPPER_LIMIT" ]
 do
 let "t += $i "
 Primes[t]=$NON_PRIME
 # Все числа, которые делятся на $t без остатка, пометить как составные.
 done

fi

 let "i += 1"
done


}


# Вызов функций.
initialize
sift
print_primes
# Это называется структурным программированием.

echo

exit 0



# ----------------------------------------------- #
# Код, приведенный ниже, не исполняется из-за команды exit, стоящей выше.

# Улучшенная версия, предложенная Stephane Chazelas,
# работает несколько быстрее.

# Должен вызываться с аргументом командной строки, определяющем верхний предел.

UPPER_LIMIT=$1 # Из командной строки.
let SPLIT=UPPER_LIMIT/2 # Рассматривать делители только до середины диапазона.

Primes=( '' $(seq $UPPER_LIMIT) )

i=1
until (( ( i += 1 ) > SPLIT )) # Числа из верхней половины диапазона могут не рассматриваться.
do
 if [[ -n $Primes[i] ]]
 then
 t=$i
 until (( ( t += i ) > UPPER_LIMIT ))
 do
 Primes[t]=
 done
 fi
done
echo ${Primes[*]}

exit 0

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

--

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

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

#!/bin/bash
# stack.sh: Эмуляция структуры "СТЕК" ("первый вошел -- последний вышел")

# Подобно стеку процессора, этот "стек" сохраняет и возвращает данные по принципу
#+ "первый вошел -- последний вышел".

BP=100 # Базовый указатель на массив-стек.
 # Дно стека -- 100-й элемент.

SP=$BP # Указатель вершины стека.
 # Изначально -- стек пуст.

Data= # Содержимое вершины стека.
 # Следует использовать дополнительную переменную,
 #+ из-за ограничений на диапазон возвращаемых функциями значений.

declare -a stack


push() # Поместить элемент на вершину стека.
{
if [ -z "$1" ] # А вообще, есть что помещать на стек?
then
 return
fi

let "SP -= 1" # Переместить указатель стека.
stack[$SP]=$1

return
}

pop() # Снять элемент с вершины стека.
{
Data= # Очистить переменную.

if [ "$SP" -eq "$BP" ] # Стек пуст?
then
 return
fi # Это предохраняет от выхода SP за границу стека -- 100,

Data=${stack[$SP]}
let "SP += 1" # Переместить указатель стека.
return
}

status_report() # Вывод вспомогательной информации.
{
echo "-------------------------------------"
echo "ОТЧЕТ"
echo "Указатель стека SP = $SP"
echo "Со стека был снят элемент \""$Data"\""
echo "-------------------------------------"
echo
}


# =======================================================
# А теперь позабавимся.

echo

# Попробуем вытолкнуть что-нибудь из пустого стека.
pop
status_report

echo

push garbage
pop
status_report # Втолкнуть garbage, вытолкнуть garbage.

value1=23; push $value1
value2=skidoo; push $value2
value3=FINAL; push $value3

pop # FINAL
status_report
pop # skidoo
status_report
pop # 23
status_report # Первый вошел -- последний вышел!

# Обратите внимание как изменяется указатель стека на каждом вызове функций push и pop.

echo
# =======================================================


# Упражнения:
# -----------

# 1) Измените функцию "push()" таким образом,
# + чтобы она позволяла помещать на стек несколько значений за один вызов.

# 2) Измените функцию "pop()" таким образом,
# + чтобы она позволяла снимать со стека несколько значений за один вызов.

# 3) Попробуйте написать простейший калькулятор, выполняющий 4 арифметических действия?
# + используя этот пример.

exit 0

--

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

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

#!/bin/bash

# Пресловутая "Q-последовательность" Дугласа Хольфштадтера *Douglas Hofstadter):

# Q(1) = Q(2) = 1
# Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), для n>2

# Это "хаотическая" последовательность целых чисел с непредсказуемым поведением.
# Первые 20 членов последовательности:
# 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12

# См. книгу Дугласа Хольфштадтера, "Goedel, Escher, Bach: An Eternal Golden Braid",
# p. 137, ff.


LIMIT=100 # Найти первые 100 членов последовательности
LINEWIDTH=20 # Число членов последовательности, выводимых на экран в одной строке

Q[1]=1 # Первые два члена последовательности равны 1.
Q[2]=1

echo
echo "Q-последовательность [первые $LIMIT членов]:"
echo -n "${Q[1]} " # Вывести первые два члена последовательности.
echo -n "${Q[2]} "

for ((n=3; n = $LIMIT; n++)) # C-подобное оформление цикла.
do # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]] для n>2
# Это выражение необходимо разбить на отдельные действия,
# поскольку Bash не очень хорошо поддерживает сложные арифметические действия над элементами массивов.

 let "n1 = $n - 1" # n-1
 let "n2 = $n - 2" # n-2
 
 t0=`expr $n - ${Q[n1]}` # n - Q[n-1]
 t1=`expr $n - ${Q[n2]}` # n - Q[n-2]
	
 T0=${Q[t0]} # Q[n - Q[n-1]]
 T1=${Q[t1]} # Q[n - Q[n-2]]
	 
Q[n]=`expr $T0 + $T1` # Q[n - Q[n-1]] + Q[n - Q[n-2]]
echo -n "${Q[n]} "
	 
if [ `expr $n % $LINEWIDTH` -eq 0 ] # Если выведено очередные 20 членов в строке.
then # то
 echo # перейти на новую строку.
fi
	 
done
	 
echo
	 
exit 0
	 
# Этот сценарий реализует итеративный алгоритм поиска членов Q-последовательности.
# Рекурсивную реализацию, как более интуитивно понятную, оставляю вам, в качестве упражнения.
# Внимание: рекурсивный поиск членов последовательности будет занимать *очень* продолжительное время.

--

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

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

#!/bin/bash
# Эмуляция двумерного массива.

# Второе измерение представлено как последовательность строк.

Rows=5
Columns=5

declare -a alpha # char alpha [Rows] [Columns];
 # Необязательное объявление массива.

load_alpha ()
{
local rc=0
local index


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
do
 local row=`expr $rc / $Columns`
 local column=`expr $rc % $Rows`
 let "index = $row * $Rows + $column"
 alpha[$index]=$i # alpha[$row][$column]
 let "rc += 1"
done

# Более простой вариант
# 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 )
# но при таком объявлении второе измерение массива завуалировано.
}

print_alpha ()
{
local row=0
local index

echo

while [ "$row" -lt "$Rows" ] # Вывод содержимого массива построчно
do

 local column=0

 while [ "$column" -lt "$Columns" ]
 do
 let "index = $row * $Rows + $column"
 echo -n "${alpha[index]} " # alpha[$row][$column]
 let "column += 1"
 done

 let "row += 1"
 echo

done

# Более простой эквивалент:
# echo ${alpha[*]} | xargs -n $Columns

echo
}

filter () # Отфильтровывание отрицательных индексов.
{

echo -n " "

if [[ "$1" -ge 0 && "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
then
 let "index = $1 * $Rows + $2"
 echo -n " ${alpha[index]}" # alpha[$row][$column]
fi
	
}
	
rotate () # Поворот массива на 45 градусов
{
local row
local column
	
for (( row = Rows; row > -Rows; row-- )) # В обратном порядке.
do
	
 for (( column = 0; column  Columns; column++ ))
 do
	 
 if [ "$row" -ge 0 ]
 then
 let "t1 = $column - $row"
 let "t2 = $column"
 else
 let "t1 = $column"
 let "t2 = $column + $row"
 fi
						 
 filter $t1 $t2 # Отфильтровать отрицательный индекс.
 done
							 
 echo; echo
							 
done
							 
# Поворот массива выполнен на основе примеров (стр. 143-146)
# из книги "Advanced C Programming on the IBM PC", автор Herbert Mayer
# (см. библиографию).
							 
}
							 
							 
#-----------------------------------------------------#
load_alpha # Инициализация массива.
print_alpha # Вывод на экран.
rotate # Повернуть на 45 градусов против часовой стрелки.
#-----------------------------------------------------#
							 
							 
# Упражнения:
# -----------
# 1) Сделайте инициализацию и вывод массива на экран
# + более простым и элегантным способом.
#
# 2) Объясните принцип работы функции rotate().
							 
exit 0

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

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

Перекрёстные ссылки книги для Глава 25. Массивы

  • Глава 24. Списки команд
  • Вверх
  • Глава 26. Файлы

Book navigation

  • Содержание
  • Часть 1. Введение
  • Часть 2. Основы
  • Часть 3. Углубленный материал
  • Часть 4. Материал повышенной сложности
    • Глава 18. Регулярные выражения
    • Глава 19. Подоболочки, или Subshells
    • Глава 20. Ограниченный режим командной оболочки
    • Глава 21. Подстановка процессов
    • Глава 22. Функции
    • Глава 23. Псевдонимы
    • Глава 24. Списки команд
    • Глава 25. Массивы
    • Глава 26. Файлы
    • Глава 27. /dev и /proc
    • Глава 28. /dev/zero и /dev/null
    • Глава 29. Отладка сценариев
    • Глава 30. Необязательные параметры (ключи)
    • Глава 31. Широко распространенные ошибки
    • Глава 32. Стиль программирования
    • Глава 33. Разное
    • Глава 34. Bash, версия 2
  • Глава 35. Замечания и дополнения
  • Библиография
  • Приложение A. Дополнительные примеры сценариев
  • Приложение B. Справочная информация
  • Приложение C. Маленький учебник по Sed и Awk
  • Приложение D. Коды завершения, имеющие предопределенный смысл
  • Приложение E. Подробное введение в операции ввода-вывода и перенаправление ввода-вывода
  • Приложение F. Системные каталоги
  • Приложение G. Локализация
  • Приложение H. История команд
  • Приложение I. Пример файла .bashrc
  • Приложение J. Преобразование пакетных (*.bat) файлов DOS в сценарии командной оболочки
  • Приложение K. Упражнения
  • Приложение L. Хронология
  • Приложение M. Авторские права

Последние материалы

  • Утилита sensors
    2 hours ago
  • Сканер Rkhunter
    1 week ago
  • Программа resize2fs
    1 week 6 days ago
  • Аудиопроигрыватель QMMP
    2 weeks 4 days ago
  • Программа Timeshift
    3 weeks 3 days ago
RSS feed

Secondary menu

  • О проекте

© 2008–2025 Олег Меньшенин mensh@yandex.ru