31

I get the following output:

Pushkin - 100500 
Gogol - 23 
Dostoyevsky - 9999

Which is the result of the following script:

for k in "${!authors[@]}"
do
    echo $k ' - ' ${authors["$k"]}
done   

All I want is to get the output like this:

Pushkin - 100500 
Dostoyevsky - 9999
Gogol - 23

which means that the keys in associative array should be sorted by value. Is there an easy method to do so?

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Graf
  • 1,437
  • 3
  • 17
  • 27

6 Answers6

31

You can easily sort your output, in descending numerical order of the 3rd field:

for k in "${!authors[@]}"
do
    echo $k ' - ' ${authors["$k"]}
done |
sort -rn -k3

See sort(1) for more about the sort command. This just sorts output lines; I don't know of any way to sort an array directly in bash.

I also can't see how the above can give you names ("Pushkin" et al.) as array keys. In bash, array keys are always integers.

Andrew Schulman
  • 3,395
  • 1
  • 21
  • 23
9

Alternatively you can sort the indexes and use the sorted list of indexes to loop through the array:

authors_indexes=( ${!authors[@]} )
IFS=$'\n' authors_sorted=( $(echo -e "${authors_indexes[@]/%/\n}" | sed -r -e 's/^ *//' -e '/^$/d' | sort) )

for k in "${authors_sorted[@]}"; do
  echo $k ' - ' ${authors["$k"]}
done 
Werner Lehmann
  • 911
  • 1
  • 8
  • 14
  • 4
    Boo, hiss re: `echo -e`. Consider `printf '%s\n' "${authors_indexes[@]}"` to expand an array into multiple lines in a way that doesn't depend on violations of POSIX sh (not just extensions, but actual violations: Any argument other than `-n` is required to print its text, unless it contains backslashes or `-n` is present, in which case behavior is entirely implementation-defined). Or `printf '%s\0' "${array[@]}"` to expand the array as NUL-delimit strings, thus in a manner that works even if you have literal newlines in your keys (which *are* legal). – Charles Duffy Nov 09 '16 at 23:34
  • ...see http://pubs.opengroup.org/onlinepubs/009695399/utilities/echo.html, particularly the APPLICATION USAGE section. – Charles Duffy Nov 09 '16 at 23:34
  • IFS is changed as there is no command on the line, only assignments. also taking Charles comment and removing the need for a indexes assignment, then the corrected solution is... `oIFS="$IFS" IFS=$'\n' authors_sorted=( $( printf '%s\n' "${!authors[@]}" | sort ) ) IFS="$oIFS"` – anthony Sep 28 '22 at 03:13
5

Extending the answer from @AndrewSchulman, using -rn as a global sort option reverses all columns. In this example, authors with the same associative array value will be output by reverse order of name.

For example

declare -A authors
authors=( [Pushkin]=10050 [Gogol]=23 [Dostoyevsky]=9999 [Tolstoy]=23 )

for k in "${!authors[@]}"
do
  echo $k ' - ' ${authors["$k"]}
done | sort -rn -k3

will output

Pushkin  -  10050
Dostoyevsky  -  9999
Tolstoy  -  23
Gogol  -  23
Options for sorting specific columns can be provided after the column specifier. i.e. sort -k3rn

Note that keys can be specified as spans. Here -k3 happens to be fine because it is the final span, but to use only column 3 explicitly (in case further columns were added), it should be specified as -k3,3, Similarly to sort by column three in descending order, and then column one in ascending order (which is probably what is desired in this example):

declare -A authors
authors=( [Pushkin]=10050 [Gogol]=23 [Dostoyevsky]=9999 [Tolstoy]=23 )
for k in "${!authors[@]}"
do
  echo $k ' - ' ${authors["$k"]}
done | sort -k3,3rn -k1,1

will output

Pushkin  -  10050
Dostoyevsky  -  9999
Gogol  -  23
Tolstoy  -  23
WaffleSouffle
  • 3,293
  • 2
  • 28
  • 27
5

The best way to sort a bash associative array by VALUE is to NOT sort it.

Instead, get the list of VALUE:::KEYS, sort that list into a new KEY LIST, and iterate through the list.

declare -A ADDR
ADDR[192.168.1.3]="host3"
ADDR[192.168.1.1]="host1"
ADDR[192.168.1.2]="host2"

KEYS=$(
for KEY in ${!ADDR[@]}; do
  echo "${ADDR[$KEY]}:::$KEY"
done | sort | awk -F::: '{print $2}'
)

for KEY in $KEYS; do
  VAL=${ADDR[$KEY]}
  echo "KEY=[$KEY] VAL=[$VAL]"
done

output:
KEY=[192.168.1.1] VAL=[host1]
KEY=[192.168.1.2] VAL=[host2]
KEY=[192.168.1.3] VAL=[host3]
mz4wheeler
  • 51
  • 1
  • 1
4

Do something with unsorted keys:

for key in ${!Map[@]}; do
   echo $key
done

Do something with sorted keys:

for key in $(for x in ${!Map[@]}; do echo $x; done | sort); do
   echo $key
done

Store sorted keys as array:

Keys=($(for x in ${!Map[@]}; do echo $x; done | sort))
Andreas Spindler
  • 7,568
  • 4
  • 43
  • 34
  • To sort on the value but also print out a concatenated keyvalue, using `echo` with a space in an inline nested loop can be problematic; My solution was to use printf and not use a space: `for kvp in $(for key in ${!Map[@]}; do printf '%s:%s\n' "$key" "${Map[$key]}"; done | sort -t: -rn -k2); do echo "$kvp"; done` – mdisibio Apr 26 '23 at 21:33
0

If you can assume the value is always a number (no spaces), but want to allow for the possibility of spaces in the key:

for k in "${!authors[@]}"; do 
  echo "${authors["$k"]} ${k}"
done | sort -rn | while read number author; do 
  echo "${author} - ${number}"
done

Example:

$ declare -A authors
$ authors=(['Shakespeare']=1 ['Kant']=2 ['Von Neumann']=3 ['Von Auersperg']=4)
$ for k in "${!authors[@]}"; do echo "${authors["$k"]} ${k}"; done | sort -rn | while read number author; do echo "${author} - ${number}"; done
Von Auersperg - 4
Von Neumann - 3
Kant - 2
Shakespeare - 1
$ 

The chosen answer seems to work if there are no spaces in the keys, but fails if there are:

$ declare -A authors
$ authors=(['Shakespeare']=1 ['Kant']=2 ['Von Neumann']=3 ['Von Auersperg']=4)
$ for k in "${!authors[@]}"; do echo $k ' - ' ${authors["$k"]}; done | sort -rn -k 3
Kant  -  2
Shakespeare  -  1
Von Neumann  -  3
Von Auersperg  -  4
$ 
abugher
  • 11
  • 3