Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Modifying Shell Arguments with the set Command

blog/cards/modifying-shell-arguments-with-the-set-command.jpg

This could be handy to dynamically adjust the arguments of a program based on whatever custom condition or criteria you need.

Quick Jump: Demo Video

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 to cp demo /usr/local/bin
  • set -- prepends command sudo to ${@}
  • ${@} runs the whole command which is command 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 to command sudo "${@}" and this works
  • Second, "${@}” runs on its own which is cp demo /usr/local/bin since those are the args passed into this sudo 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.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per month (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments