34

Is there a simple way to reverse an array?

#!/bin/bash

array=(1 2 3 4 5 6 7)

echo "${array[@]}"

so I would get: 7 6 5 4 3 2 1
instead of: 1 2 3 4 5 6 7

jimmij
  • 47,140
nath
  • 5,694

14 Answers14

30

Another unconventional approach:

#!/bin/bash

array=(1 2 3 4 5 6 7)

f() { array=("${BASH_ARGV[@]}"); }

shopt -s extdebug
f "${array[@]}"
shopt -u extdebug

echo "${array[@]}"

Output:

7 6 5 4 3 2 1

If extdebug is enabled, array BASH_ARGV contains in a function all positional parameters in reverse order.

Cyrus
  • 12,309
24

Unconventional approach (all not pure bash):

  • if all elements in an array are just one characters (like in the question) you can use rev:

     echo "${array[@]}" | rev
    
  • otherwise if none of the array elements contain a new line:

     printf '%s\n' "${array[@]}" | tac | tr '\n' ' '; echo
    
  • and if you can use zsh:

     echo ${(Oa)array}
    
jimmij
  • 47,140
  • just been looking up tac, as the opposite of cat quite good to remember, THANKS! – nath Dec 25 '17 at 02:17
  • 4
    Though i like the idea of rev, i need to mention that rev will not work correctly for numbers with two digits. For example an array element of 12 using rev will be printed as 21. Give it a try ;-) – George Vasiliou Dec 26 '17 at 21:46
  • @GeorgeVasiliou Yes, that will work only if all elements are one characters (numbers, letters, punctations, ...). That's why I gave also second, more general solution. – jimmij Dec 26 '17 at 22:04
  • The zsh example works for me, but outputs as a string, not an array – Scott Anderson Jul 24 '20 at 10:50
  • the zsh example is great! the first time it did not work for me because I did not understand it was (Oa) and not a 0. – baggiponte Oct 12 '22 at 06:42
  • This answer does not have a general bash solution. The first option cannot handle array elements with more than one character, the second option cannot handle array elements with new lines., and the third option is not a bash solution. – lmat - Reinstate Monica Jan 29 '24 at 14:17
19

I have answered the question as written, and this code reverses the array. (Printing the elements in reverse order without reversing the array is just a for loop counting down from the last element to zero.) This is a standard "swap first and last" algorithm.

array=(1 2 3 4 5 6 7)

min=0 max=$(( ${#array[@]} -1 ))

while [[ min -lt max ]] do # Swap current first and last elements x="${array[$min]}" array[$min]="${array[$max]}" array[$max]="$x"

# Move closer
(( min++, max-- ))

done

echo "${array[@]}"

It works for arrays of odd and even length.

Chris Davies
  • 116,213
  • 16
  • 160
  • 287
14

If you actually want the reverse in another array:

reverse() {
    # first argument is the array to reverse
    # second is the output array
    declare -n arr="$1" rev="$2"
    for i in "${arr[@]}"
    do
        rev=("$i" "${rev[@]}")
    done
}

Then:

array=(1 2 3 4)
reverse array foo
echo "${foo[@]}"

Gives:

4 3 2 1

This should correctly handle cases where an array index is missing, say you had array=([1]=1 [2]=2 [4]=4), in which case looping from 0 to the highest index may add additional, empty, elements.

muru
  • 72,889
  • Thanks for this one, it works pretty well, though for some reason shellcheck prints two warnings: array=(1 2 3 4) <-- SC2034: array appears unused. Verify it or export it. and for: echo "${foo[@]}" <-- SC2154: foo is referenced but not assigned. – nath Dec 26 '17 at 23:15
  • 1
    @nath they're indirectly used, that's what the declare line is for. – muru Dec 27 '17 at 00:37
  • Clever, but note that declare -n seems not to work in bash versions before 4.3. – G-Man Says 'Reinstate Monica' Sep 09 '18 at 21:33
  • 1
    For reference, it's called a "nameref". "A variable can be assigned the nameref attribute using the -n option to the declare or local builtin commands (see Bash Builtins) to create a nameref, or a reference to another variable." -- GNU Bash Manual – boweeb Apr 03 '20 at 14:22
12

To swap the array positions in place (even with sparse arrays)(since bash 3.0):

#!/bin/bash
# Declare an sparse array to test:
array=([5]=101 [6]=202 [10]=303 [11]=404 [20]=505 [21]=606 [40]=707)
echo "Initial array values"
declare -p array

swaparray(){ local temp; temp="${array[$1]}"
             array[$1]="${array[$2]}"
             array[$2]="$temp"
           }

ind=("${!array[@]}")                         # non-sparse array of indexes.

min=-1; max="${#ind[@]}"                     # limits to one before real limits.
while [[ min++ -lt max-- ]]                  # move closer on each loop.
do
    swaparray "${ind[min]}" "${ind[max]}"    # Exchange first and last
done

echo "Final Array swapped in place"
declare -p array
echo "Final Array values"
echo "${array[@]}"

On execution:

./script
Initial array values
declare -a array=([5]="101" [6]="202" [10]="303" [11]="404" [20]="505" [21]="606" [40]="707")

Final Array swapped in place
declare -a array=([5]="707" [6]="606" [10]="505" [11]="404" [20]="303" [21]="202" [40]="101")

Final Array values
707 606 505 404 303 202 101

For older bash, you need to use a loop (in bash (since 2.04)) and using $a to avoid the trailing space:

#!/bin/bash

array=(101 202 303 404 505 606 707)
last=${#array[@]}

a=""
for (( i=last-1 ; i>=0 ; i-- ));do
    printf '%s%s' "$a" "${array[i]}"
    a=" "
done
echo

For bash since 2.03:

#!/bin/bash
array=(101 202 303 404 505 606 707)
last=${#array[@]}

a="";i=0
while [[ last -ge $((i+=1)) ]]; do 
    printf '%s%s' "$a" "${array[ last-i ]}"
    a=" "
done
echo

Also (using the bitwise negation operator) (since bash 4.2+):

#!/bin/bash
array=(101 202 303 404 505 606 707)
last=${#array[@]}

a=""
for (( i=0 ; i<last ; i++ )); do 
    printf '%s%s' "$a" "${array[~i]}"
    a=" "
done
echo
6

Ugly, unmaintainable, but one-liner:

eval eval echo "'\"\${array['{$((${#array[@]}-1))..0}']}\"'"
user23013
  • 1,087
  • 1
  • 9
  • 18
  • Not simpler, but shorter: eval eval echo "'\"\${array[-'{1..${#array[@]}}']}\"'". –  Jul 03 '19 at 19:11
  • And even for sparse arrays: ind=("${!array[@]}");eval eval echo "'\"\${array[ind[-'{1..${#array[@]}}']]}\"'" –  Jul 03 '19 at 19:24
  • @Isaac But no longer one-liner and only ugly and unmaintainable for the sparse array version, unfortunately. (Should still be faster than pipes for small arrays, though.) – user23013 Jul 03 '19 at 19:32
  • Well, technically, it is a "one-liner"; not a one command, yes, but a "one liner" it is. I agree, yes, very ugly and a maintenance problem, but fun to play with. –  Jul 03 '19 at 19:46
  • Nice hack. You can even make this work safely for creating a reversed array: eval eval "'rev=(\$(printf \"%q \"' '\"\${array[-'{1..${#array[@]}}']}\"' '))'". This is for dense, non-empty arrays. Doing the same for sparse arrays should be possible too. – Socowi Aug 18 '21 at 10:41
6

To reverse an arbitrary array (which may contain any number of elements with any values):

With zsh:

array_reversed=("${(@Oa)array}")

With bash 4.4+, given that bash variables can't contain NUL bytes anyway, you can use GNU tac -s '' on the elements printed as NUL delimited records:

readarray -td '' array_reversed < <(
  ((${#array[@]})) && printf '%s\0' "${array[@]}" | tac -s '')

Note however that bash arrays were inspired from ksh arrays instead of csh/zsh arrays, and are more like associative arrays with keys limited to positive integers (so called sparse arrays), and that method doesn't preserve the keys of the arrays. For instance, for an array like:

array=( [3]=a [12]=b [42]=c )

You get

array_reversed=( [0]=c [1]=b [2]=a )

POSIXly, to reverse the one and only POSIX shell array ($@, made of $1, $2...) in place:

code='set --'
n=$#
while [ "$n" -gt 0 ]; do
  code="$code \"\${$n}\""
  n=$((n - 1))
done
eval "$code"
5

Pure bash solution, would work as a one-liner.

$: for (( i=${#array[@]}-1; i>=0; i-- ))
>  do rev[${#rev[@]}]=${array[i]}
>  done
$: echo  "${rev[@]}"
7 6 5 4 3 2 1
  • 1
    nice one!!! THX; here the one liner to copy :-) array=(1 2 3 4 5 6 7); for (( i=${#array[@]}-1; i>=0; i-- ));do rev[${#rev[@]}]=${array[i]}; done; echo "${rev[@]}" – nath Jul 02 '19 at 21:14
  • 2
    Doing rev+=( "${array[i]}" ) seems simpler. –  Jul 03 '19 at 20:46
  • Six of one, half-dozen of the other. I'm not fornd of that syntax, but have no reason for it - just prejudice and preference. You do you. – Paul Hodges Jul 05 '19 at 13:40
3
#!/bin/bash
(a=(1 2 3 4 5) r=(); for e in "${a[@]}"; do r=("$e" "${r[@]}"); done; declare -p a r)

prints

declare -a a=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5")
declare -a r=([0]="5" [1]="4" [2]="3" [3]="2" [4]="1")
1

Try this

#!/bin/bash

array=(1 2 3 4 5 6) index=$((${#array[@]}-1))

for e in "${array[@]}"; do result[$((index--))]="$e" done

echo "${result[@]}"

0

According to TIMTOWDI (There Is More Than One Way To Do It), here is my solution, reversing array a into r:

#!/bin/bash
set -u
a=(1 2 3)
t=("${a[@]}")
declare -p a t
r=()
while [ "${#t[@]}" -gt 0 ]
do
    r+=("${t[-1]}")
    unset 't[-1]'
done
echo "${r[@]}"
declare -p r

When executing with BASH "4.4.23(1)-release (x86_64-suse-linux-gnu)" I got:

+ set -u
+ a=(1 2 3)
+ t=("${a[@]}")
+ declare -p a t
declare -a a=([0]="1" [1]="2" [2]="3")
declare -a t=([0]="1" [1]="2" [2]="3")
+ r=()
+ '[' 3 -gt 0 ']'
+ r+=("${t[-1]}")
+ unset 't[-1]'
+ '[' 2 -gt 0 ']'
+ r+=("${t[-1]}")
+ unset 't[-1]'
+ '[' 1 -gt 0 ']'
+ r+=("${t[-1]}")
+ unset 't[-1]'
+ '[' 0 -gt 0 ']'
+ echo 3 2 1
3 2 1
+ declare -p r
declare -a r=([0]="3" [1]="2" [2]="1")
U. Windl
  • 1,411
  • 1
    Wasn't aware of that; thanks! – U. Windl Nov 18 '22 at 08:25
  • Well, thinking about it, the extra level of indirection isn't really needed. Originally I had associative arrays in mind, but reversing the keys doesn't really make any sense, so I simplified the solution. – U. Windl Nov 18 '22 at 08:31
  • You're still missing some quotes. Try it with a=('*' $'a\nb' 'foo bar') for instance or after IFS=0123456789. – Stéphane Chazelas Nov 18 '22 at 08:44
  • Agreed, but the original question does not use such complicated items ;-) Also it's one pair of double-quotes missing, right? – U. Windl Nov 18 '22 at 08:55
  • 2
    ${#t[@]} still asks the shell to split+glob it. In bash, you need to quote expansions in list contexts (and here it's in arguments to the [ command so is a list context) unless you want split+glob. See also Security implications of forgetting to quote a variable in bash/POSIX shells – Stéphane Chazelas Nov 18 '22 at 09:04
  • OK, maybe the BASH manual should actually warn about that; I had always thought that it simply returns a number, namely the number of items in the array. The manual says "${#name[subscript]} expands to the length of ${name[subscript]}. If subscript is or @, the expansion is the number of elements in the array.*" – U. Windl Nov 18 '22 at 09:14
  • It does, but if you forget to quote it, like for any parameter expansion, it is subjected to split+glob which you don't want here. I agree the bash manual (and the POSIX specification which still has many examples with missing quotes) should be more explicit. shellcheck can also be used to spot some of those mistakes. – Stéphane Chazelas Nov 18 '22 at 09:17
  • OK, I read a lot about it, but honestly: Isn't the assumption for most BASH questions and answers that "IFS is set to a half-way reasonable value" (i.e.: "default")? I'd say 99.5% of all shell scripts will break if IFS is set in some bad way, and IMHO the solution is not to add quotes like mad everywhere, but to use a different language then. Also: If IFS contains 0, would I have to quote 0, too? – U. Windl Nov 18 '22 at 09:39
  • No, $IFS is only involved in read and unquoted parameter expansions, command substitutions and arithmetic expansions. Leaving those unquoted is invoking that feature. It's a bad design but that's how it is. If you don't intend to do glob(split(var-contents)) you should use the quotes or switch to a shell with a saner design like rc, es or fish (or zsh though zsh still does empty removal upon unquoted expansions). – Stéphane Chazelas Nov 18 '22 at 11:24
0

Two different versions

Inspired from Cyrus's answer and wiki.wooledge.org. Very quick as there are no loop and no forks! ... On not too large bunch of data!!, regarding Stéphane Chazelas's comment, if you plan to manipulate big bunch of datas, you'd better mandate specialised tools!

And here, confined into one single function.

Print submited array in reversed order:

printReverseArray () {
    if shopt -q extdebug; then
        printf "%s " "${BASH_ARGV[@]}"
    else
        shopt -s extdebug
        "${FUNCNAME}" "$@"
        shopt -u extdebug
    fi
}

Sample run:

printReverseArray world! good Hello
Hello good world!

printReverseArray baz "Foo bar" Foo bar baz

Reverse array variable:

reverseArray() { 
    if shopt -q extdebug; then
        _ArrayToReverse=("${BASH_ARGV[@]}")
    else
        local -n _ArrayToReverse=$1
        shopt -s extdebug
        "${FUNCNAME}" "${_ArrayToReverse[@]}"
        shopt -u extdebug
    fi
}

Then

myArray=({a..d}{1,2})
echo ${myArray[@]}
a1 a2 b1 b2 c1 c2 d1 d2

reverseArray myArray echo ${myArray[@]} d2 d1 c2 c1 b2 b1 a2 a1

  • Note that despite the fork and exec of tac, I find my approach is more than 10 times as fast as your very quick one on array=( {1..10000} ) on my system for instance (0.07 vs 0.8 second). Remember bash is one of the slowest shells around, and doing things builtin is not always a guarantee that it will be faster, especially for large datasets (where efficiency matters most). – Stéphane Chazelas May 16 '23 at 06:41
  • @StéphaneChazelas Correct! Answer edited! Anyway, if you plan to reverse repeatedly a small array, your solution will quickly become very slow. – F. Hauri - Give Up GitHub May 16 '23 at 06:58
  • Actually, I find that my approach is still faster even on an empty array. Not sure how bash can be so slow on yours that it would be slower than forking + executing + shoving data through pipes. Maybe the option processing is very slow in bash. – Stéphane Chazelas May 16 '23 at 07:04
  • Just tested with array=([0]="c" [1]="b" [2]="a"): 1000x reverseArray => 0.034s, while your approach: 1000x readarray...tac) => 4.882 seconds!!! – F. Hauri - Give Up GitHub May 16 '23 at 07:12
  • I don't know why I get so bad perf with your approach, I'll investigate a bit more when I have some time. That's with bash 5.1 on a 13 year old i5-based PC running Ubuntu 22.04. I get 2.6 vs 0.7 seconds for 1000x on a=() (where tac is not run) and 2.6 vs 2.3 for 1000x on a=(x). – Stéphane Chazelas May 16 '23 at 07:22
  • On another machine running Ubuntu 20.04 (bash 5.0), mine is only faster for empty arrays or for arrays of size > 150 items or about. – Stéphane Chazelas May 16 '23 at 07:30
-1

Bash

array=(1 2 3 4 5 6 7)
echo "${array[@]} " | tac -s ' '

Or

array=(1 2 3 4 5 6 7)
reverse=$(echo "${array[@]} " | tac -s ' ')
echo ${reverse[@]}

Result

7 6 5 4 3 2 1

Version

$ tac --version
tac (GNU coreutils) 8.28
mon
  • 180
  • 1
    tac was already mentioned: https://unix.stackexchange.com/a/412874/260978, https://unix.stackexchange.com/a/467924/260978, https://unix.stackexchange.com/a/413176/260978 – Olorin Jan 31 '19 at 06:43
  • I appreciate this answer because it uses tac -s ' ' to separate the input according to spaces. This makes the implementation short and easy to understand. – Jasha Jun 26 '21 at 13:18
  • That being said, this answer has the drawback that the output reverse is not an array. – Jasha Jun 26 '21 at 13:26
  • In my environment the result seems to behave like an array if I iterate over it. Also, maybe it could become a proper array by modifying the line to surround the expression (before assignment) with parenthesis, like: reverse=($(echo "${array[@]} " | tac -s ' ')) – r_alex_hall Jun 27 '21 at 21:54
  • 1
    tac won't work robustly unless you print with null byte as separator and use -s '' as shown in Stéphane's answer. – Toby Speight Nov 18 '22 at 19:25
-1

You can also consider using seq:

array=(1 2 3 4 5 6 7)

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

In FreeBSD you can omit the -1 increment parameter:

for i in $(seq $((${#array[@]} - 1)) 0); do
    echo "${array[$i]}"
done
Toby Speight
  • 8,678