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 sorting 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.

Tags:

Function

Bash