<- Example 4: Grouping integers into ranges | Example 5: Reversing positional parameters
Reversing positional parameters
This example was presented as a programming exercise in #bash, not as a real-world problem. I'm presenting it here in three parts.
Write a function foo which calls function bar with its positional parameters reversed.
- Do the same using only POSIX sh features.
Do it in POSIX sh without using eval.
Part 1 is easy, part 2 is medium, and part 3 is hard.
Discussion
As this is a programming exercise, there is no real-world use case, and no need to analyze the problem specification to determine the actual goal. We can use a helper program or function to show arguments for testing purposes.
hobbit:~$ cat bin/args #!/bin/sh printf "%d args:" "$#" test "$#" -gt 0 && printf " <%s>" "$@" echo
I'll be using this in place of bar to test my implementations.
Implementation
Part 1
For part 1, we're allowed to use bash features such as indexed arrays. This means the obvious approach -- construct an array containing the reversed parameters -- is quite simple.
# Part 1: using bash features
argrev() {
local args=() i
for ((i=$#; i>0; i--)); do
args+=("${!i}")
done
args "${args[@]}"
}And, testing it:
hobbit:~$ argrev 1 '' '2 3' '*' '"' 5 args: <"> <*> <2 3> <> <1>
Part 2
For part 2, we can't use an array, because POSIX sh doesn't specify them. Indirect parameter expansions (${!i}) aren't available either. This eliminates the obvious approach. We'll have to think of another way.
One possible solution is to generate a string containing the reversed parameters in a quoted format, and then eval it to generate a command. The hard part here is getting the quoted parameters. Generally, when working with sh, "quoting" a parameter for a future eval means we replace every instance of ' with '\'' and then enclose the result in '...'. E.g. foo becomes 'foo', foo bar becomes 'foo bar', can't becomes 'can'\''t', and so on.
This means we need to call sed once for every parameter. Unpleasant!
# Part 2: using POSIX sh features (with eval)
argrev2() {
args=
for arg; do
quoted=\'$(printf '%s\n' "$arg" | sed "s/'/'\\\\''/g")\'
args="$quoted $args"
done
eval args "$args"
}hobbit:~$ argrev2 1 '' '2 3' '*' '"' \' 6 args: <'> <"> <*> <2 3> <> <1>
Part 3
Now, part 3 is fairly tricky. I can think of two ways to do it. The first way, which I won't use, would be to create a bunch of files, each containing one of the parameters to be reversed, with their names in descending order. E.g. if there are 10 parameters, write the first parameter in file 10, the second in file 09, and so on. Then, the files could be read in glob order (for f in *), with each file's content being appended to the positional parameters, finally leading up to args "$@". There are a few tricky pieces in here -- generating the proper filenames with an appropriate number of padding zeros, ensuring that each parameter is read back exactly (including newlines), etc.
However, the way I would solve part 3 is to use a trick I happen to have seen before. When looping over the positional parameters, e.g. for arg, the shell actually keeps a copy of the positional parameters in memory, and any alterations you make to the positional parameters inside that loop have no effect on the loop iteration. We can use this trick to build up a new set of positional parameters while we're iterating over the original set.
# Part 3: using POSIX sh features, no eval
argrev3() {
first=1
for arg; do
if test "$first"; then
set --
first=
fi
set -- "$arg" "$@"
done
args "$@"
}hobbit:~$ argrev3 1 '' '2 3' '*' '"' \' 6 args: <'> <"> <*> <2 3> <> <1>
Once you know about this trick, the implementation is fairly easy to write. Getting the correct number of backslashes in the sed command in part 2 is actually harder. This implementation is much more efficient than part 2, since we're not calling sed multiple times.
<- Example 4: Grouping integers into ranges | Example 5: Reversing positional parameters
