13

So if I've got a variable

VAR='10 20 30 40 50 60 70 80 90 100'

and echo it out

echo "$VAR"
10 20 30 40 50 60 70 80 90 100

However, further down the script I need to reverse the order of this variable so it shows as something like

echo "$VAR" | <code to reverse it>
100 90 80 70 60 50 40 30 20 10

I tried using rev and it literally reversed everything so it came out as

echo "$VAR" | rev
001 09 08 07 06 05 04 03 02 01
1

8 Answers 8

22

On GNU systems, the reverse of cat is tac:

$ tac -s" " <<< "$VAR "            # Please note the added final space.
100 90 80 70 60 50 40 30 20 10
4
  • Cheers! That did the trick. I've still got a lot to learn Nov 3, 2016 at 13:44
  • @JhonKugelman Without the echo, the output has an additional newline, equivalent to this: echo $'100 \n90 80 70 60 50 40 30 20 10'
    – user232326
    Nov 3, 2016 at 23:16
  • With the current tac -s" " <<< "$VAR " version, it still inserts a leading blank line, adds a trailing space and omits the trailing newline character (as if printf '\n%s ' "$reversed_VAR" instead of expected printf '%s\n' "$reversed_VAR") Nov 4, 2016 at 13:15
  • @don_crissti The original answer: echo $(tac -s" " <<< $VAR) does not have a trailing space because the $(…) removes trailing newlines and spaces if the IFS has them. My solution will be corrected soon, thanks.
    – user232326
    Nov 4, 2016 at 14:45
6

For a short list, and in a variable, the shell itself is the fastest solution:

var="10 20 30 40 50 60 70 80 90 100"
set -- $var; unset a 
for ((i=$#;i>0;i--)); do
    printf '%s%s' "${a:+ }" "${!i}"
    a=1
done
echo

Output:

100 90 80 70 60 50 40 30 20 10 

Note: the split operation in set -- $var is safe for the exact string used here, but will not be so if the string contains wildcard characters (*, ? or []). It may be avoided with set -f before the split, if needed. The same note is also valid for the following solutions (except where noted).

Or, if you want to set some other variable with the reverse list:

var="10 20 30 40 50 60 70 80 90 100"
set -- $var
for i; do out="$i${out:+ }$out"; done
echo "$out"

Or using only the positional parameters.

var="10 20 30 40 50 60 70 80 90 100"
set -- $var
a=''
for    i
do     set -- "$i${a:+ $@}"
       a=1
done
echo "$1"

Or using only one variable (which may be var itself):
Note: This solution is not affected by globing nor IFS.

var="10 20 30 40 50 60 70 80 90 100"

a=" $var"
while [[ $a ]]; do
    printf '<%s>' "${a##* }"
    var="${a% *}"
done
0
2

awk to the rescue

$ var='10 20 30 40 50 60 70 80 90 100'

$ echo "$var" | awk '{for(i=NF; i>0; i--) printf i==1 ? $i"\n" : $i" "}'
100 90 80 70 60 50 40 30 20 10


with perl, courtesy How to read an IP address backwards? shared by @steeldriver

$ echo "$var" | perl -lane '$,=" "; print reverse @F'
100 90 80 70 60 50 40 30 20 10


Or, with bash itself by converting the string to array

$ arr=($var)    
$ for ((i=${#arr[@]}-1; i>=0; i--)); do printf "${arr[i]} "; done; echo
100 90 80 70 60 50 40 30 20 10 
2

You can use bash features such as arrays to do it entirely in-process:

#!/bin/bash
VAR="10 20 30 40 50 60 70 80 90 100"
a=($VAR); rev_a=()
for ((i=${#a[@]}-1;i>=0;i--)); do rev_a+=( ${a[$i]} ); done; 
RVAR=${rev_a[*]}; 

This should be about the fastest way to do it provided that $VAR isn't very very large.

1
  • That however doesn't extend to variables whose value contains wildcard characters (like VAR='+ * ?'). Use set -o noglob or set -f to fix that. More generally, it's a good idea to consider the value of $IFS and the state of globbing when using the split+glob operator. No that it makes little sense to use that operator in rev_a+=( ${a[$i]} ), rev_a+=( "${a[$i]}" ) would make a lot more sense. Nov 4, 2016 at 10:32
1
printf "%s\n" $var | tac | paste -sd' '
100 90 80 70 60 50 40 30 20 10
1

Use a bit of Python magic here:

bash-4.3$ VAR='10 20 30 40 50 60 70 80 90 100'
bash-4.3$ python -c 'import sys;print " ".join(sys.argv[1].split()[::-1])'  "$VAR" 
100 90 80 70 60 50 40 30 20 10

The way this works is fairly simple:

  • we refer to "$VAR" as second command line paramenter sys.argv[1] ( first parameter with -c option is always just that - -c itself. Notice the quoting around "$VAR" to prevent splitting the variable itself into individual words ( which hisi done by shell , not python )
  • Next we use .split() method, to break it into a list of strings
  • [::-1] part is just extended slice syntax to get reverse of the list
  • finally we join everything together back into one string with " ".join() and print out

But those who aren't big Python fans, we could use AWK to do essentially same thing: split, and print array in reverse:

 echo "$VAR" | awk '{split($0,a);x=length(a);while(x>-1){printf "%s ",a[x];x--};print ""}'
100 90 80 70 60 50 40 30 20 10 
3
  • 1
    or somewhat cleaner python3 -c 'import sys;print(*reversed(sys.argv[1:]))' $VAR
    – iruvar
    Nov 4, 2016 at 4:35
  • @iruvar, not cleaner, that would invoke the shell's split+glob operator without making sure $IFS contains the proper characters and without disabling the glob part. Nov 4, 2016 at 13:09
  • @StéphaneChazelas, true enough - the python portion is still cleaner(IMHO) though. So, going back to "$VAR" yields python3 -c 'import sys;print(*reversed(sys.argv[1].split()))' "$VAR"
    – iruvar
    Nov 4, 2016 at 14:55
1

zsh:

print -r -- ${(Oa)=VAR}

$=VAR splits $VAR on $IFS. (Oa) orders the resulting list in reverse array order. print -r -- (like in ksh), same as echo -E - (zsh specific) is a reliable versions of echo: prints its arguments as-is separated by space, terminated by newline.

If you want to split on space only, and not on whatever $IFS contains (space, tab, newline, nul by default), either assign space to $IFS, or use an explicit splitting like:

print -r -- ${(Oas: :)VAR}

To sort in reverse numerical order:

$ VAR='50 10 20 90 100 30 60 40 70 80'
$ print -r -- ${(nOn)=VAR}
100 90 80 70 60 50 40 30 20 10

POSIXly (so would also work with bash):

With shell builtin (except for printf in some shells) mechanisms only (better for variables with a short value):

unset -v IFS # restore IFS to its default value of spc, tab, nl
set -o noglob # disable glob
set -- $VAR # use the split+glob operator to assign the words to $1, $2...

reversed_VAR= sep=
for i do
  reversed_VAR=$i$sep$reversed_VAR
  sep=' '
done
printf '%s\n' "$reversed_VAR"

With awk (better for large variables, especially with bash, but up to the limit of the size of the arguments (or of a single argument)):

 awk '
   BEGIN {
     n = split(ARGV[1], a);
     while (n) {printf "%s", sep a[n--]; sep = " "}
     print ""
   }' "$VAR"
0

Inelegant POSIX software tools one-liner:

echo `echo $VAR | tr ' ' '\n' | sort -nr`

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.