How to sort an array in Bash
Original response:
array=(a c b "f f" 3 5)
readarray -t sorted < <(for a in "${array[@]}"; do echo "$a"; done | sort)
output:
$ for a in "${sorted[@]}"; do echo "$a"; done
3
5
a
b
c
f f
Note this version copes with values that contains special characters or whitespace (except newlines)
Note readarray is supported in bash 4+.
Edit Based on the suggestion by @Dimitre I had updated it to:
readarray -t sorted < <(printf '%s\0' "${array[@]}" | sort -z | xargs -0n1)
which has the benefit of even understanding sorting elements with newline characters embedded correctly. Unfortunately, as correctly signaled by @ruakh this didn't mean the the result of readarray
would be correct, because readarray
has no option to use NUL
instead of regular newlines as line-separators.
You don't really need all that much code:
IFS=$'\n' sorted=($(sort <<<"${array[*]}"))
unset IFS
Supports whitespace in elements (as long as it's not a newline), and works in Bash 3.x.
e.g.:
$ array=("a c" b f "3 5")
$ IFS=$'\n' sorted=($(sort <<<"${array[*]}")); unset IFS
$ printf "[%s]\n" "${sorted[@]}"
[3 5]
[a c]
[b]
[f]
Note: @sorontar has pointed out that care is required if elements contain wildcards such as *
or ?
:
The sorted=($(...)) part is using the "split and glob" operator. You should turn glob off:
set -f
orset -o noglob
orshopt -op noglob
or an element of the array like*
will be expanded to a list of files.
What's happening:
The result is a culmination six things that happen in this order:
IFS=$'\n'
"${array[*]}"
<<<
sort
sorted=($(...))
unset IFS
First, the IFS=$'\n'
This is an important part of our operation that affects the outcome of 2 and 5 in the following way:
Given:
"${array[*]}"
expands to every element delimited by the first character ofIFS
sorted=()
creates elements by splitting on every character ofIFS
IFS=$'\n'
sets things up so that elements are expanded using a new line as the delimiter, and then later created in a way that each line becomes an element. (i.e. Splitting on a new line.)
Delimiting by a new line is important because that's how sort
operates (sorting per line). Splitting by only a new line is not-as-important, but is needed preserve elements that contain spaces or tabs.
The default value of IFS
is a space, a tab, followed by a new line, and would be unfit for our operation.
Next, the sort <<<"${array[*]}"
part
<<<
, called here strings, takes the expansion of "${array[*]}"
, as explained above, and feeds it into the standard input of sort
.
With our example, sort
is fed this following string:
a c
b
f
3 5
Since sort
sorts, it produces:
3 5
a c
b
f
Next, the sorted=($(...))
part
The $(...)
part, called command substitution, causes its content (sort <<<"${array[*]}
) to run as a normal command, while taking the resulting standard output as the literal that goes where ever $(...)
was.
In our example, this produces something similar to simply writing:
sorted=(3 5
a c
b
f
)
sorted
then becomes an array that's created by splitting this literal on every new line.
Finally, the unset IFS
This resets the value of IFS
to the default value, and is just good practice.
It's to ensure we don't cause trouble with anything that relies on IFS
later in our script. (Otherwise we'd need to remember that we've switched things around--something that might be impractical for complex scripts.)