pedrosaurio
pedrosaurio

Reputation: 4926

How to reverse array in bash onliner FOR loop?

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.

Upvotes: 63

Views: 49195

Answers (11)

Stef
Stef

Reputation: 11

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"

Upvotes: 0

jambalaio
jambalaio

Reputation: 21

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
$ 

Upvotes: 2

Alan Carlyle
Alan Carlyle

Reputation: 1143

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.

Upvotes: 1

M. Modugno
M. Modugno

Reputation: 791

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

Upvotes: 2

S0AndS0
S0AndS0

Reputation: 920

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.

Upvotes: 1

Johan Snowgoose
Johan Snowgoose

Reputation: 394

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

Upvotes: 2

Alek
Alek

Reputation: 893

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

Upvotes: 4

choroba
choroba

Reputation: 241988

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

Upvotes: 81

Robert
Robert

Reputation: 19

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.

Upvotes: 1

mikeserv
mikeserv

Reputation: 694

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

Upvotes: 6

bereal
bereal

Reputation: 34292

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

Upvotes: 31

Related Questions