Using — Correctly: A Guide To Argument Parsing In Bash Builtins

Understanding Argument Parsing in Bash

In Bash scripts, arguments refer to the data passed to commands, programs, functions, and scripts. Understanding how Bash handles argument parsing is crucial for writing robust scripts that accept input correctly.

There are two main types of arguments in Bash:

  • Positional Arguments – Arguments that are mapped to variables based on their order or position. $1 refers to the first argument, $2 to the second, and so on.
  • Named Arguments – Arguments that start with – or — followed by the name and value. For example: --file=config.txt

When Bash encounters an argument, it tries to determine whether it is positional or named. Positional arguments are simply assigned to variables, while named ones involve more parsing to extract the name/value pairs.

Some built-in Bash commands like set and shopt accept named arguments starting with -. However, generally double dashes — signify the end of positional arguments for a command and the start of named ones.

Using — in Bash Builtins

The — symbol, also known as double-dash, has an important purpose when used in Bash built-in commands and scripts – it explicitly marks the end of positional arguments and the start of named ones.

Purpose of —

Consider this example:

mkdir new-project
cd new-project 
touch file-{1..3}
rm file-1 file-2 file-3

This will fail because rm treats the filenames as positional arguments. file-1 will be deleted but file-2 and file-3 lead to an error.

By adding — we explicitly state that what follows are just filenames, not positional parameters with special meaning:

mkdir new-project
cd new-project
touch file-{1..3}  
rm -- file-1 file-2 file-3

Now all files are correctly removed. So — marks the end of options and positional arguments, telling the command to treat the rest as just parameters.

When — is Required

Using — is essential in these common cases:

  • Passing filenames or arguments that start with – to avoid confusion with named flags.
  • When options should be passed to a program instead of the command itself. For example, docker run -- sh -c ls passes -c to sh not docker.
  • Passing arguments from one program to another, when positional parameters must be preserved and not interpreted by intermediate commands.

Common Mistakes with —

Misusing — can lead to frustrating errors or misbehavior. Here are some common anti-patterns:

  • Inserting — in the middle of options. It must mark the end of options.
  • Overusing — when filenames don’t conflict with flags or positional arguments.
  • Assuming — passes arguments to all programs in a pipeline. Arguments must be explicitly handled program-by-program.

Argument Parsing Examples

Let’s explore some code examples to demonstrate correct argument handling behavior in Bash.

Basic Argument Parsing

Bash makes command arguments available in special variables like $1, $2 etc. Here is code that prints all arguments passed:

#!/bin/bash

echo "Total arguments: $#"
echo "Arguments: $@"
echo "First argument: $1"
echo "Second argument: $2"
echo "All other arguments: ${@:3}" 

This accepts any number of positional arguments and prints them all.

Parsing Arguments with Flags

For more robust argument handling this script uses flags that start with – or –:

#!/bin/bash

verbose=false
filenames=()

while [[ $# -gt 0 ]]; do
   case $1 in
     -v|--verbose) verbose=true; shift ;;
     --) shift; filenames+=("$@"); break ;;
     *) filenames+=("$1"); shift ;; 
   esac   
done

echo "Verbose mode: $verbose" 
echo "Filenames: ${filenames[@]}"

This uses flags like -v, handles –, and also gathers remaining positional arguments into an array called filenames.

Passing Arguments to Other Programs

To pass arguments correctly to other commands or scripts, preserving positional parameters, we must carefully use — along with quoting:

#!/bin/bash

files=$@

echo "Files are: $files"

# Won't work - passes $files as one argument 
find $files -type f

# Correct - passes each filename positionally
find -- $files -type f  

# Alternative - pass array explicitly 
find -- "${@}" -type f

This takes filenames as arguments, stores them, and passes them in turn to commands like find and grep consistently.

Tips for Proper Argument Handling

When writing Bash scripts that accept arguments, keep these tips in mind:

Quote Arguments with Spaces

Always quote arguments that have spaces and special characters:

# Unquoted - broken into separate arguments 
cp first file.txt file-copy.txt

# Quoted - treated as single filename 
cp "first file.txt" file-copy.txt

Check for Invalid Arguments

Validate passed arguments before using them with if statements or comparison operators:

if [[ $# -lt 2 ]]; then
  echo "Usage: $0 src dest" 
  exit 1
fi   

if [[ ! -d "$1" ]]; then
   echo "Error: Source directory $1 not found" 1>&2  
   exit 1
fi

Use getopts for Robust Parsing

For advanced argument handling, leverage Bash’s built-in getopts module:

  
while getopts ":hv:f:" opt; do
  case $opt in
    h) show_help; exit 0 ;;
    v) verbose=$OPTARG ;;
    f) file=$OPTARG ;;
    \?) echo "Invalid option: -$OPTARG"; exit 1 ;; 
  esac    
done 

This correctly handles flags, optional arguments, invalid inputs, and more.

Conclusion – Best Practices for Argument Parsing

Positional vs named arguments, quoting, parameter expansion are key concepts for properly handling input in Bash scripts. Mastering arguments allows passing input correctly to commands, programs, and scripts.

The key takeaways are:

  • Use — between options/flags and positional arguments.
  • Quote arguments with spaces and special characters.
  • Validate required arguments with conditionals early on.
  • Pass arguments positionally using — or explicit arrays.
  • Use getopts for advanced, robust argument parsing.

Following bash best practices for argument handling leads to scripts that work reliably in all edge cases.

Leave a Reply

Your email address will not be published. Required fields are marked *