cmdparser - parse command line arguments with Bash built-in regex matching
Author: jv
License: The MIT License, Copyright (c) 2009 jv
Description: a basic regex-based command line parser for use in Bash scripts (Mac OS X); uses Bash built-in regex matching via the comparison operator "=~" and the array variable BASH_REMATCH; an alternative to the built-in getopts command (cf. help getopts); use at your own risk
Bash version: GNU bash, version 3.2.25(1)-release
Usage: /path/to/script_with_cmdparser -a -b -c -f file
Related links:
- cmdparser - parse command line arguments (links)
- Process positional parameters non-destructively in Bash
- Update your Bash shell via MacPorts
Version 1: modifies (the number of) command line arguments ($# and $@).
Version 2: does not modify (the number of) command line arguments ($# and $@).
License: The MIT License, Copyright (c) 2009 jv
Description: a basic regex-based command line parser for use in Bash scripts (Mac OS X); uses Bash built-in regex matching via the comparison operator "=~" and the array variable BASH_REMATCH; an alternative to the built-in getopts command (cf. help getopts); use at your own risk
Bash version: GNU bash, version 3.2.25(1)-release
Usage: /path/to/script_with_cmdparser -a -b -c -f file
Related links:
- cmdparser - parse command line arguments (links)
- Process positional parameters non-destructively in Bash
- Update your Bash shell via MacPorts
Version 1: modifies (the number of) command line arguments ($# and $@).
#!/opt/local/bin/bash # bash --version # GNU bash, version 3.2.25(1)-release # This version of cmdparser uses Bash built-in regular expression matching and # modifies (the number of) command line arguments ($# and $@). export PATH=/usr/bin:/bin:/usr/sbin:/sbin export IFS=$' \t\n' # create a fake command line #set -- -abcc -c -zz -flag1="" -flag2=arg -flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' -flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ ! ' -flag9 ~/Desktop/*.txt filename1 filename2 filename3 set -- -abcc -c -zz -flag1="" -flag2=arg -flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' -flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ ! ' -flag9 '~/Desktop/*.txt' filename1 filename2 filename3 printf "%s\n" "$@" | nl #printf "%s" "$@"$'\n' | nl #printf "%s" "${@/%/ }" | nl : <<-'COMMENT' # copy & paste examples for the command line echo "filename1" "filename2" "filename3" | ~/Desktop/cmdparser.txt -abcc -c -zz -flag1 arg -flag2=arg --flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' --flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ ! ' -flag9 ~/Desktop/*.txt - echo "filename1" "filename2" "filename3" | ~/Desktop/cmdparser.txt -abcc -c -zz -flag1 arg -flag2=arg --flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' --flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ ! ' -flag9 '~/Desktop/*.txt' - COMMENT # cmdparser usage="usage: $(/usr/bin/basename "$0") [-a] [-b] [-c] [-cc] [-zz] [-flag1 arg] [-flag2 'arg1 arg2 ...'] [-flag3=arg] [-flag4=\"arg1 arg2\"] ..." # Note: names of flags and switches may not contain the characters "-", "." or "=". # define the names of flags as a regular expression # flags are command line options that require arguments flags="(flag1|flag2|flag3|flag4|flag5|flag6|flag7|flag8|flag9)" # define the names of switches as a regular expression # Switches are command line options that do not take arguments. # Make sure multi-char switches precede single-char switches in the regular expression. # Note that the regular expression contains neither the special read-from-stdin switch "-" # nor the special end-of-options switch "--". switches="(cc|zz|a|b|c)" declare flag1 flag2 flag3 flag4 flag5 flag6 flag7 flag8 flag9 # flags declare -i a=0 b=0 c=0 cc=0 zz=0 # switches declare argstr argvar argvar_escaped char flagvar optstr piped pipedstr # script variables declare -i optid pipedvar # piped="piped" will be used for variable creation # example: piped="piped"; pipedstr="piped arg"; eval $piped='"$(echo "$pipedstr")"'; echo "$piped" piped="piped" # default value is set to "no pipe" pipedvar=0 pipedstr="" # if /dev/stdin has a size greater than zero ... if [[ -s /dev/stdin ]]; then pipedstr="$(</dev/stdin)"; fi if [[ $# -eq 0 ]] && [[ -z "$pipedstr" ]]; then printf "\n%s\n\n%s\n\n" 'No arguments specified!' "$usage" 1>&2 exit 1 fi if [[ $# -eq 0 ]] && [[ -n "$pipedstr" ]]; then eval $piped='"${pipedstr}"' pipedvar=1 fi # if there are command line arguments ... # Note that $pipedvar may still be set to 1 below if the special read-from-stdin switch "-" is given. if [[ $pipedvar -eq 0 ]]; then optstr=" " optid=0 while [[ -n "$optstr" ]]; do # try to extract valid flags or switches from positional parameter $1 # $1 gets shifted afterwards (cf. help shift) [[ "$1" =~ ^--?${flags}$ ]] optstr="${BASH_REMATCH[0]}" if [[ -n "$optstr" ]]; then optid=1; fi if [[ -z "$optstr" ]]; then optid=2; [[ "$1" =~ ^--?${switches}$ ]]; optstr="${BASH_REMATCH[0]}"; fi if [[ -z "$optstr" ]]; then optid=3; [[ "$1" =~ ^--?${switches}+$ ]]; optstr="${BASH_REMATCH[0]}"; fi if [[ -z "$optstr" ]]; then optid=4; [[ "$1" =~ ^--?(${flags}=.*|${flags}[^[:space:]]+)$ ]]; optstr="${BASH_REMATCH[0]}"; fi if [[ -z "$optstr" ]]; then if [[ "$1" = "-" ]] && [[ "$@" = "-" ]]; then optid=5 optstr="-" fi fi if [[ -z "$optstr" ]]; then # append a space to each command line argument argstr="$(printf "%s" "${@/%/ }")" [[ "$argstr" =~ [[:space:]]--?(${flags}|${switches}) ]] if [[ -n "${BASH_REMATCH[0]}" ]]; then printf "\n%s\x21\n\n%s\n\n%s\n\n" "Undefined non-option string: ${1:0:1000} is followed by a legal flag or switch" "${BASH_REMATCH[0]}" "$usage" 1>&2 exit 1 fi fi if [[ "$1" = "--" ]]; then shift; break; fi # -- marks end of options if [[ -z "$optstr" ]]; then break; fi # no further flags or switches to process # flag followed by space (example: -f file) if [[ $optid -eq 1 ]]; then if [[ -z "$2" ]]; then printf "%s\n%s\n" "no argument given to flag: ${1}" "$usage" 1>&2 exit 1 fi flagvar="${1#"${1%%[!-]*}"}" # remove leading - or -- argvar="$2" eval $flagvar='"${argvar}"' shift 2 # shift positional parameters $1 & $2 (that is, a flag plus its argument) continue # single switch (example: -a) elif [[ $optid -eq 2 ]]; then flagvar="${1#"${1%%[!-]*}"}" eval $flagvar='"1"' shift continue # combined switch (example: -abcc) elif [[ $optid -eq 3 ]]; then flagvar="${1#"${1%%[!-]*}"}" while [[ -n "$flagvar" ]]; do [[ "$flagvar" =~ ^${switches}.*$ ]] char="${BASH_REMATCH[1]}" eval $char='"1"' [[ "$flagvar" =~ ^${switches}(.*)$ ]] flagvar="${BASH_REMATCH[2]}" done shift continue # flag without following space (example: -ffile) elif [[ $optid -eq 4 ]]; then [[ "${1#"${1%%[!-]*}"}" =~ ^${flags}=?(.*)$ ]] argvar="${BASH_REMATCH[2]}" [[ "${1#"${1%%[!-]*}"}" =~ ^${flags}=?.*$ ]] flagvar="${BASH_REMATCH[1]}" # alternative #[[ "${1}" =~ ^--?${flags}=?(.*)$ ]] #argvar="${BASH_REMATCH[2]}" #[[ "${1}" =~ ^--?${flags}=?.*$ ]] #flagvar="${BASH_REMATCH[1]}" eval $flagvar='"${argvar}"' shift continue # the special read-from-stdin switch "-" elif [[ $optid -eq 5 ]]; then pipedvar=1 eval $piped='"${pipedstr}"' shift break fi # remove positional parameter $1 from "$@" shift done fi # if [[$pipedvar -eq 0 ]]; then ... echo printf "%s\t%s\n" "a:" "${a}" printf "%s\t%s\n" "b:" "${b}" printf "%s\t%s\n" "c:" "${c}" printf "%s\t%s\n" "cc:" "${cc}" printf "%s\t%s\n" "zz:" "${zz}" printf "%s\t%s\n" "flag1:" "${flag1}" printf "%s\t%s\n" "flag2:" "${flag2}" printf "%s\t%s\n" "flag3:" "${flag3}" printf "%s\t%s\n" "flag4:" "${flag4}" printf "%s\t%s\n" "flag5:" "${flag5}" printf "%s\t%s\n" "flag6:" "${flag6}" printf "%s\t%s\n" "flag7:" "${flag7}" printf "%s\t%s\n" "flag8:" "${flag8}" printf "%s\t%s\n" "flag9:" "${flag9}" echo if [[ $pipedvar -eq 1 ]] && [[ -z "$@" ]]; then echo "remaining string-piped: ${piped}" else echo "remaining string: ${@}" fi echo if [[ $flag9 == '~/Desktop/*.txt' ]]; then printf "%s\n" ~/Desktop/*.txt | nl; fi echo exit 0
Version 2: does not modify (the number of) command line arguments ($# and $@).
#!/opt/local/bin/bash # bash --version # GNU bash, version 3.2.25(1)-release # This version of cmdparser uses Bash builtin regex matching and # does not modify (the number of) command line arguments ($# and $@). # create a fake command line set -- -abcc -c -zz -flag1="" -flag2=arg$'\n'plus_newline -flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' -flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ !' -flag9 ~/Desktop/*.txt filename1 filename2 filename3 #set -- -abcc -c -zz -flag1="" -flag2=arg$'\n'plus_newline -flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' -flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ !' -flag9 '~/Desktop/*.txt' filename1 filename2 filename3 printf "%s\n" "$@" | nl #printf "%s" "$@"$'\n' | nl #printf "%s" "${@/%/ }" | nl : <<-'COMMENT' # copy & paste examples echo "filename1" "filename2" "filename3" | ~/Downloads/Mac-OS-X-bash-scripts/bash-cmdparser/cmdparser-non-destructive-1.txt -abcc -c -zz -flag1 arg -flag2=arg$'\n'plus_newline --flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' --flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ !' -flag9 ~/Desktop/*.txt - echo "filename1" "filename2" "filename3" | ~/Downloads/Mac-OS-X-bash-scripts/bash-cmdparser/cmdparser-non-destructive-1.txt -abcc -c -zz -flag1 arg -flag2=arg$'\n'plus_newline --flag3="arg" -flag4='arg1=*,arg2=?,arg3=!' -flag5 '(arg1|arg2|arg3)' -flag6 'arg1=ag,arg2=bg,arg3=cg' --flag7 An\ argument\ with\ spaces\! -flag8='Yet another argument with spaces / * + ` \ !' -flag9 '~/Desktop/*.txt' - COMMENT echo echo "Number of positional parameters: ${#}" echo # cmdparser export PATH=/usr/bin:/bin:/usr/sbin:/sbin export IFS=$' \t\n' # Note: names of flags and switches may not contain the characters "-", "." or "=". # define the names of flags as a regular expression # flags are command line options that require arguments flags="(flag1|flag2|flag3|flag4|flag5|flag6|flag7|flag8|flag9)" # define the names of switches as a regular expression # Switches are command line options that do not take arguments. # Make sure multi-char switches precede single-char switches in the regular expression. # Note that the regular expression contains neither the special read-from-stdin switch "-" # nor the special end-of-options switch "--". switches="(cc|zz|a|b|c)" usage="usage: $(/usr/bin/basename "$0") [-a] [-b] [-c] [-cc] [-zz] [-flag1 arg] [-flag2 'arg1 arg2 ...'] [-flag3=arg] [-flag4=\"arg1 arg2\"] ..." declare flag1 flag2 flag3 flag4 flag5 flag6 flag7 flag8 flag9 # flags declare -i a=0 b=0 c=0 cc=0 zz=0 # switches declare argn argstr argvar argvar_escaped char flagvar optstr piped pipedstr # script variables declare -i optid pipedvar declare -a argarslice # piped="piped" will be used for variable creation # example: piped="piped"; pipedstr="piped arg"; eval $piped='"$(echo "$pipedstr")"'; echo "$piped" piped="piped" # default value is set to "no pipe" pipedvar=0 pipedstr="" # if /dev/stdin has a size greater than zero ... if [[ -s /dev/stdin ]]; then pipedstr="$(</dev/stdin)"; fi if [[ $# -eq 0 ]] && [[ -z "$pipedstr" ]]; then printf "\n%s\n\n%s\n\n" 'No arguments specified!' "$usage" 1>&2 exit 1 fi if [[ $# -eq 0 ]] && [[ -n "$pipedstr" ]]; then eval $piped='"${pipedstr}"' pipedvar=1 fi # if there are command line arguments ... # Note that $pipedvar may still be set to 1 below if the special read-from-stdin switch "-" is given if [[ $pipedvar -eq 0 ]]; then optstr=" " optid=0 # processing one positional parameter at a time without modifying $# or $@ # Process positional parameters non-destructively in Bash, http://codesnippets.joyent.com/posts/show/1706 for (( i=1; i <= $#; i++ )); do argn="${@:${i}:1}" # current positional parameter # "${@:(${i}+1):1}": the positional parameter following the current one # "${@:${i}}": all positional parameters starting with the current one [[ "$argn" =~ ^--?${flags}$ ]] optstr="${BASH_REMATCH[0]}" if [[ -n "$optstr" ]]; then optid=1; fi if [[ -z "$optstr" ]]; then optid=2; [[ "$argn" =~ ^--?${switches}$ ]]; optstr="${BASH_REMATCH[0]}"; fi if [[ -z "$optstr" ]]; then optid=3; [[ "$argn" =~ ^--?${switches}+$ ]]; optstr="${BASH_REMATCH[0]}"; fi if [[ -z "$optstr" ]]; then optid=4; [[ "$argn" =~ ^--?(${flags}=.*|${flags}[^[:space:]]+)$ ]]; optstr="${BASH_REMATCH[0]}"; fi if [[ -z "$optstr" ]]; then if [[ "${argn}" = "-" ]] && [[ "${@:${i}}" = "-" ]]; then optid=5 optstr="-" fi fi if [[ -z "$optstr" ]]; then # append a space to each command line argument argarslice=( "${@:${i}}" ) argstr="$(printf "%s" "${argarslice[@]/%/ }")" [[ "$argstr" =~ [[:space:]]--?(${flags}|${switches}) ]] if [[ -n "${BASH_REMATCH[0]}" ]]; then printf "\n%s\x21\n\n%s\n\n%s\n\n" "Undefined non-option string: ${argn:0:1000} is followed by a legal flag or switch" "${BASH_REMATCH[0]}" "$usage" 1>&2 exit 1 fi fi if [[ "${argn}" = "--" ]]; then break; fi # -- marks end of options if [[ -z "$optstr" ]]; then break; fi # no further flags or switches to process # flag followed by space (example: -f file) if [[ $optid -eq 1 ]]; then if [[ -z "${@:(${i}+1):1}" ]]; then printf "%s\n%s\n" "no argument given to flag: ${argn}" "$usage" 1>&2 exit 1 fi flagvar="${argn#"${argn%%[!-]*}"}" # remove leading dashes argvar="${@:(${i}+1):1}" eval $flagvar='"${argvar}"' let "i += 1" # skip argument of current flag in next for loop continue # single switch (example: -a) elif [[ $optid -eq 2 ]]; then flagvar="${argn#"${argn%%[!-]*}"}" eval $flagvar='"1"' continue # combined switch (example: -abcc) elif [[ $optid -eq 3 ]]; then flagvar="${argn#"${argn%%[!-]*}"}" while [[ -n "$flagvar" ]]; do [[ "$flagvar" =~ ^${switches}.*$ ]] char="${BASH_REMATCH[1]}" eval $char='"1"' [[ "$flagvar" =~ ^${switches}(.*)$ ]] flagvar="${BASH_REMATCH[2]}" done continue # flag without following space (example: -ffile) elif [[ $optid -eq 4 ]]; then [[ "${argn#"${argn%%[!-]*}"}" =~ ^${flags}=?(.*)$ ]] argvar="${BASH_REMATCH[2]}" [[ "${argn#"${argn%%[!-]*}"}" =~ ^${flags}=?.*$ ]] flagvar="${BASH_REMATCH[1]}" # alternative #[[ "${argn}" =~ ^--?${flags}=?(.*)$ ]] #argvar="${BASH_REMATCH[2]}" #[[ "${argn}" =~ ^--?${flags}=?.*$ ]] #flagvar="${BASH_REMATCH[1]}" eval $flagvar='"${argvar}"' continue # the special read-from-stdin switch "-" elif [[ $optid -eq 5 ]]; then pipedvar=1 eval $piped='"${pipedstr}"' break fi done # for loop fi # if [[$pipedvar -eq 0 ]]; then ... echo printf "%s\t%s\n" "a:" "${a}" printf "%s\t%s\n" "b:" "${b}" printf "%s\t%s\n" "c:" "${c}" printf "%s\t%s\n" "cc:" "${cc}" printf "%s\t%s\n" "zz:" "${zz}" printf "%s\t%s\n" "flag1:" "${flag1}" printf "%s\t%s\n" "flag2:" "${flag2}" printf "%s\t%s\n" "flag3:" "${flag3}" printf "%s\t%s\n" "flag4:" "${flag4}" printf "%s\t%s\n" "flag5:" "${flag5}" printf "%s\t%s\n" "flag6:" "${flag6}" printf "%s\t%s\n" "flag7:" "${flag7}" printf "%s\t%s\n" "flag8:" "${flag8}" printf "%s\t%s\n" "flag9:" "${flag9}" echo if [[ $pipedvar -eq 1 ]] && [[ -z "$@" ]]; then echo "remaining string-piped: ${piped}" else echo "remaining string: ${@}" fi echo echo "Number of positional parameters: ${#}" echo if [[ $flag9 == '~/Desktop/*.txt' ]]; then printf "%s\n" ~/Desktop/*.txt | nl; fi #if [[ $flag9 == '~/Desktop/*.txt' ]]; then printf "%s\n" ${flag9} | nl; fi echo exit 0