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.