Blog-o de Ganso
Publicado en

Parsing Bash Options Like a Champ

Say we want to create a script parrot.sh whose sole purpose in this world is to echo every argument that is passed to it:

#!/bin/bash
# parrot.sh: Output all given arguments. If no argument is given, print nothing.
[[ $# -gt 0 ]] && echo $@

parrot-2

Easy, huh? But I find this parrot boring, so let's now say we want to be able to choose the color of the output. We could accomplish this by using the first argument as the color selector and the rest of the arguments as the string to be echoed.

#!/bin/bash
# parrot.sh: Output all given arguments with an optional color. If no argument is given, print nothing.

colors=(red green yellow blue)
color(){
    case "$1" in
        red)
            tput setaf 1
            ;;
        green)
            tput setaf 2
            ;;
        yellow)
            tput setaf 3
            ;;
        blue)
            tput setaf 4
            ;;
        *)
    esac
}

if [[ " ${colors[@]} " =~ $1 ]]; then
  color $1 && echo "${@:2:$#}"
else
  echo $@
fi

Since we might still want to use parrot without colored output, the script first checks whether the first argument is an actual color and if it isn't, the argument is echoed normally. Let's test the new script:

parrot-3

Oh, there is a problem. The output is clearly not what is intended whenever we want to output a phrase that starts with a word that could be interpreted as a color. We could add a sentinel value to the color array but it seems quite hacky. Additionally, what if we wanted to add another another functionality, such as word replacing or printing every argument in uppercase? Clearly this script could get out of control really fast.

Enter getopts

Go and give this article a quick read and come back. It's ok, I'll wait.

Using getopts we can easily define and parse the arguments given to our script. If you read the article, you now know that we can define an option for the color and stop worrying about the corner cases such as phrases that begin with our sentinel, so let's update our parrot script with this new tool.

#!/bin/bash
# parrot.sh: Output all given arguments with an optional color. If no argument is given, print nothing.

# ... the color function is defined as before

usage(){
  echo "usage: parrot.sh [-c color] words"
}

while getopts ":c:" opt; do
  case $opt in
    c)
      [[ " ${colors[@]} " =~ " $OPTARG " ]] && COLOR=$OPTARG
    ;;
    *)
      usage
      exit 1
  esac
done

shift $((OPTIND - 1)) # Now $1 refers to the first non-option
[[ $# -gt 0 ]] && color "$COLOR" && echo "$@"

In this new version, we can pass a -c option to the script to choose the color and we're back to printing all the non-option arguments as we did in our first version. Pay attention to the shift command, if we weren't using it, we'd echo all arguments (including options). Additionally, a usage message has been added to let clients know how to use the script. The ":c:" string passed to getopts enables the fail-silent mode (no error messages are output by getopts) and defines an option -c that takes a mandatory argument.

parrot-4

Finally, let's say we want to add an option to output our phrase using uppercase. Now that we know how to use getopts it is only a matter of adding the new option to the case statement and using tr to modify our arguments. Note: I'm using the version of bash that came preinstalled with El Capitan and the ${var^^} option to change a string into uppercase isn't working, ergo, I had to use tr.

The updated version of our parrot script follows:

#!/bin/bash

usage(){
  echo "usage: parrot.sh [-c color] [-u] words"
}

color(){
    case "$1" in
        red)
            tput setaf 1
            ;;
        green)
            tput setaf 2
            ;;
        yellow)
            tput setaf 3
            ;;
        blue)
            tput setaf 4
            ;;
        *)
    esac
}
colors=(red green yellow blue)

while getopts ":c:u" opt; do
  case $opt in
    c)
      [[ " ${colors[@]} " =~ " $OPTARG " ]] && COLOR=$OPTARG
    ;;
    u)
      UPPER=1
    ;;
    *)
      usage
      exit 1
  esac
done

shift $((OPTIND - 1)) # Now $1 refers to the first non-option
[[ $# -eq 0 ]] && exit
ret=$@
[[ "$UPPER" == "1" ]] && ret=`echo $ret | tr [[:lower:]] [[:upper:]]`
color "$COLOR" && echo "$ret"

We can now mix our options as we like:

parrot-5

Conclusion

Using getopts can greatly simplify the use of options in our scripts and functions. It is a must-have in the tool belt of every bash ninja.