Objective

Learn how to use the bash getopts builtin to parse a script options

Operating System and Software Versions

  • Operating System: - All Linux distributions

Requirements

  • No special requirements, just access to a bash shell

Difficulty

MEDIUM

Conventions

  • # - requires given command to be executed with root privileges either directly as a root user or by use of sudo command
  • $ - given command to be executed as a regular non-privileged user

Introduction

Modifying a bash script behavior via runtime options, just like we normally do with command line programs, can be very useful. The bash shell provides the getopts builtin command, a standard way to achieve this. How to use it is what we will learn in this tutorial.

Creating our tests script

For the purpose of this tutorial, we are going to create a test script, and, with a lot of fantasy, we are going to call it test.sh. The purpose of this script will be to output something depending on the option we will provide (it won't be the most useful script in the world but it will be enough to explain how things work).

Let's open our text editor and begin:
#!/bin/bash
set -e
set -u
set -o pipefail
The provided shell options at the beginning of the scripts are not mandatory, but it's a good habit to use them in every script we write. In brief, -e, short for errexitmodifies the behavior of the shell that will exit whenever a command exits with a non zero status (with some exceptions). -u is another very important option: this makes the shell to treat undefined variables as errors.

Finally the pipefail changes the way commands inside a pipe are evaluated. The exit status of a pipe will be that of the rightmost command to exit with a non zero status, or zero if the all the programs in the pipe are executed successfully. In other words, the pipe will be considered successful if all the commands involved are executed without errors.

The getopts construct

After this brief digression, let's return to the main point of this tutorial and let's see how getopts works:
#!/bin/bash
set -e
set -u
set -o pipefail

while getopts 'lha:' OPTION; do
  case "$OPTION" in
    l)
      echo "linuxconfig"
      ;;

    h)
      echo "h stands for h"
      ;;

    a)
      avalue="$OPTARG"
      echo "The value provided is $OPTARG"
      ;;
    ?)
      echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
      exit 1
      ;;
  esac
done
shift "$(($OPTIND -1))"
Let's analyze what we have above. First of all the getopts commands must be used inside a while loop so all options are parsed. Then immediately after the getopts keyword, we defined the possible options our script will accept. They are l, h and finally a.

What does the colon after this last option means? It is the way we tell getopts that the option requires an argument. Each parsed option will be stored inside the $OPTION variable, while an argument, when present, will become the value of the$OPTARG one.

Each option is managed inside a case statement, with a final ? case that will be matched whenever an option that doesn't exist will be provided. In that case we will just echo the user the correct script usage and exit with an error code.

Testing our script

First, we are going to call our script providing a non existing option, just to test the case we mentioned above. Let's give the script executable permissions and then call it:
chmod +x test.sh && ./tesh.sh -c
As expected we will receive a message telling us that the provided option, is illegal and then instructions about how to use the script:
./test.sh: illegal option -- c
script usage: test.sh [-l] [-h] [-a somevalue]
While the script usage string is the one we set to be printed on usage error, the above it, it's a message automatically generated by getopts and it can be disabled by prefixing the options with a :. Now let's see what happens when we use the script the way it was meant to:
./test.sh -l
linuxconfig
We used the l option, and the script printed onscreen the string we set in the corresponding case, this is also what happens if we provide the -h option:
./test.sh -h
h stands for h
Let's now try to call the script with the a option. As said above this option requires an argument, and will fail if the latter is not provided:
./test.sh -a
./test.sh: option requires an argument -- a
script usage: test.sh [-l] [-h] [-a somevalue]
As expected, the script responded with an error message, reminding us that the option we provided requires an argument:
./test.sh -a tux
The value provided is tux
This time the script responded without error. Notice how the argument we provided, tux it's printed because it becomes the value of the $OPTARG variable.

Using getopts, you can provide also more than one option at the time to your script, combining the flags when you launch it. For example, let's see what happens when we try to call our scripts with both the l and h options:
./test.sh -lh
linuxconfig
h stands for h
As we can observe, both the options got processed, in the order we provided them. Of curse the options can be given separately, and we can also add the a option, but always remembering to add the required argument:
./test.sh -l -h -a tux_rulez
linuxconfig
h stands for h
The value provided is tux_rulez

The $OPTIND variable

If we take another look at the very trivial script we have written above, we see another instruction at the end of the while loop: shift $(($OPTIND -1)). What is its purpose? Let's say we call the script this way:
./test.sh -l -h -a hello world
When the script is invoked, the $OPTIND variable is set to 1, and it is incremented each time an option is parsed, until it reaches the last one. Therefore, in this case, it will have a value of 5 when it will reach the last element, which is hello, the mandatory argument required by the -a option.

shift is a shell builtin which moves the positional parameters of the script down a specified number of positions provided to it as a positive number, discarding the related arguments.

The way we launched the script above, the last provided argument, "world", is not parsed by getopts, which at that point already finished its job.

Now, if we make a shift of the value of $OPTIND - 1 (that is the effective number of the parsed options, 4 in this case) on the positional parameters, what remains are just the arguments that are not options, in this case "world", that we can access by using the $* variable later in this script.