Simple Usage of Optional Flags in Bash Script

Simple Usage of Optional Flags in Bash Script

14-05-2023
Staple
Bash, Scripting
cookie-banner

what is optional flags? #

one of the powerful features of command-line programs is the capability to pass multiple arguments during the time of execution. that way, the program can run straight-up the way we wanted to without having another STDIN session to take the user input.

there are multiple ways of passing multiple arguments and parsing them so the program can read them without hassle. one of the best approaches is to have optional flags. these flags are represented by character letters preceded by a hyphen, -u duke64, or by a word preceded by a double hyphen, --username duke64. the parameter value of each corresponding flag is defined next to it by whitespace.

one of the program that is designated for this feature is the getopts utility, which retrieves options and option arguments from a list of parameters. getopts is a replacement for GNU/Linux utility getopt from a standalone program to become a shell’s built-in command. even though both serve the same main function, one difference is that getopts cannot read word-based flags, while getopt can. to overcome that while still maintaining the script’s readability and simplicity from using getopt, we will use positional arguments instead, such as $1, that will be passed and parsed manually later on.

simple use case #

there are two cases that we will demonstrate: the use of getopts in testflag-1.sh and positional arguments in testflag-2.sh. both scripts are going to contain the same mockup functions that are later on going to be tested; they are:

  • usage() for listing the possible flags
  • invalid() for throwing erorr message of unknown flag(s)
  • generate() as the non-argument flag function to generate the whole output message, with the argument $1 for the name of the user, $2 for the age, and $3 for the sex
 1#!/bin/bash
 2#----------
 3usage() {
 4  echo "list of available options goes here"
 5  exit 0; }
 6invalid() {
 7  echo "testflag: detecting improper option"
 8  echo "try option '-h' for more information"
 9  exit 1; }
10generate() {
11  echo "my name is $1 ($3)" | grep -E --color "$1|$3"
12  echo "i am $2 years old"  | grep -E --color "$2"
13  exit 0; }
14#----------  

getopts #

in getopts, a collection of the valid option letters, OPTSTRING, will determine whether the flag is argument-based or not. that distinction is defined by the usage of a trailing semicolon :, which indicates that an argument variable called OPTARG will set that particular argument from the flag. here are the following snippets on the overall usage of getopts:

14#----------
15while getopts "n: a: s: g h" opt; do
16  case $opt in
17    n) n="${OPTARG}" ;;
18    a) a="${OPTARG}" ;;
19    s) s="${OPTARG}" ;;
20    g) opt_sub="generate" ;;
21    h) usage ;;
22    *) invalid ;;
23  esac
24done
25#----------
26case $opt_sub in
27  "generate") [[ "$n" && "$a" && "$s" ]] && generate "$n" "$a" "$s" || invalid ;;
28esac
29#----------
30usage

the argument-based flag in OPTSTRING that is going to take the user’s arguments are n: for the name of the user, a: for the age, and s: for the sex. as one of the improvement from getopts, it will also throws an error if an argument from the argument-based flag is not supplied, which is set to an explicit null.

each OPTSTRING option, later on, will be iterated from the while loop and checking it to its corresponding letter via case statement. if the provided flag by the user does not exist in the OPTSRING option and one of the case statements, then the option will be thrown to the default case to perform an invalid() function.

lastly, there’s a case statement that will check if the user wanted to perform generate() using the -g flag. with the AND logical operator (&&) in the if statement, all the other mandatory argument-based flags must be provided, or the script will throw the invalid() function to warn the user about an incomplete argument. if all the arguments don’t meet any cases defined in the case statement, the script will display the usage() function instead.

here is one case for using getopts in testflag-1.sh on testing the overall flag to produce the desired output as well as the built-in error handling:

positional arguments #

in positional arguments, one of the approaches we take is to create an array called args first that act as a pool of valid options, including letters and their corresponding keywords. while that array stores all the valid arguments, the special variable $# will store the total arguments that are being passed to the script, which requires at least one flag for the while loop to run functionally. here are the following snippets on the overall usage on positional arguments.

14#----------
15args=(n name a age s sex g generate h help)
16while [ $# -gt 0 ]; do
17  if [[ "$1" == -* ]]; then
18    raw_opt=$(printf "%s\n" "$1" | tr -d '-')
19    if [[ $raw_opt ]]; then
20      if [[ $(echo "${args[@]}" | grep -ow "$raw_opt" | wc -w) -eq 1 ]]; then
21        case $1 in
22          -n | --name)      n="$2" ;;
23          -a | --age)       a="$2" ;;
24          -s | --sex)       s="$2" ;;
25          -g | --generate)  opt="generate" ;;
26          -h | --help) usage ;;
27        esac
28      else
29        echo "$0: illegal option -- $raw_opt"
30        invalid
31      fi
32    fi
33  fi
34  shift  
35done
36#----------
37case $opt in
38  "generate") [[ "$n" && "$a" && "$s" ]] && generate "$n" "$a" "$s" || invalid ;;
39esac
40#----------
41usage

there are nested if statements that we will be walking through. the first statement in line 17 is to check whether the passed raw arguments are using the valid flag format. it is determined by checking each argument that at least has the first character as a hyphen. the use of double hyphens for a word-based flag isn’t a problem here since it uses a trailing wildcard. once it passes, the flag in $1 will be truncated and stored in raw_opt variable so that it can be compared just by word or letter from the args array.

while the statement at line 19 is to check if the flag isn’t null, the last if statement is used to check whether the flag exists within the args pool. the usage of wc is going to tell if the grep command is successful; the output should be 1, as it matches the overall operator to be equal to 1. keeping the positional arguments still in $1 as the flag character or word and $2 as the flag’s value is possible by using the shift command, which moves the positional arguments to always begin in $1 for all the flag, despite the iteration from the while loop.

unlike using getopts, here we need to create our error handling of invalid flag options that are being provided, which is why we have several if statements to do so. since it can’t read a default case like getopts, we can put those invalid cases in the else block if the flag isn’t matched any of the args flags. then the rest is the same as the getopts section, which has a specific case statement to perform the generate() function if all the mandatory flags are provided.

here are one case for using positional arguments as the optional flag in testflag-2.sh on testing the overall flag, both letter-based and word-based, to produce the desired output as well as the custom error handling:

the verdict is in #

to have a script that handles kinds of flags, both getopts and positional arguments servers well in terms of parsing it. while getopts more towards simplicity and readability of the overall workflow, using positional arguments and parsing them manually may give you some form of flexibility, especially the capability to read word-based flags like we used to see on other programs in GNU/Linux. good luck with whatever stuff you’re doing

resources #

references #