Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

Bash Scripting with getopts

Recommended resources

Hello! Welcome back to our module on Advanced Bash Scripting.

In our last lesson, we built a foundation for robust scripting by implementing "strict mode" with set -euo pipefail and learning how to handle cleanup gracefully using trap. We now have a template for scripts that are safe and predictable. However, they are still rigid, with configurations hardcoded into variables.

Today, we'll make our scripts flexible and interactive by turning them into proper command-line tools. Your learning outcome for this lesson is to parse command-line arguments and options in Bash scripts using getopts. This will allow you to build scripts that are configurable at runtime, a crucial skill for creating reusable automation for your CI/CD pipelines, system administration tasks, and more.

1. Why We Need More Than $1, $2...

You're likely familiar with using positional parameters like $1, $2, etc., to pass inputs to a script. While simple for one or two required arguments, this approach quickly becomes fragile. What if the order is wrong? What if an argument is optional?

Let's see just how easily this can break.

How to Use Bash Getopts With Examples - KodeKloud

The article 'How to Use Bash Getopts With Examples' from KodeKloud starts with a perfect demonstration of the limitations of positional parameters. It shows how a simple script can fail in confusing ways when arguments are swapped or omitted.

Read the introductory section and the part titled 'Positional parameters are the variables that Bash assigns...'. Follow the demo.sh example to see how it misbehaves when arguments are swapped or missing. This will set the stage for why getopts is necessary.

As you saw, relying on argument order is a recipe for bugs. We need a way to parse named options (like -f filename or -v), which is the standard for command-line tools.


2. The getopts Built-in: A Portable Solution

Bash provides a built-in command called getopts for this exact purpose. It's designed to parse short options (e.g., -a) in a standard, portable way.

A quick but important note: Don't confuse getopts (with an 's') with the older, external utility getopt. getopts is built into Bash, making it more portable and predictable across different systems like Linux and macOS. For most DevOps scripting, getopts is the preferred choice for its reliability.

The core of getopts is a while loop combined with a case statement. This structure iterates through the options provided on the command line, allowing you to act on each one.


3. Parsing Simple Flags

Let's start by implementing options that act as simple on/off flags, such as a -h for help or -v for verbose output.

The basic syntax is while getopts "optstring" opt; do ... done.

  • The optstring defines the valid option characters. For now, our optstring will be "hv", meaning we recognize -h and -v.
  • opt is the variable that will hold the character for the option found in each iteration of the loop (e.g., h or v).

Let's modify the script template from our last lesson to include a usage function and parse a -h flag.




#!/bin/bash




# Enforce strict mode.
set -euo pipefail




# --- Functions ---
usage() {
  echo "Usage: $0 [-h] [-v] [other arguments...]"
  echo "A demonstration script for getopts."
  echo "  -h: Display this help message."
  echo "  -v: Enable verbose mode."
}

main() {
  local verbose=false




  # The getopts loop
  while getopts "hv" opt; do
    case "$opt" in
      h)
        usage
        exit 0
        ;;
      v)
        verbose=true
        ;;
      '?') # Note the quotes
        echo "Error: Invalid option." >&2
        usage
        exit 1
        ;;
    esac
  done

  echo "Script execution starts."
  if [[ "$verbose" == true ]]; then
    echo "Verbose mode is enabled."
  fi



  # ... rest of the script logic ...
}




# --- Main Execution ---
main "$@"

In this structure:

  • We initialize a verbose flag to false.
  • The while loop processes options.
  • The case statement checks which option was found.
    • If it's -h, we show the usage message and exit successfully.
    • If it's -v, we set our verbose variable to true.
    • The ?) case is a special pattern that catches any option not defined in our optstring ("hv"). This is our first layer of error handling.

4. Parsing Options with Arguments

Flags are useful, but most scripts need to accept values, like a filename or a username. To do this, you add a colon (:) after the option character in the optstring. For example, "f:" tells getopts that the -f option requires an argument.

When an option requires an argument, getopts stores that argument in a special variable called OPTARG.

The following video provides a clear, practical walkthrough of handling an option with an argument.

Command Line Arguments with getopts Linux Shell Programming BASH Script Tutorial

This video from Kris Occhipinti's channel demonstrates how to define an option that takes an argument and how to access its value using the $OPTARG variable.

Watch from 03:59 to 06:00. Focus on these key points: How the colon (:) is used in the optstring (i:cm) to signify that -i takes an argument. How the value provided to the option (e.g., chris in -i chris) is captured in the $OPTARG variable inside the case statement.


5. Robust Error Handling

Our script can handle invalid options, but what if a required argument is missing (e.g., the user types -f but no filename)? By default, getopts prints its own error message, but we can take full control for more professional error reporting.

To enable this "silent" error-handling mode, you place a colon (:) at the very beginning of your optstring.

When you do this, getopts behaves differently for errors:

  • Missing argument: Instead of matching ?, it matches : in the case statement. The name of the option missing its argument is placed in OPTARG.
  • Invalid option: It still matches ?, and the invalid option character is placed in OPTARG.

This gives you complete control over the error messages. Let's explore this with a detailed article.

How to Use Bash Getopts With Examples - KodeKloud

The KodeKloud article we saw earlier has an excellent section on handling options with arguments and the associated error conditions.

Read 'Example 2: Handling multiple options (with arguments) with getopts'. Study the script and pay close attention to the optstring :x:y:. Notice how the case statement now has both a :) and a ?) block to handle missing arguments and invalid options separately. This is the pattern for production-quality scripts.


6. Processing Remaining Arguments with shift

getopts stops parsing as soon as it encounters a non-option argument (one that doesn't start with -). But what if your script needs to process those too? For example: backup.sh -v --config /etc/app.conf /home/user.

Here, /home/user is a positional argument. To access it cleanly, we first need to get rid of the options that getopts has already processed. This is done using shift.

getopts keeps track of the index of the next argument to be processed in a variable called OPTIND. After the loop finishes, OPTIND points to the first non-option argument.

By running shift $((OPTIND - 1)), we tell the shell to discard all arguments from the beginning up to that index. After this command, $1 will now be the first positional argument, $2 the second, and so on.

How to Use Bash Getopts With Examples - KodeKloud

Let's return to the KodeKloud article for a concise explanation of this crucial final step in argument parsing.

Read the subsection 'Position of non-option arguments' within the 'Best Practices & Common Pitfalls' section. It shows the exact shift command and explains that "$@" will then contain only the remaining positional arguments.


7. An Updated Template for Command-Line Tools

Let's integrate everything we've learned into our robust script template. This gives you a powerful boilerplate for nearly any Bash-based command-line tool you'll need to write.




#!/bin/bash




# Enforce strict mode for safer scripts.
set -euo pipefail




# --- Global Variables ---
readonly SCRIPT_NAME="${0##*/}"
readonly TMP_DIR=$(mktemp -d)




# --- Functions ---




# Function to handle cleanup on script exit.
cleanup() {
  local exit_code=$? # Capture exit code.
  echo "--- Performing cleanup ---"
  rm -rf "$TMP_DIR"
  echo "Temporary directory '$TMP_DIR' removed."
  
  if [[ $exit_code -ne 0 ]]; then
    echo "Script failed with exit code: $exit_code" >&2
  fi
  
  exit "$exit_code"
}




# Standard usage message.
usage() {
  echo "Usage: $SCRIPT_NAME [-h] [-v] -o <output_file> <input_file>"
  echo "  -h             Display this help message."
  echo "  -v             Enable verbose mode."
  echo "  -o <file>      Specify the output file (required)."
}

main() {



  # --- Default values for options ---
  local verbose=false
  local output_file=""




  # --- Parse command-line options ---
  # The leading colon enables silent error handling.
  while getopts ":hvo:" opt; do
    case "$opt" in
      h)
        usage
        exit 0
        ;;
      v)
        verbose=true
        ;;
      o)
        output_file="$OPTARG"
        ;;
      ':')
        echo "Error: Option '-$OPTARG' requires an argument." >&2
        usage
        exit 1
        ;;
      '?')
        echo "Error: Invalid option '-$OPTARG'." >&2
        usage
        exit 1
        ;;
    esac
  done




  # --- Shift away parsed options ---
  shift $((OPTIND - 1))




  # --- Validate arguments ---
  # Check that the required output file was provided.
  if [[ -z "$output_file" ]]; then
    echo "Error: Output file must be specified with -o." >&2
    usage
    exit 1
  fi
  



  # Check that we have exactly one positional argument left.
  if [[ $# -ne 1 ]]; then
    echo "Error: Exactly one input file must be specified." >&2
    usage
    exit 1
  fi

  local input_file="$1"




  # --- Main script logic ---
  echo "Script execution begins."
  [[ "$verbose" == true ]] && echo "Verbose mode is ON."

  echo "Input file: $input_file"
  echo "Output file: $output_file"
  echo "Processing in temporary directory: $TMP_DIR"
  



  # Example operation
  cp "$input_file" "$output_file"
  echo "File copied successfully."
  
  log_message "This will not be logged because the script exits above."
}




# --- Register trap and execute main ---
trap cleanup EXIT
main "$@"

Study this template carefully. It combines the robustness from our last lesson (set -e, trap) with a complete, production-ready argument parsing implementation.


Conclusion

You've now moved beyond simple scripts and can build powerful, flexible command-line tools in Bash. By using getopts correctly, you make your automation more professional, user-friendly, and maintainable.

Key Takeaways:

  • getopts is the standard, portable Bash built-in for parsing short command-line options.
  • The while/case structure is the core pattern for using getopts.
  • The optstring defines valid options; a colon (:) after a letter means it requires an argument.
  • A leading colon (:) in the optstring enables silent error handling, giving you full control over error messages for invalid options (?) and missing arguments (:).
  • The argument for an option is stored in the $OPTARG variable.
  • Use shift $((OPTIND - 1)) after the loop to remove parsed options and easily access remaining positional arguments.

Preview of the Next Lesson:
With our script structure now solid and flexible, we'll turn to a core task in DevOps: processing text data. In the next lesson, we will learn how to filter and extract text using grep with regular expressions and key options (-i, -v, -c, -o).