[!TIP|label:see also:]

shell parameter parsers

PARSER STYLE DESCRIPTION PROS CONS PORTABILITY
🔸 case + shift loop Manual parsing with a while + case combo - Highly customizable
- Simple
- Error-prone for complex flags
- No automatic help
✅ POSIX-compliant
🔸 getopts (built-in) Built-in short-option parser - Easy for short options (-x) - No long options
- No auto help
✅ POSIX-compliant
🔹 getopt (external) External tool to parse long/short flags - Supports long & short
- Auto reordering
- Not always installed
- Not portable across BSD/Linux
⚠️ Not portable
🔸 bash-argparse (lib) Bash-based helper library - Structured, powerful
- Python-like syntax
- Adds dependency
- Not always installed
❌ Bash-only
🔹 docopts (doc-style) Parses from usage doc string - Clean UX
- Declarative
- Heavy
- Requires Python or external
❌ External
🔸 argbash (codegen) Generates parsing boilerplate - Auto docs/help
- Safe/robust
- Needs build step ❌ Not runtime native
🔹 shflags (Google) Lightweight Bash lib - Google-style
- Nice syntax
- Needs sourcing a lib ⚠️ Bash-only
PARSER SHORT OPTS LONG OPTS PORTABLE HELP GEN EXTERNAL?
Manual Case ✅ (manual) ✅ POSIX
getopts ✅ POSIX
getopt ✅ (GNU)
bash-argparse ❌ Bash
docopts ✅ (Python)
argbash ✅ (tool)
shflags ✅ (lib)

pass parameters to another script

[!NOTE]

  • objective:
    $ ./b.sh 1 2 3 4 5` -> $ ./a.sh 2 3 4 5
    
  • b.sh

    #!/bin/bash
    echo """
    b.sh:
      \$1                   : "$1"
      \$#                   : "$#"
      \$@                   : "$@"
      \${@: -1}             : ${@: -1}
      \${@: -2}             : ${@: -2}
      \${@: -3}             : ${@: -2}
      \${@: -\$(( \$#-1 ))} : ${@: -$(( $#-1 ))}
      \$(echo '\${@: -\$(( \$#-1 ))}' | cut -d' ' -f1-) : $(echo "${@: -$(( $#-1 ))}" | cut -d' ' -f1-)
    """
    
    echo -e "\n'~~> ./a.sh \"\${@: -1}\"': ~~~> ./a.sh ${@: -1}:"
    ./a.sh "${@: -1}"
    
    echo -e "\n'~~> ./a.sh \$(echo '\${@: -1}' | cut -d' ' -f1-)': ~~~> ./a.sh $(echo "${@: -1}" | cut -d' ' -f1-):"
    ./a.sh $(echo "${@: -1}" | cut -d' ' -f1-)
    
    echo -e "\n'~~> ./a.sh \"\${@: -4}\"': ~~~> ./a.sh ${@: -4}:"
    ./a.sh "${@: -4}"
    
    echo -e "\n'~~> ./a.sh \$(echo '\${@: -\$(( \$#-1 ))}' | cut -d' ' -f1-)': ~~~> ./a.sh $(echo "${@: -$(( $#-1 ))}" | cut -d' ' -f1-)"
    ./a.sh $(echo "${@: -$(( $#-1 ))}" | cut -d' ' -f1-)
    
  • a.sh

    echo """
    a.sh:
      \$1: "$1"
      \$#: "$#"
      \$@: "$@"
      \${@: -$(( $#-2 ))}: ${@: -$(( $#-2 ))}
    """
    
  • result

    $ ./b.sh 1 2 3 4 5
    
    b.sh:
      $1                 : 1
      $#                 : 5
      $@                 : 1 2 3 4 5
      ${@: -1}           : 5
      ${@: -2}           : 4 5
      ${@: -3}           : 4 5
      ${@: -$(( $#-1 ))} : 2 3 4 5
      $(echo '${@: -$(( $#-1 ))}' | cut -d' ' -f1-) : 2 3 4 5
    
    '~~> ./a.sh "${@: -1}"': ~~~> ./a.sh e:
    a.sh:
      $1: 5
      $#: 1
      $@: 5
      ${@: --1}: 5
    
    '~~> ./a.sh $(echo '${@: -1}' | cut -d' ' -f1-)': ~~~> ./a.sh 5:
    a.sh:
      $1: 5
      $#: 1
      $@: 5
      ${@: --1}: 5
    
    '~~> ./a.sh "${@: -4}"': ~~~> ./a.sh 2 3 4 5:
    a.sh:
      $1: b
      $#: 4
      $@: 2 3 4 5
      ${@: -2}: 4 5
    
    '~~> ./a.sh $(echo '${@: -$(( $#-1 ))}' | cut -d' ' -f1-)': ~~~> ./a.sh 2 3 4 5
    a.sh:
      $1: 2
      $#: 4
      $@: 2 3 4 5
      ${@: -2}: 4 5
    

Manual Case-Loop Parser

[!NOTE]

  • ✅ Pros: Simple, portable
  • ❌ Cons: No built-in validation or help
#!/usr/bin/env bash
# shellcheck disable=SC1079,SC1078

usage="""USAGE
\t$0\t[-h|--help] [-c|--clean] [-t|--tag <tag>] [-i|--image <image>]
\t\t\t[-v|--ver <new-version>] [-n|--name <name>]
\t\t\t[-p|--prop <key=value>]
"""

while test -n "$1"; do
  case "$1" in
    -c | --clean    ) clean=true        ; shift   ;;
    -t | --tag      ) tag=$2            ; shift 2 ;;
    -i | --image    ) image=$2          ; shift 2 ;;
    -v | --ver      ) ver=$2            ; shift 2 ;;
    -n | --name     ) name=$2           ; shift 2 ;;
    -p | --prop     ) prop=$2           ; shift 2 ;;
    -h | --help | * ) echo -e "${usage}"; exit 0  ;;
  esac
done

echo """
  clean : ${clean}
    tag : ${tag}
  image : ${image}
    ver : ${ver}
   name : ${name}
   prop : ${prop}
"""
  • result

    $ ./longopts.sh -h
    USAGE
      ./longopts.sh [-h|--help] [-c|--clean] [-t|--tag <tag>] [-i|--image <image>]
                    [-v|--ver <new-version>] [-n|--name <name>]
                    [-p|--prop <key=value>]
    
    $ ./longopts.sh -c
      clean : true
        tag :
      image :
        ver :
       name :
       prop :
    
    $ ./longopts.sh -c -t 'ttt' -i 'iii' --ver '1.1.1' --name 'name'
      clean : true
        tag : ttt
      image : iii
        ver : 1.1.1
       name : name
       prop :
    
until [ -z "$1" ]; do # Until all parameters used up
  echo "\$@  : $@ "; shift ;
done

# result
$ ./shift.sh 1 2 3 4 5
$@  : 1 2 3 4 5
$@  : 2 3 4 5
$@  : 3 4 5
$@  : 4 5
$@  : 5

Bash Equals-Separated

for i in "$@"; do
  case $i in
    -e=* | --extension=*  ) EXTENSION="${i#*=}"  ;  shift    ;;
    -s=* | --searchpath=* ) SEARCHPATH="${i#*=}" ;  shift    ;;
    --default             ) DEFAULT=YES          ;  shift    ;;
    -* | --*              ) echo "Unknown option $i"; exit 1 ;;
    *                     )                                  ;;
  esac
done

echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)

with one or more values

[!TIP]

  • positional_args+=("$1") to add the arguments to an array one by one
var1=""
flag_verbose=false

# POSIX format using `while [ $# -gt 0 ]`
while [[ $# -gt 0 ]]; do
  case "$1" in
    -v|--verbose ) flag_verbose=true             ;  shift   ;;
    -f|--file    ) var1="$2"                     ;  shift 2 ;;
    --dry-run    ) dryrun=true                   ;  shift   ;;
    -*           ) echo "Unknown option: $1" >&2 ;  exit 1  ;;
    *            ) positional_args+=("$1")       ;  shift   ;;
  esac
done

additional params on --

#!/usr/bin/env bash
# shellcheck disable=SC2051,SC2086

VERBOSE=false
DEBUG=false
MEMORY=
AOPT=
while true; do
  case "$1" in
    -v | --verbose ) VERBOSE=true ; shift   ;;
    -d | --debug   ) DEBUG=true   ; shift   ;;
    -m | --memory  ) MEMORY="$2"  ; shift 2 ;;
    --             ) shift        ; AOPT=$@  ;  break ;;
    *              ) break                  ;;
  esac
done

echo """
  VERBOSE       : ${VERBOSE}
  DEBUG         : ${DEBUG}
  MEMORY        : ${MEMORY}
  AOPT          : ${AOPT}
"""

# example
$ ./param.sh -v -m '256Gi' -- --author 'marslo'
  VERBOSE       : true
  DEBUG         : false
  MEMORY        : 256Gi
  AOPT          : --author marslo

$ ./param.sh -v -- -m '256Gi' --author 'marslo'
  VERBOSE       : true
  DEBUG         : false
  MEMORY        :
  AOPT          : -m 256Gi --author marslo

shift with uncertain params

echo '---------------- before shift -------------------'
echo ".. \$# : $#"
echo ".. \$@ : $@"
echo ".. \$* : $*"

echo '---------------- after shift -------------------'
opt=''

while [[ $# -gt 0 ]]; do
  case "$1" in
    -*) opt+="$1 "; shift;;
     *) break            ;;
  esac
done

echo ".. \$#   : $#"
echo ".. \$@   : $@"
echo ".. \$*   : $*"
echo ".. \$opt : $opt"

if [[ 0 = "$#" ]]; then
  echo -e "\033[0;33mERROR: must provide at least one non-opt param\033[0m"
  exit 2
elif [[ 1 = "$#" ]]; then
  path=''
  params="$1"
else
  path=${*: -1}
  params=${*: 1:$#-1}
fi

echo '---------------- result -------------------'
echo ">> opt    : ${opt}"
echo ">> params : ${params}"
echo ">> path   : ${path}"

POSIX getopts Parser

[!NOTE]

  • ✅ Pros: POSIX, built-in
  • ❌ Cons: No long options, no multi-word values
while getopts "f:vd" opt; do
  case $opt in
    f  ) FILE="$OPTARG" ;;
    v  ) VERBOSE=true   ;;
    d  ) DRYRUN=true    ;;
    \? ) echo "Invalid option: -$OPTARG"; exit 1 ;;
  esac
done
shift $((OPTIND -1))
# Reset in case getopts has been used previously in the shell.
OPTIND=1
output_file=''
verbose=0

while getopts "h?vf:" opt; do
  case "$opt" in
    h|\? ) show_help; exit 0   ;;
    v    ) verbose=1           ;;
    f    ) output_file=$OPTARG ;;
  esac
done

shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"

GNU getopt Parser

[!NOTE]

  • MacOS: brew install gnu-getopt

  • ✅ Pros: Short + long, nice user UX

  • ❌ Cons: Not portable across BSD/macOS by default (GNU-only)
#!/usr/bin/env bash

ARGS=$(getopt -o f:vd -l file:,verbose,dry-run -- "$@")
eval set -- "$ARGS"

while true; do
  case "$1" in
    -f|--file    ) FILE="$2"    ;  shift 2 ;;
    -v|--verbose ) VERBOSE=true ;  shift   ;;
    --dry-run    ) DRYRUN=true  ;  shift   ;;
    --           ) shift        ;  break   ;;
    *            ) break                   ;;
  esac
done
# option --output/-o requires 1 argument
LONGOPTS=debug,force,output:,verbose
OPTIONS=dfo:v

! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
  # e.g. return value is 1
  #  then getopt has complained about wrong arguments to stdout
  exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
  case "$1" in
    -d|--debug   ) d=y          ;  shift            ;;
    -f|--force   ) f=y          ;  shift            ;;
    -v|--verbose ) v=y          ;  shift            ;;
    -o|--output  ) outFile="$2" ;  shift 2          ;;
    --           ) shift        ;  break            ;;
    *            ) echo "Programming error"; exit 3 ;;
  esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

bash-argparse Library

[!NOTE]

  • Anvil/bash-argsparse

  • ✅ Pros: Clean, descriptive, built-in help

  • ❌ Cons: Bash-only, external lib needed
# requires sourcing the library
source argparse.sh

argparse "$@" <<EOF
--file -f [arg]    Path to the file
--verbose -v       Enable verbose output
--dry-run          Enable dry-run mode
EOF

docopts

[!NOTE]

  • docopt/docopt

  • ✅ Pros: Elegant, self-documenting

  • ❌ Cons: Requires Python + docopts installed
# usage section (docopts parses this!)
read -r -d '' usage <<EOF
Usage:
  script.sh [-v|--verbose] [--dry-run] -f <file>
Options:
  -f --file=<file>     File to process
  -v --verbose         Verbose output
  --dry-run            Simulate actions
EOF

eval "$(docopts -A args -h "$usage" : "$@")"

# access like $args_file, $args_verbose, $args_dry_run

argbash Code Generator

[!NOTE]

# ARG_POSITIONAL_SINGLE([file], [File to process])
# ARG_OPTIONAL_BOOLEAN([verbose], [v], [Enable verbose])
# ARG_OPTIONAL_BOOLEAN([dry-run], [], [Dry run mode])
# ARG_HELP([Script to demonstrate argbash])
# ARGBASH_GO()

# The above is preprocessed by `argbash` into full Bash script

shflags (Google-style shell option lib)

[!NOTE]

. ./shflags

DEFINE_string 'file' '' 'file to use' 'f'
DEFINE_boolean 'verbose' false 'verbose mode' 'v'
DEFINE_boolean 'dry-run' false 'simulate mode' ''

FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"

echo "File: ${FLAGS_file}"

references

Copyright © marslo 2020-2024 all right reserved,powered by GitbookLast Modified: 2025-04-15 03:38:49

results matching ""

    No results matching ""