How to reverse shell arguments?

Portably, no arrays required (only positional parameters) and works with spaces and newlines:

flag=''; for a in "$@"; do set -- "$a" ${flag-"$@"}; unset flag; done

Example:

$ set -- one "two 22" "three
> 333" four

$ printf '<%s>' "$@"; echo
<one><two 22><three
333><four>

$ flag=''; for a in "$@"; do set -- "$a" ${flag-"$@"}; unset flag; done

$ printf '<%s>' "$@"; echo
<four><three
333><two 22><one>

The value of flag controls the expansion of ${flag-"$@"}. When flag is set, it expands to the value of flag (even if it is empty). So, when flag is flag='', ${flag....} expands to an empty value and it gets removed by the shell as it is unquoted. When the flag gets unset, the value of ${flag-"$@"} gets expanded to the value at the right side of the -, that's the expansion of "$@", so it becomes all the positional arguments (quoted, no empty value will get erased). Additionally, the variable flag ends up erased (unset) not affecting following code.


When wanting to use no array for temporary storage, we can use the fact that a for loop always iterates over an unchanging static set of elements. In a sense, we can use the loop itself as a temporary storage of the positional parameters while rebuilding the list in reverse order.

To be able to do this, we also need to empty the list on the first iteration. The code below uses a simple flag to detect whether this has to be done or not. When the list is emptied, the flag is toggled.

flag=true
for value do
    if "$flag"; then
        set --
        flag=false
    fi

    set -- "$value" "$@"
done

This is unfortunately quite slow, as the list of positional parameters is effectively rebuilt in each iteration (set -- some-list sets all positional parameters). The bash shell takes about 50 seconds to reverse the integers between 1 and 10000, while zsh takes just over 15 seconds.

Using Isaac's trick with ${flag-"$@"} (which expands to "$@" only if flag is unset) actually makes the whole thing run slower; 1 minute 50 seconds (!) in bash and 25 seconds in zsh.

I'm assuming this is due to some implementation particularities in how the shells perform the test on $flag and/or expand "$@" for the ${flag-"$@"} expansion (the shell might possibly expand "$@" twice internally?).


If allowing ourselves to use an array as temporary storage (this would not be standard, but still fairly portable since we often know what shell we're writing our scripts for), we can use the value $# (the number of positional parameters) as an index into which to store the current value while looping over the positional parameters. Decreasing this value using shift in each iteration gives the effect of inserting values from the end of the array towards the start.

In bash, arrays start at index 0, and since the shift comes after the assignment, the last positional parameter will be stored at index 1 rather than 0. This has no consequence for how the code works in bash, it will still generate the correct result, but it makes it also work in zsh (which uses 1-based array indexes by default).

Code:

tmp=()
for value do
    tmp[$#]=$value
    shift
done

set -- "${tmp[@]}"

With bash or zsh, this uses about 0.6 seconds to reverse the integers between 1 and 10000.


Copied from this answer of mine to Bash - print reversed file list using glob, to reverse the list of positional parameters POSIXly:

eval "set -- $(awk 'BEGIN {for (i = ARGV[1]; i; i--) printf " \"${"i"}\""}' "$#")"

Or slightly more legible on several lines:

eval "set -- $(
  awk '
    BEGIN {
      for (i = ARGV[1]; i; i--)
        printf " \"${" i "}\""
    }' "$#"
)"

The idea being to use awk to help generate the set -- "${3}" "${2}" "${1}" shell code for eval to interpret when "$@" has 3 elements for instance.

For large lists, it is likely to be significantly faster than using a shell loop especially one that rebuilds a list at each iteration. The awk code could be replaced by a shell loop that gives the same output (as @mosvy has shown in comments), but in my tests with bash5+gawk4.1, it's still twice as slow except for very short lists.

In zsh, you'd use the Oa parameter flag which is explicitly designed to reverse an array:

set -- "${(Oa)@}"

On my system (slightly slower than @Kusalananda's), and on a list of positional parameters obtained with set $(seq 10000), with bash5 + gawk4.2.1, that eval approach takes 0.4s while @Kusalananda's takes 1 minute and @Isaac's takes 2 minutes (zsh's Oa approach takes about 2 milliseconds).

With the sh and awk from busybox 1.30.1, those timings become: 0.06s, 11s, 11s respectively.