Как отсортировать массив в Bash
У меня есть массив в Bash, например:
array=(a c b f 3 5)
Мне нужно отсортировать массив. Не просто показ содержимого сортированным способом, но и получение нового массива с отсортированными элементами. Новый отсортированный массив может быть совершенно новым или старым.
Ответы
Ответ 1
Вам не нужно много кода:
IFS=$'\n' sorted=($(sort <<<"${array[*]}"))
unset IFS
Поддерживает пробелы в элементах (если только это не перевод строки) и работает в Bash 3.x.
например.:
$ array=("a c" b f "3 5")
$ IFS=$'\n' sorted=($(sort <<<"${array[*]}")); unset IFS
$ printf "[%s]\n" "${sorted[@]}"
[3 5]
[a c]
[b]
[f]
Примечание. @sorontar указал, что необходимо соблюдать осторожность, если элементы содержат символы подстановки, такие как *
или ?
:
Часть sorted = ($ (...)) использует оператор "split и glob". Вы должны выключить glob: set -f
или set -o noglob
или shopt -op noglob
, или элемент массива, например *
, будет расширен до списка файлов.
Что происходит:
В результате получается шесть вещей, которые происходят в следующем порядке:
IFS=$'\n'
"${array[*]}"
<<<
sort
sorted=($(...))
unset IFS
Во-первых, IFS=$'\n'
Это важная часть нашей операции, которая влияет на результаты 2 и 5 следующим образом:
Дано:
"${array[*]}"
расширяется до каждого элемента, ограниченного первым символом IFS
sorted=()
создает элементы путем разделения на каждый символ IFS
IFS=$'\n'
настраивает так, чтобы элементы расширялись с использованием новой строки в качестве разделителя, а затем создавались таким образом, чтобы каждая строка становилась элементом. (то есть разделение на новую строку.)
Разделение новой строкой важно, потому что так работает sort
(сортировка по строке). Разделение только новой строкой не так важно, но необходимо сохранить элементы, содержащие пробелы или табуляции.
Значение по умолчанию IFS
- это пробел, табуляция, за которой следует новая строка, и она не подходит для нашей работы.
Далее часть sort <<<"${array[*]}"
<<<
, называемый here strings, принимает расширение "${array[*]}"
, как описано выше, и подает его на стандартный ввод sort
.
В нашем примере sort
передается следующая строка:
a c
b
f
3 5
Так как sort
сортирует, он производит:
3 5
a c
b
f
Далее, часть sorted=($(...))
Часть $(...)
, называемая подстановка команд, заставляет ее содержимое (sort <<<"${array[*]}
) запускаться как обычную команду, в то время как полученный стандартный вывод выводится как литерал, который идет куда угодно $(...)
.
В нашем примере это производит нечто похожее на простое написание:
sorted=(3 5
a c
b
f
)
sorted
затем становится массивом, который создается путем разбиения этого литерала на каждой новой строке.
Наконец, unset IFS
Это сбрасывает значение IFS
на значение по умолчанию, и это просто хорошая практика.
Это сделано для того, чтобы мы не создавали проблем с чем-либо, что опирается на IFS
позже в нашем скрипте. (В противном случае нам нужно помнить, что мы все изменили - что может быть непрактично для сложных сценариев.)
Ответ 2
Оригинальный ответ:
array=(a c b "f f" 3 5)
readarray -t sorted < <(for a in "${array[@]}"; do echo "$a"; done | sort)
выход:
$ for a in "${sorted[@]}"; do echo "$a"; done
3
5
a
b
c
f f
Примечание. Эта версия соответствует значениям, содержащим специальные символы или пробелы (кроме строк новой строки)
Примечание readarray поддерживается в bash 4+.
Изменить. Основываясь на предложении @Dimitre, я обновил его до:
readarray -t sorted < <(printf '%s\0' "${array[@]}" | sort -z | xargs -0n1)
который имеет преимущество даже в понимании элементов сортировки с символами новой строки, встроенными правильно. К сожалению, как правильно сигнализировал @ruakh, это не означало, что результат readarray
был бы правильным, потому что readarray
не имеет возможности использовать NUL
вместо обычных новых строк как разделители строк.
Ответ 3
Здесь используется чистая реализация Bash quicksort:
#!/bin/bash
# quicksorts positional arguments
# return is in array qsort_ret
qsort() {
local pivot i smaller=() larger=()
qsort_ret=()
(($#==0)) && return 0
pivot=$1
shift
for i; do
if [[ $i < $pivot ]]; then
smaller+=( "$i" )
else
larger+=( "$i" )
fi
done
qsort "${smaller[@]}"
smaller=( "${qsort_ret[@]}" )
qsort "${larger[@]}"
larger=( "${qsort_ret[@]}" )
qsort_ret=( "${smaller[@]}" "$pivot" "${larger[@]}" )
}
Использовать как, например,
$ array=(a c b f 3 5)
$ qsort "${array[@]}"
$ declare -p qsort_ret
declare -a qsort_ret='([0]="3" [1]="5" [2]="a" [3]="b" [4]="c" [5]="f")'
Эта реализация рекурсивна... так что здесь итеративный quicksort:
#!/bin/bash
# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
qsort() {
(($#==0)) && return 0
local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
qsort_ret=("[email protected]")
while ((${#stack[@]})); do
beg=${stack[0]}
end=${stack[1]}
stack=( "${stack[@]:2}" )
smaller=() larger=()
pivot=${qsort_ret[beg]}
for ((i=beg+1;i<=end;++i)); do
if [[ "${qsort_ret[i]}" < "$pivot" ]]; then
smaller+=( "${qsort_ret[i]}" )
else
larger+=( "${qsort_ret[i]}" )
fi
done
qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" )
if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi
if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi
done
}
В обоих случаях вы можете изменить используемый порядок: я использовал сравнения строк, но вы можете использовать арифметические сравнения, сравнить время модификации файла wrt и т.д., просто используйте соответствующий тест; вы даже можете сделать его более общим и использовать первый аргумент, который используется для использования тестовой функции, например,
#!/bin/bash
# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
# First argument is a function name that takes two arguments and compares them
qsort() {
(($#<=1)) && return 0
local compare_fun=$1
shift
local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
qsort_ret=("[email protected]")
while ((${#stack[@]})); do
beg=${stack[0]}
end=${stack[1]}
stack=( "${stack[@]:2}" )
smaller=() larger=()
pivot=${qsort_ret[beg]}
for ((i=beg+1;i<=end;++i)); do
if "$compare_fun" "${qsort_ret[i]}" "$pivot"; then
smaller+=( "${qsort_ret[i]}" )
else
larger+=( "${qsort_ret[i]}" )
fi
done
qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" )
if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi
if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi
done
}
Затем вы можете использовать эту функцию сравнения:
compare_mtime() { [[ $1 -nt $2 ]]; }
и используйте:
$ qsort compare_mtime *
$ declare -p qsort_ret
чтобы файлы в текущей папке отсортировались по времени модификации (сначала самые новые).
Примечание. Эти функции являются чистыми Bash! никаких внешних утилит и никаких подоболочек! они безопасны по любым смешным символам, которые у вас могут быть (пробелы, символы новой строки, символы глобуса и т.д.).
Ответ 4
Если вам не нужно обрабатывать специальные символы оболочки в элементах массива:
array=(a c b f 3 5)
sorted=($(printf '%s\n' "${array[@]}"|sort))
В bash вам понадобится внешняя программа сортировки.
С zsh не требуются внешние программы и легко обрабатываются специальные символы оболочки:
% array=('a a' c b f 3 5); printf '%s\n' "${(o)array[@]}"
3
5
a a
b
c
f
ksh имеет set -s
для сортировки ASCIIbetically.
Ответ 5
В 3-часовой поездке на поезде из Мюнхена во Франкфурт, с которой мне трудно было добраться, потому что Октоберфест начинается завтра), я думал о своем первом посте. Использование глобального массива - гораздо лучшая идея для общей функции сортировки. Следующая функция обрабатывает суровые строки (новые строки, пробелы и т.д.):
declare BSORT=()
function bubble_sort()
{ #
# @param [ARGUMENTS]...
#
# Sort all positional arguments and store them in global array BSORT.
# Without arguments sort this array. Return the number of iterations made.
#
# Bubble sorting lets the heaviest element sink to the bottom.
#
(($# > 0)) && BSORT=("[email protected]")
local j=0 ubound=$((${#BSORT[*]} - 1))
while ((ubound > 0))
do
local i=0
while ((i < ubound))
do
if [ "${BSORT[$i]}" \> "${BSORT[$((i + 1))]}" ]
then
local t="${BSORT[$i]}"
BSORT[$i]="${BSORT[$((i + 1))]}"
BSORT[$((i + 1))]="$t"
fi
((++i))
done
((++j))
((--ubound))
done
echo $j
}
bubble_sort a c b 'z y' 3 5
echo ${BSORT[@]}
Отпечатки:
3 5 a b c z y
Тот же вывод создается из
BSORT=(a c b 'z y' 3 5)
bubble_sort
echo ${BSORT[@]}
Обратите внимание, что, вероятно, Bash внутренне использует интеллектуальные указатели, поэтому операция свопинга может быть дешевой (хотя я сомневаюсь). Однако bubble_sort
демонстрирует, что более продвинутые функции, такие как merge_sort
, также доступны для языка оболочки.
Ответ 6
tl; dr:
Сортировка массива a_in
и сохранение результата в a_out
(элементы не должны иметь встроенных строк новой строки [1]
):
Bash v4 +:
readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort)
Bash v3:
IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort)
Преимущества для антик-решения:
-
Вам не нужно беспокоиться о случайном чередовании (случайная интерпретация элементов массива как шаблонов имен файлов), поэтому для отключения перетаскивания (set -f
и set +f
для восстановления позже) не требуется дополнительная команда.
-
Вам не нужно беспокоиться о сбросе IFS
с помощью unset IFS
. [2]
Дополнительное чтение: пояснение и пример кода
Вышеупомянутый сочетает Bash с внешней утилитой sort
для решения, которое работает с произвольными однострочными элементами и лексической или числовой сортировкой (необязательно по полю):
-
Производительность. Для около 20 элементов или более это будет быстрее, чем чистое решение Bash - значительно и все чаще, когда вы получаете около 100 элементов.
(Точные пороговые значения будут зависеть от вашего конкретного входа, машины и платформы.)
- Причина заключается в том, что он избегает Bash циклов.
-
printf '%s\n' "${a_in[@]}" | sort
выполняет сортировку (лексически, по умолчанию - см. sort
POSIX spec):
-
"${a_in[@]}"
безопасно расширяет элементы массива a_in
как отдельные аргументы, независимо от того, что они содержат (включая пробелы).
-
printf '%s\n'
затем печатает каждый аргумент, т.е. каждый элемент массива - в своей собственной строке, как есть.
-
Обратите внимание на использование подстановки процесса (<(...)
), чтобы предоставить отсортированный результат как вход в read
/readarray
(путем перенаправления на stdin, <
), поскольку read
/readarray
должен выполняться в текущей оболочке (не должен запускаться в подоболочке) чтобы выходная переменная a_out
была видимой для текущей оболочки (для того, чтобы переменная оставалась определенной в остальной части script).
-
Чтение sort
вывода в переменную массива:
-
Bash v4 +: readarray -t a_out
считывает отдельные строки, выводимые sort
в элементы переменной массива a_out
, не включая конечный \n
в каждом элементе (-t
).
-
Bash v3: readarray
не существует, поэтому read
необходимо использовать:
IFS=$'\n' read -d '' -r -a a_out
сообщает read
читать в массив (-a
) переменную a_out
, считывая весь ввод, по строкам (-d ''
), но разбивая его на элементы массива на строки новой строки (IFS=$'\n'
. $'\n'
, который создает литеральную новую строку (LF), представляет собой так называемую строку с цитированием ANSI C).
(-r
, параметр, который должен использоваться всегда с read
, отключает неожиданную обработку символов \
.)
Аннотированный пример кода:
#!/usr/bin/env bash
# Define input array `a_in`:
# Note the element with embedded whitespace ('a c')and the element that looks like
# a glob ('*'), chosen to demonstrate that elements with line-internal whitespace
# and glob-like contents are correctly preserved.
a_in=( 'a c' b f 5 '*' 10 )
# Sort and store output in array `a_out`
# Saving back into `a_in` is also an option.
IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort)
# Bash 4.x: use the simpler `readarray -t`:
# readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort)
# Print sorted output array, line by line:
printf '%s\n' "${a_out[@]}"
Из-за использования sort
без параметров это дает лексическую сортировку (цифры сортируются перед буквами, а цифровые последовательности обрабатываются лексически, а не как числа):
*
10
5
a c
b
f
Если вам нужна цифровая сортировка по 1-ому полю, вы должны использовать sort -k1,1n
вместо sort
, который дает (сортировка не чисел для чисел и чисел сортируется правильно):
*
a c
b
f
5
10
[1] Для обработки элементов со встроенными новыми строками используйте следующий вариант (Bash v4 +, с GNU sort
):
readarray -d '' -t a_out < <(printf '%s\0' "${a_in[@]}" | sort -z)
.
Полезный ответ Michał Górny содержит решение Bash v3.
[2] Пока IFS
установлен в варианте Bash v3, это изменение относится к команде.
Напротив, то, что следует IFS=$'\n'
в ответе на antak, - это назначение, а не команда, и в этом случае изменение IFS
является глобальным.
Ответ 7
Другое решение, использующее внешний sort
, и справляется с любыми специальными символами (кроме NULs:)). Должен работать с bash -3.2 и GNU или BSD sort
(к сожалению, POSIX не включает -z
).
local e new_array=()
while IFS= read -r -d '' e; do
new_array+=( "${e}" )
done < <(printf "%s\0" "${array[@]}" | LC_ALL=C sort -z)
Сначала посмотрите на перенаправление ввода в конце. Мы используем встроенный printf
для записи элементов массива с нулевым завершением. Цитирование гарантирует, что элементы массива передаются как есть, а спецификация оболочки printf
заставляет его повторно использовать последнюю часть строки формата для каждого оставшегося параметра. То есть, это эквивалентно чем-то вроде:
for e in "${array[@]}"; do
printf "%s\0" "${e}"
done
Список элементов с нулевым завершением затем передается в sort
. Опция -z
заставляет ее считывать элементы с нулевым символом, сортировать их и выводить также нуль-конец. Если вам нужно получить только уникальные элементы, вы можете передать -u
, поскольку он более переносимый, чем uniq -z
. LC_ALL=C
обеспечивает стабильный порядок сортировки независимо от локали - иногда полезен для скриптов. Если вы хотите, чтобы sort
уважал языковой стандарт, удалите его.
Конструкция <()
получает дескриптор для чтения из порожденного конвейера, а <
перенаправляет на него стандартный ввод цикла while
. Если вам нужен доступ к стандартным входам внутри канала, вы можете использовать другой дескриптор - упражнение для читателя:).
Теперь вернемся к началу. Встроенный модуль read
считывает выходные данные из перенаправленного stdin. Установка пустого IFS
отключает разделение слов, которое здесь не нужно - в результате read
считывает всю "строку" ввода в одну предоставленную переменную. Параметр -r
отключает также обработку нежелательной обработки. Наконец, -d ''
устанавливает разделитель строки в NUL, то есть сообщает read
читать строки с нулевым завершением.
В результате цикл выполняется один раз для каждого последующего элемента массива с нулевым завершением, причем значение сохраняется в e
. Пример просто помещает элементы в другой массив, но вы можете их обработать напрямую:).
Конечно, это только один из многих способов достижения одной и той же цели. Как я вижу, это проще, чем реализовать полный алгоритм сортировки в bash, а в некоторых случаях он будет быстрее. Он обрабатывает все специальные символы, включая новые строки, и должен работать на большинстве общих систем. Самое главное, это может научить вас чему-то новому и удивительному в отношении bash:).
Ответ 8
попробуйте следующее:
echo ${array[@]} | awk 'BEGIN{RS=" ";} {print $1}' | sort
Выход будет:
3
5
a
b
c
f
Проблема решена.
Ответ 9
мин сортировать:
#!/bin/bash
array=(.....)
index_of_element1=0
while (( ${index_of_element1} < ${#array[@]} )); do
element_1="${array[${index_of_element1}]}"
index_of_element2=$((index_of_element1 + 1))
index_of_min=${index_of_element1}
min_element="${element_1}"
for element_2 in "${array[@]:$((index_of_element1 + 1))}"; do
min_element="'printf "%s\n%s" "${min_element}" "${element_2}" | sort | head -n+1'"
if [[ "${min_element}" == "${element_2}" ]]; then
index_of_min=${index_of_element2}
fi
let index_of_element2++
done
array[${index_of_element1}]="${min_element}"
array[${index_of_min}]="${element_1}"
let index_of_element1++
done
Ответ 10
Если вы можете вычислить уникальное целое число для каждого элемента массива, например:
tab='0123456789abcdefghijklmnopqrstuvwxyz'
# build the reversed ordinal map
for ((i = 0; i < ${#tab}; i++)); do
declare -g ord_${tab:i:1}=$i
done
function sexy_int() {
local sum=0
local i ch ref
for ((i = 0; i < ${#1}; i++)); do
ch="${1:i:1}"
ref="ord_$ch"
(( sum += ${!ref} ))
done
return $sum
}
sexy_int hello
echo "hello -> $?"
sexy_int world
echo "world -> $?"
тогда вы можете использовать эти целые числа как индексы массивов, потому что Bash всегда использует разреженный массив, поэтому не нужно беспокоиться о неиспользуемых индексах:
array=(a c b f 3 5)
for el in "${array[@]}"; do
sexy_int "$el"
sorted[$?]="$el"
done
echo "${sorted[@]}"
- Pros. Быстро.
- Cons. Дублированные элементы объединяются, и невозможно сопоставить содержимое с 32-битными уникальными целыми числами.
Ответ 11
array=(a c b f 3 5)
new_array=($(echo "${array[@]}" | sed 's/ /\n/g' | sort))
echo ${new_array[@]}
Содержание эха new_array будет:
3 5 a b c f
Ответ 12
Я не уверен, что вам понадобится внешняя программа сортировки в Bash.
Вот моя реализация для простого алгоритма сортировки пузырьков.
function bubble_sort()
{ #
# Sorts all positional arguments and echoes them back.
#
# Bubble sorting lets the heaviest (longest) element sink to the bottom.
#
local array=([email protected]) max=$(($# - 1))
while ((max > 0))
do
local i=0
while ((i < max))
do
if [ ${array[$i]} \> ${array[$((i + 1))]} ]
then
local t=${array[$i]}
array[$i]=${array[$((i + 1))]}
array[$((i + 1))]=$t
fi
((i += 1))
done
((max -= 1))
done
echo ${array[@]}
}
array=(a c b f 3 5)
echo " input: ${array[@]}"
echo "output: $(bubble_sort ${array[@]})"
Это напечатает:
input: a c b f 3 5
output: 3 5 a b c f
Ответ 13
a=(e b 'c d')
shuf -e "${a[@]}" | sort >/tmp/f
mapfile -t g </tmp/f
Ответ 14
Существует обходной путь для обычной проблемы пространств и строк:
Используйте символ, который не находится в исходном массиве (например, $'\1'
или $'\4'
или аналогичный).
Эта функция выполняет задание:
# Sort an Array may have spaces or newlines with a workaround (wa=$'\4')
sortarray(){ local wa=$'\4' IFS=''
if [[ $* =~ [$wa] ]]; then
echo "$0: error: array contains the workaround char" >&2
exit 1
fi
set -f; local IFS=$'\n' x nl=$'\n'
set -- $(printf '%s\n' "${@//$nl/$wa}" | sort -n)
for x
do sorted+=("${x//$wa/$nl}")
done
}
Это отсортирует массив:
$ array=( a b 'c d' $'e\nf' $'g\1h')
$ sortarray "${array[@]}"
$ printf '<%s>\n' "${sorted[@]}"
<a>
<b>
<c d>
<e
f>
<gh>
Это будет жаловаться на то, что исходный массив содержит обходной символ:
$ array=( a b 'c d' $'e\nf' $'g\4h')
$ sortarray "${array[@]}"
./script: error: array contains the workaround char
Описание
- Мы установили две локальные переменные
wa
(обходной путь char) и нулевой IFS
- Затем (с ifs null) мы проверяем, что весь массив
$*
.
- Не содержит никаких проблем char
[[ $* =~ [$wa] ]]
.
- Если это так, поднимите сообщение и сообщите об ошибке:
exit 1
- Избегайте расширений имен файлов:
set -f
- Задайте новое значение IFS (
IFS=$'\n'
) переменной цикла x
и новой строки var (nl=$'\n'
).
- Мы печатаем все значения полученных аргументов (входной массив
[email protected]
).
- но мы заменяем любую новую строку обходным способом char
"${@//$nl/$wa}"
.
- отправьте эти значения для сортировки
sort -n
.
- и поместите все отсортированные значения в позиционные аргументы
set --
.
- Затем мы назначаем каждый аргумент один за другим (для сохранения новых строк).
- в цикле
for x
- в новый массив:
sorted+=(…)
- внутри кавычек для сохранения любой существующей новой строки.
- восстановление обходного пути к новой строке
"${x//$wa/$nl}"
.
- сделано
Ответ 15
sorted=($(echo ${array[@]} | tr " " "\n" | sort))
В духе bash/linux я буду использовать лучший инструмент командной строки для каждого шага. sort
выполняет основное задание, но требует ввода, разделенного новой строкой, а не пространства, поэтому простой проложенный конвейер просто делает:
Содержимое массива Echo → заменить пространство с помощью новой строки → sort
$()
должен отражать результат
($())
заключается в том, чтобы поместить "эхо-результат" в массив
Примечание: как @sorontar упоминается в comment к другому вопросу:
В отсортированной = ($ (...)) части используется оператор split и glob. Вы должны отключить glob: set -f или set -o noglob или shopt -op noglob, или элемент массива вроде * будет расширен до списка файлов.