When I do something like ARGS="$@", the shell just concatenates the arguments stored in "$@". Is there a way to store "$@" into a variable in pure POSIX shell to be able to use it to call another program or function with the passed arguments?
1 Answer
The only array-like structure in a POSIX shell is the $@ list whose elements can be accessed independently with $1,$2,$3,... The number of elements available is given by $#.
$@ elements can be modified using set and shift.
The top-level and each function call has its own separate $@ array which is initialised to the arguments received by the script/function when it is called.
Using set or shift inside a function does not affect the top-level $@.
It is possible to save all the original elements of $@ somewhere but I don't believe there is way to use the result in a form as simple as bash's "${arr[@]}" syntax. (Individual elements may accessed without too much effort but not, trivially, the array as a whole.)
However, by appropriately reloading/manipulating the elements of $@ it can be used directly, although performing the manipulation is likely to be rather tedious.
A quick search for ways to accomplish the saving found these approaches:
- https://github.com/friendly-bits/POSIX-arrays (
declare_i_arr) - https://www.etalabs.net/sh_tricks.html (Working with arrays)
- https://github.com/PowerUser64/posix-arrays (
array_init)
Rich's code from the second link is probably the simplest and looks like:
save(){
for i do
printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
done
echo " "
}
and is used as:
myarray=$(save "$@")
set -- foo bar baz boo
# ... do stuff with new $@ ...
eval "set -- $myarray"
The eval is safe since $myarray expands to a list of single-quoted strings.
See
how-to-use-pseudo-arrays-in-posix-shell-script
for a more efficient and arguably easier to understand awk implementation of this idea. Here's the performance difference between the 2 implementations (timed using bash):
$ cat tst.sh
#!/usr/bin/env bash
save_sed(){
for i do
printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
done
echo " "
}
save_awk() {
LC_ALL=C awk -v q=\' '
BEGIN {
for ( i=1; i<ARGC; i++ ) {
gsub(q, q "\\" q q, ARGV[i])
printf "%s ", q ARGV[i] q
}
print ""
}
' "$@"
}
echo "calling sed in a loop:"
time save_sed >/dev/null $(seq 100)
echo ""
echo "calling awk once:"
time save_awk >/dev/null $(seq 100)
$ ./tst.sh
calling sed in a loop:
real 0m3.403s
user 0m1.014s
sys 0m2.615s
calling awk once:
real 0m0.042s
user 0m0.015s
sys 0m0.031s
The awk versions performance hardly changes when the number of arguments change while the loop+sed version increases/decreases by about a factor of 10 when the number of arguments changes by the same factor of 10.
$@acts as an array. POSIX shell does not support array variables. To re-use all args, just use"$@"as insomething_else "$@".$@no longer refers to the arguments the script was called with - you can propagate the elements down but it's a bit tedious and probably inefficient and would require every function to know how to handle the extra arguments