How to create a function that can sort an array in bash?
bash
I don't think bash
has any builtin support for that yet. Options would be to implement a sort algorithm by hand or to invoke sort
to do the sorting.
If we consider that array elements can contain any byte value but 0 in bash
, to do it reliably, we'd need to pass the list of elements NUL-delimited and use the -z
option to sort
(non-standard but available in GNU sort or FreeBSD sort).
bash-4.4 (released September 2016) makes it easier as it introduced a -d
option to its readarray
builtin to specify the delimiter.
To sort array a
into array b
:
readarray -td '' b < <(printf '%s\0' "${a[@]}" | sort -z)
would sort the array reliably. Use the -n
, -r
options to sort
to sort numerically or in reverse (or any sort criteria supported by sort
).
To implement your sortarray
function (sorts all the arrays passed by-name as arguments):
sortarray() for array do
eval '((${#'"$array"'[@]} <= 1))' || readarray -td '' "$array" < <(
eval "printf '%s\0' \"\${$array[@]}\" | sort -z")
done
With earlier versions of bash
, you can use read -d
in a loop to achieve the same:
b=()
while IFS= read -rd '' item; do b+=("$item"); done < <(
printf '%s\0' "${a[@]}" | sort -z)
For the sortarray
function:
sortarray() for array do eval '
tmp=()
while IFS= read -rd "" item; do tmp+=("$item"); done < <(
printf "%s\0" "${'"$array"'[@]}" | sort -z)
'"$array"'=("${tmp[@]}")'
done
zsh
Zsh has builtin support to sort arrays.
you can use the o
parameter expansion flag to sort lexically (O
for reverse order). You can add the n
flag to sort numerically:
$ a=('' 12 2 d é f $'a\nb')
$ printf '<%s>\n' "${(@o)a}"
<>
<12>
<2>
<a
b>
<d>
<é>
<f>
$ printf '<%s>\n' "${(@no)a}"
<>
<2>
<12>
<a
b>
<d>
<é>
<f>
In locales that don't already sort case-independently, you can also add the i
flag for that.
To assign to an array:
b=("${(@o)a}")
So a sortarray
function would be like:
sortarray() for array do eval "$array=(\"\${(@o)$array}\")"; done
AT&T ksh (ksh88 or ksh93, both of which can be found as sh on some systems)
set -s -- "${a[@]}"
b=("$@")
set -s
sorts the list of arguments and stores it in the positional parameters. The order is lexical.
A sortarray
function could be:
sortarray() for array do
eval 'set -s -- "${'"$array"'[@]}"; '"$array"'=("$@")'
done
Sort the easy way with sort
, tr
:
arr=($(for i in {0..9}; do echo $((RANDOM%100)); done))
echo ${arr[*]}| tr " " "\n" | sort -n | tr "\n" " "
Into a new array:
arr2=($(echo ${arr[*]}| tr " " "\n" | sort -n))
Without help by tr
/sort
, for example bubblesort:
#!/bin/bash
sort () {
for ((i=0; i <= $((${#arr[@]} - 2)); ++i))
do
for ((j=((i + 1)); j <= ((${#arr[@]} - 1)); ++j))
do
if [[ ${arr[i]} -gt ${arr[j]} ]]
then
# echo $i $j ${arr[i]} ${arr[j]}
tmp=${arr[i]}
arr[i]=${arr[j]}
arr[j]=$tmp
fi
done
done
}
# arr=(6 5 68 43 82 60 45 19 78 95)
arr=($(for i in {0..9}; do echo $((RANDOM%100)); done))
echo ${arr[@]}
sort ${arr[@]}
echo ${arr[@]}
For 20 numbers, bubblesort might be sufficient.
sortnums(){
local OLDPWD IFS=' /'
cd -- "$(mktemp -d)" || return
touch -- $*; ls -A
cd - >/dev/null &&
rm -rf -- "$OLDPWD"
}
Here's a slightly more complicated, and somewhat slower version which nevertheless does not squeeze duplicates and which sorts (reasonably sized) decimal numbers in numeric order - though (space-split) other strings are still sorted, string length is considered first. And to handle generic strings you'd almost definitely want to set the g=[0-9]
glob differently.
I'll be honest - I would (maybe) consider sorting a list of words or numbers like this, but it wouldn't otherwise occur to me to create a file with a name that wouldn't at least fit comfortably within a paragraph. And so it splits on spaces. Most often, that's the right thing to do. It is, however, also hampered by a sanity requirement of treating /
like a null. But it was just for fun, anyway, really.
fs_sort(){
local OLDPWD IFS=' /' opt="$-" g
cd -- "$(mktemp -d)" || return
set -C ### noClobber for testable >
for g in $* ### disallow any / reference
do until command >" $g" ### who needs dot glob?
do g=" $g" ### ' 1' lex== ' 1'
done; done 2>&1 ### -C is bitchy
g=[0-9] ### now glob the array
while set -f *\ $g && ### set it &&
<"$1" g+=? arr+=( $* ) ### <chk && (clean) it
do set +f; done 2>&1 ### clear it
set +fC "-${opts:--}" ### put stuff where we found it
cd - && rm -rf -- "$OLDPWD" ### don't leave our trash out
} >/dev/null ### cd - is chatty
If there's any lesson in this, maybe it should be what a filthy thing bash arrays are in the first place. If data was simply kept in files we'd never have any issue sort
ing it in the first place. Imagine how much easier it would be to maintain important shell state when necessary if your login shells just grabbed themselves a tiny chunk of tmpfs at startup, copied a ~/.sh
directory into it, and then copied back any files you may have marked sticky since at shutdown. All of your state names would sort as simply as set *
, and their contents would be accessible to any utility you wanted to call on them as is any other file.