
Handling Arguments in Bash Scripts
1335 words. Time to Read: About 13 minutes.Using Bash scripts to automate a set of commands is the first step in a journey of building tools and making your life easier. Simple, top-to-bottom scripts that run the same way each time are powerful, and can save you time. There will come a point, though, when you want to customize the behavior of your script on the fly: making directories with custom names, downloading files from specific git repositories, specifying IP addresses or ports, and more. That’s where script arguments come in.
Built-In Variables
Bash provides us with some variables that are present in any script you write. Here are a few of the most useful:
$1, $2, $3, ...
: Positional Arguments
These are positional arguments. They hold the arguments given after your script as it was run on the command line. For example, if you have a script that gets run like this:
The variable $1
will hold the value "200"
, and the variable $2
will hold the value "goats"
.
For example, I use positional arguments in one of my simplest scripts, but it’s one that I run almost every day at work (simplified for the moment):
Do you see how I took the special variable $1
and stored its value in an actual named variable? You don’t need to do that, necessarily. I could have said mkdir -p "$1/{CAD,drawings,mold,resources}"
and everything would still work fine. However, I like to store positional arguments into named variables near the top of my script so that anyone who reads my script will have an idea of what the script is expecting. Granted, it’s no substitute for good documentation and robust error handling, but it’s a nice little self-documenting readability bonus that helps out a little. It’s definitely a good practice to get into.
When I run it like this:
It generates the directory structure:
$0
: The Script Name
This provides the script name as it was called. This is a nice shortcut that’s especially helpful for things like outputting usage messages and help.
Then, we can run it like this:
$#
: Argument Count
This one is great for error handling. What happens when our script doesn’t get the arguments it needs? Let’s update the greeting script above with some error handling.
$?
: Most Recent Exit Code
I don’t personally use this one all that much in scripts, but I do use it on the command line a lot. A lot of commands don’t even give any output when they fail. They just do nothing. So how do you know if it failed? The exit code of the last command run gets stored in the $?
variable.
Here’s an in-script example:
$@ and $*
: All the Args
These variables seem to cause the most confusion in Bash newbies—and reasonably so! They do almost exactly the same thing, but the differences can be a big deal depending on your situation. Here’s the low-down.
When you don’t quote these variables, they both do the same thing, dropping all of the arguments provided in at that location.
Running this:
See how even the quoted argument got split up on spaces? Sometimes this is what you want, but very often it is not. The quoted version of these variables is where things get interesting.
When you quote $*
, it will output all of the arguments received as one single string, separated by a space1 regardless of how they were quoted going in, but it will quote that string so that it doesn’t get split up later.
Running it:
See? One argument! You want to implement echo
ourselves?
Neat, right?
In contrast, when you quote $@
, Bash will go through and quote each argument as they were given originally. This is probably the most useful version, in my opinion, because it lets you pass all the arguments to sub-commands while still retaining spaces and quotes exactly how they were without letting Bash’s automatic string splitting muck things up.
You’ll see this one a lot in scripts that have a ton of functions. It’s traditional, if you’ve got lots of functions, to make the last function in the script a main
function that handles all the arguments and contains the orchestration logic of the script. Then, in order to run the main function, typically, the last line of the script is main "$@"
. Thus:
Looking Forward: Fancier Args
Hopefully now, you’re starting to feel the power of customization. You can abstract some tasks away using Bash scripts, even if they contain situationally specific logic, because you can provide arguments at call time! But this is just the tip of the iceberg. If you’ve been around the command-line long, you’ve probably seen some commands that have oodles of flags, options, settings, and subcommands that all need processed, and doing it with simple positional arguments won’t be powerful enough to get the job done.
I’ll have another post ready soon that goes over fancier argument parsing: how to handle many arguments, flags, getops
, shift
, STDIN, and more.
Like my stuff? Have questions or feedback for me? Want to mentor me or get my help with something? Get in touch! To stay updated, subscribe via RSS