64

How can I reverse the order in which I perform a for loop for a defined array

To iterate through the array I am doing this:

$ export MYARRAY=("one" "two" "three" "four")
$ for i in ${MYARRAY[@]}; do echo $i;done
one
two
three
four

Is there a function where I can reverse the order of the array?

One thought I had is to generate a sequence of inverted indexes and call the elements by using this reversed index but maybe there is a quicker alternative, or at least easier to read.

1
  • 2
    Dear late reader: several of the answers will fail if the array elements contain special characters. In particular most things tac will mess up if elements contain a newline character. So before picking an answer for your problem, be sure what you're up to. Commented Apr 17 at 16:10

12 Answers 12

82

You can use the C-style for loop:

for (( idx=${#MYARRAY[@]}-1 ; idx>=0 ; idx-- )) ; do
    echo "${MYARRAY[idx]}"
done

For an array with "holes", the number of elements ${#arr[@]} doesn't correspond to the index of the last element. You can create another array of indices and walk it backwards in the same way:

#! /bin/bash
arr[2]=a
arr[7]=b

echo ${#arr[@]}  # only 2!!

indices=( ${!arr[@]} )
for ((i=${#indices[@]} - 1; i >= 0; i--)) ; do
    echo "${arr[indices[i]]}"
done
Sign up to request clarification or add additional context in comments.

11 Comments

The echo is not 100% foolproof but this is the best method to loop through an array in reverse order! +1
in zsh, at least, unless you change idx=${#MYARRAY[@]}-1 to idx=${#MYARRAY[@]} the last element in the array will always be removed.
@AlexejMagura: The question is tagged bash. I tested the code in bash. Without -1, an empty line is printed at the beginning. ${#array[@]} returns the number of elements, which is not the same as the index of the last element.
@gniourf_gniourf - I disagree. I think the one I demonstrate is foolproof.
@gniourf_gniourf It works for bash. And zsh. If the zsh_version environment variable is set it behaves as if its a zsh array, otherwise its a bash array. Also, there would be no problems with echo, and it should be much faster as it doesnt have to loop. Whats more, it actually sets a new current environment array in the process.
|
32

You can use tac, which is an opposite of cat in sense that it reverses the lines.

MYARRAY=("one" "two" "three" "four")
for item in "$MYARRAY"; do
   echo "$item"; 
done | tac

# four
# three
# two
# one

6 Comments

I don't advise this methods for several reasons: Useless use of a forking through tac and missing quotes!
Also note that tac is usually only available on Linux boxes.
@D.Shawley This is bash.
@jwg looks like a shell script snippet using a command that is usually only available on Linux boxes. IOW, not just bash but bash in a GNU Linux environment.
I can confirm that tac is not available on OSX.
|
6
_arr+=( '"${_arrev} is an actual "${array[@]}"' )  ⏎
_arr+=( '"${_arrev} is created as a result"' )
_arr+=( '"of reversing the key order in"' )
_arr+=( '"this "${_arr}. It handles zsh and"' )
_arr+=( '"bash arrays intelligently by tracking"' )
_arr+=( '"shell "$ENV." quotes=fine ( i hope ) "' )

. <<REVERSE /dev/stdin                    ⏎
    _arrev=( $(: $((l=${#_arr[@]}${ZSH_VERSION++1})) ; printf '"${_arr[$(('$l'-%d))]}" ' `seq 1 $l`) )
REVERSE

echo ; printf %s\\n ${_arrev}

"shell "$ENV." quotes=fine ( i hope ) "
"bash arrays intelligently by tracking"
"this "${_arr}. It handles zsh and"
"of reversing the key order in"
"${_arrev} is created as a result"
"${_arrev} is an actual "${array[@]}"

This should handle any possible array, I think.

If you're interested in what's going on up there, I suggest you have a look here first. Then maybe here, definitely here, and, if you've got the time, here and here.

In all of those answers I discuss different aspects of the here-document (and in many others) which you can use to your advantage. For instance I discuss twice-evaluating variables, which is done above, and in one declare a function that globally declares another function named "_$1" in just 5 or 6 lines - most of which were _$1() { func body ; }. It's pretty handy if you use it correctly.

Regarding the auto-switch between bash/zsh, well that's something else, but very simple as well. See here.

3 Comments

Interesting method. Please add some explanations. Also, the OP seemed to want to print the content of the array (even though it's not clearly stated); please also adapt your answer for only printing the array in reverse order.
@gniourf_gniourf I've linked to different answers in which I've already explained the fundamentals of what it's doing. Honestly, though, I don't even know how arrays work - I never use them. I know this works and can't fail thanks to printf and the shell doing the rest. Hope I helped.
@gniourf_gniourf Only printing? Easily done, but i dont know where it is you came up with it as a requirement. If you desire only to print it simply replace the declaration with a print statement. For example, instead of arrev=( ) use printf %s\ . Still, it seems much more practical to call on an actual builtin array as needed and then to unset or arrev=() the variable when you no longer need it.
4

How about this:

for i in `printf '%s\n' "${MYARRAY[@]}"|tac`; do echo $i; done

The output is:

four
three
two
one

Limitation: doesn't work if array contain newlines. As a workaround you may implement function:

reverse(){ reversed=();local i;for ((i=$#;i>0;i--)); do reversed+=("${!i}");done; }

and use it this way:

reverse "${MYARRAY[@]}" && for i in "${reversed[@]}"; do echo $i; done

4 Comments

That's a complicated way of writing printf '%s\n' "${MYARRAY[@]}"|tac.
It's a way of iterating array in reverse order. You can put anything you want in place of echo $i.
Ah, okay. This breaks in the - admittedly contrived - case of a newline within one array element: MYARRAY=($'part1\npart2' second) as tac separates by newlines, and not actual array elements.
Thanks, @Benjamin. I fixed the answer.
3

Well, here follows another solution but one-liner...

$ export MYARRAY=("one" "two" "three" "four")
$ for i in ${!MYARRAY[@]}; do echo ${MYARRAY[-1 - $i]} ; done
four
three
two
one
$

That's because you may refer to array items with negatives, so that the very last has -1 index, the second last -2 and so on.

For an array with "holes", by using the array of indexes approach, follows a not so one-liner solution:

$ MYARRAY=("one" "two" "three" "four")
$ MYARRAY[45]="forty-five"
$ MYARRAY[83]="eighty-three"
$ MYARRAY+=("a" "b" "c")
$ idxs=( ${!MYARRAY[@]} )
$ for i in ${!idxs[@]} ; do echo ${MYARRAY[${idxs[$((-1 - ${i}))]}]} ; done
c
b
a
eighty-three
forty-five
four
three
two
one
$ 

Comments

2

Simple as a string:

% unset c; a="1 2 3 4 5"; for b in $a; do c="$b $c"; done; echo $c

5 4 3 2 1

You sure you want array syntax??:

 % unset c; declare -a c; a=(1 2 3 4 5); i=0; for b in ${a[*]}; \
    do c[$((${#a[@]}-$i))]=$b; i=$(($i+1)); done; echo ${c[*]}

5 4 3 2 1

Comments

2

you can also consider using seq

MYARRAY=("one" "two" "three" "four")

for i in $(seq $((${#MYARRAY[@]} - 1)) -1 0); do
    echo ${MYARRAY[$i]}
done

in freebsd you can omit -1 increment parameter:

for i in $(seq $((${#MYARRAY[@]} - 1)) 0); do
    echo ${MYARRAY[$i]}
done

1 Comment

instead of seq, you can also get the indices in reverse order using tac -s' ' <<<${!MYARRAY[@]} or, if the array has less than 10 entries, using rev <<<${!MYARRAY[@]}
1

If you are talking about a sequential numerical array (say to delete bash history) you can just list this in reverse order for instance:

for i in {16..10}; do history -d $i; done

I realize that this is kind of off topic and I apologize for that however I figured that it was probably worth mentioning.

Comments

1

I'd advise limiting one-liner usage for such things and instead write a function that's sourced into your shell or script. Here's an example for newer versions of Bash where it's possible to pass multiple arrays by reference to a function...

reverse_array(){
  local -n _source_array_ref="${1}"
  local -n _destination_array_ref="${2}"

  for ((_index=${#_source_array_ref[@]}-1; _index>=0; _index--)); do
    _destination_array_ref+=("${_source_array_ref[$_index]}")
  done
}


_list=(spam ham space)
_new_list=()

reverse_array '_list' '_new_list'

printf '%s\n' "${_new_list[@]}"
#> space
#> ham
#> spam

... however, with this technique do be aware that it makes a function that's impure (has side-effects), meaning that debugging _list or _new_list can become difficult when in a Bash scripting mindset... also current examples don't take into account that one could re-run reverse_array and end up with _new_list appended to multiple times; which may or may not be desired.

Comments

1

Here's an easy and intuitive way to reverse an array.

It does it the same way as you would in languages that have car/cdr style list data structures. Build a new list with the elements being pushed in front of each other.

#!/usr/bin/env bash

pets=( ant bat cat dog )
pet_rev=()

for pet in ${pets[@]}
do
  # this puts the next pet in front of the ones already in pet_rev 
  pet_rev=( $pet ${pet_rev[@]} )
done

echo "${pet_rev[@]}"

The output is of course; dog cat bat ant

This could be put in a single line with the reversed array assigned back to the original one. Though doing so would be a little hard to read.

Comments

0

Here is another approach that does not involve any loop.

Get the full array declaration with declare -p, revert the indices and declare a new array. Basically, we want to transform

declare -a OLD=([0]="aa" [1]="bb" [2]="cc")

into

declare -a NEW=([2-0]="aa" [2-1]="bb" [2-2]="cc")

That works because the array indices are subject to ARITHMETIC EVALUATION.

Here is a simple implementation that assumes that no array element contains the character '['.

A=("aa" "bb" "cc" "dd")

TMP=( "${A[@]}" )           # get a copy of A with contiguous indices
len="${#TMP[@]}"            # get array length.
decl=$(declare -p TMP)      # get full declaration of TMP
decl=${decl#*=}             # remove prefix up to first '=' 
decl="${decl//[/[$len-1-}"  # insert len-1 in front of all indices 
declare -a B=$decl          # and declare our new inverted array  

# B should now contain "dd" "cc" "bb" "aa"

Comments

0

Reverse Loop over Sparse Bash Array (Two Methods)

#!/usr/bin/env bash

# Declare a numerically indexed array with arbitrary indices
limits=([2]=10 [16]=20 [4]=26 [3]=39 [5]=48)

printf '%s\n' 'forward'

# Iterate in the default (usually ascending index) order
for v in "${limits[@]}"; do
  printf '%s\n' "$v"
done

printf '\n%s\n' 'reverse (as-is from Bash ordering)'

# Reverse loop using the order of indices as given by Bash (unsorted)
limits_idx=("${!limits[@]}")

# Iterate the indices in reverse order using a C-style loop
# (safe here because limits_idx is a contiguous indexes array)
for ((i=${#limits_idx[@]}; i--; i>=0)); do
  j=${limits_idx[i]}
  printf '%s\n' "${limits[j]}"
done

printf '\n%s\n' 'reverse (numerically sorted)'

# Get sorted indices in descending numeric order safely using mapfile
mapfile -t sorted_desc_idx < <(printf '%s\n' "${!limits[@]}" | sort -nr)

# Loop using sorted indices
for i in "${sorted_desc_idx[@]}"; do
  printf '%s\n' "${limits[i]}"
done

Explanations of Both Reverse Strategies

Method 1: Native Bash Reverse Loop

  • Uses "${!array[@]}" to extract indices into an intermediate array.

  • Loops over that array in reverse using a C-style loop.

Method 2: External Sorting with sort -nr

  • Uses printf | sort -nr to explicitly sort the indices in descending numeric order.

  • Then loops over them with a regular for.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.