Modifying Shell Arguments with the set Command
This could be handy to dynamically adjust the arguments of a program based on whatever custom condition or criteria you need.
Here’s a minimal example:
You can put this into a demo
file and chmod +x demo
it.
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
echo "Before set: \$@ == ${@}"
set -- "${@}"
echo " After set: \$@ == ${@}"
Here’s a few outputs depending on how you run it:
$ ./demo
Before set: $@ ==
After set: $@ ==
$ ./demo hello world
Before set: $@ == hello world
After set: $@ == hello world
But now modify the script to change set -- "${@}"
into set -- "${@}", nice to meet you
:
$ ./demo hello world
Before set: $@ == hello world
After set: $@ == hello world, nice to meet you
And now modify the script to change the above line to set -- a b c "${@}"
:
$ ./demo hello world
Before set: $@ == hello world
After set: $@ == a b c hello world
The takeaway here is you can use set -- <...>
to prepend or append arguments
to "${@}"
. In this case we’re only using echo which echos things back but you
can use this to solve real use cases such as modifying flags passed to a
script based on a condition.
Remove the first or last argument
In the above demo script you can change the set line to set -- "${@:2}"
and
then you’d get this output which removes the first argument passed in:
$ ./demo hello world 1 2 3
Before set: $@ == hello world 1 2 3
After set: $@ == world 1 2 3
You could imagine having a script where maybe you want to process all arguments passed in and do something but then later on in the script you want to act on everything but the first argument.
Funny enough for this use case there’s a built-in command called shift
which
does the same thing but it’s nice to see how you could technically solve it
with set
.
If you wanted to get rid of the last argument then you could use set -- "${@:1:$#-1}"
:
$ ./demo hello world 1 2 3
Before set: $@ == hello world 1 2 3
After set: $@ == hello world 1 2
Overriding sudo
I’ve done a video about this in the past which
goes into the details of solving the problem but it skimmed over the details of
using set
.
Here’s the relevant pieces of that script:
sudo() {
[[ "${EUID}" == 0 ]] || set -- command sudo "${@}"
"${@}"
}
sudo cp demo /usr/local/bin
With our newfound knowledge of set
we can break down what’s happening here:
- The overwritten
sudo
function gets called - If / when the
OR
condition fires… ${@}
is currently set tocp demo /usr/local/bin
set --
prependscommand sudo
to${@}
${@}
runs the whole command which iscommand sudo cp demo /usr/local/bin
- The “real” sudo command is called and we copy the file
If we didn’t use set --
then the cp
command ends up being called twice:
- First, it does run with
sudo
due tocommand sudo "${@}"
and this works - Second,
"${@}
" runs on its own which iscp demo /usr/local/bin
since those are the args passed into thissudo
function and this gives a permission denied error
The video below goes into more detail and shows running the commands.
# Demo Video
Timestamps
- 0:09 – A shout out to Alex
- 0:41 – A little demo script to try things out
- 1:08 – Appending and prepending values to $@
- 2:01 – Chopping off the first or last element of $@
- 3:00 – Revisiting the override sudo real world use case
What types of use cases have you solved with set? Let me know below.