Author: jv
License: The MIT License, Copyright (c) 2007 jv
Description: a command line option parser for use in bash scripts (Mac OS X); an alternative to getopts
Usage: /path/to/script_with_cmdparser -a -b -c -f file -d dir (example)
Hints: add security features as needed (such as resetting the $PATH & $IFS variables, using full command paths, etc.; cf. Shell scripting 101: Part 6)
#!/bin/bash # cmdparser # 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)" # 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: $(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 # flags declare cc zz a b c # switches #declare -i cc=0 zz=0 a=0 b=0 c=0 declare optstr flagvar argvar argvar_escaped pipedstr piped 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 # if /dev/stdin has a size greater than zero ... if [[ -s /dev/stdin ]]; then pipedstr="$("; else pipedstr=""; fi if [[ $# -eq 0 ]] && [[ -z "$(echo $pipedstr)" ]]; then echo "No arguments specified!" echo >&2 "$usage" exit 1 fi if [[ $# -eq 0 ]] && [[ -n "$(echo $pipedstr)" ]]; then eval $piped='"$(echo "$pipedstr")"' pipedvar=1 fi # if there are command line arguments ... if [[ $pipedvar -eq 0 ]]; then optstr=" " optid=0 while [[ -n "$optstr" ]]; do # try to extract valid flags or switches from $1 #echo "\$1: $1" optstr="$(echo "$1" | grep -E "^\-\-?$flags$")" if [[ -n "$optstr" ]]; then optid=1; fi if [[ -z "$optstr" ]]; then optid=2; optstr="$(echo "$1" | grep -E "^\-\-?$switches$")"; fi if [[ -z "$optstr" ]]; then optid=3; optstr="$(echo "$1" | grep -E "^\-\-?$switches+$")"; fi if [[ -z "$optstr" ]]; then optid=4; optstr="$(echo "$1" | grep -E "^(\-\-?$flags\=.*|\-\-?$flags[^ ]+)$")"; fi if [[ -z "$optstr" ]]; then if [[ "$1" = "-" ]] && [[ "$@" = "-" ]]; then optid=5 optstr="-" fi fi if [[ -z "$optstr" ]] && ( [[ -n "$(echo "$@" | grep -Eo "(^ *| )\-\-?$flags" )" ]] || [[ -n "$(echo "$@" | grep -Eo "(^ *| )\-\-?$switches" )" ]] ); then optstr="$(echo $1)" echo "illegal option removed: $optstr" shift continue #optstr=" " #echo >&2 "$usage" #exit 1 fi #echo -e "optstr:\t$optstr" #echo -e "optid:\t$optid" #echo -e "piped:\t$piped" if [[ "$1" = "--" ]]; then shift; break; fi # -- marks end of options if [[ -z "$optstr" ]]; then break; fi # flag followed by space (example: -f file) if [[ $optid -eq 1 ]]; then if [[ -z "$2" ]]; then echo; echo "flag \"$1\" removed: no argument given!" shift if [[ -n "$(echo "$@")" ]]; then shift; continue; else shift; break; fi #echo >&2 "$usage" #exit 1 fi # make sure flag $1 is not directly followed by yet another flag or switch if [[ "$2" = "-" ]] || [[ "$2" = "--" ]] || [[ -n "$(echo "$2" | grep -E "^\-\-?$flags.*$" )" ]] || [[ -n "$(echo "$2" | grep -E "^\-\-?$switches+$" )" ]]; then echo; echo "flag \"$1\" removed because of illegal argument: \"$2\"" shift continue #echo >&2 "$usage" #exit 1 fi flagvar="$(expr "$1" : "^\-\{1,2\}\(.*\)$")" argvar="$2" eval $flagvar='"$(echo "$argvar")"' shift 2 continue # single switch (example: -a) elif [[ $optid -eq 2 ]]; then flagvar="$(expr "$1" : "^\-\{1,2\}\(.*\)$")" eval $flagvar='"$(echo "1")"' shift continue # combined switch (example: -abcc) elif [[ $optid -eq 3 ]]; then flagvar="$(expr "$1" : "^\-\{1,2\}\(.*\)$")" while [[ -n "$flagvar" ]]; do char="$(echo "$flagvar" | sed -E "s/^$switches.*$/\1/")" eval $char='"$(echo "1")"' flagvar="$(echo "$flagvar" | sed -E "s/^$switches//")" done shift continue # flag without following space (example: -ffile) elif [[ $optid -eq 4 ]]; then argvar="$(echo "$1" | sed -E "s/^\-\-?$flags\=?//")" argvar_escaped="$(echo "$argvar" | sed -E 's/([^[:alnum:]])/\\\1/g')" # escape special regex metacharacters such as ., ?, * #argvar_escaped="$(echo "$argvar" | sed -E 's/([[:punct:]])/\\\1/g')" #echo "argvar_escaped: $argvar_escaped" flagvar="$(echo "$1" | sed -E -e 's/^-\-?//' -e "s/\=?$argvar_escaped$//")" eval $flagvar='"$(echo "$argvar")"' shift continue # the special read-from-stdin switch "-" elif [[ $optid -eq 5 ]]; then pipedvar=1 eval $piped='"$(echo "$pipedstr")"' shift break fi # remove $1 from "$@" shift done fi # if [[$pipedvar -eq 0 ]]; then ... echo echo -e "a:\t$a" echo -e "b:\t$b" echo -e "c:\t$c" echo -e "cc:\t$cc" echo -e "zz:\t$zz" echo -e "flag1:\t$flag1" echo -e "flag2:\t$flag2" echo -e "flag3:\t$flag3" echo -e "flag4:\t$flag4" echo -e "flag5:\t$flag5" echo -e "flag6:\t$flag6" echo -e "flag7:\t$flag7" echo -e "flag8:\t$flag8" echo if [[ $pipedvar -eq 1 ]] && [[ -z "$(echo $@)" ]]; then echo "remaining string-piped: $piped"; else echo "remaining string: $@"; fi echo exit 0
Test cases:
touch ~/Desktop/cmdparser; chmod +x ~/Desktop/cmdparser
1.
~/Desktop/cmdparser -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!' filename1 filename2 filename3
Output:
a: 1
b: 1
c: 1
cc: 1
zz: 1
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!
remaining string: filename1 filename2 filename3
2.
echo filename | ~/Desktop/cmdparser -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!' -
Output:
a: 1
b: 1
c: 1
cc: 1
zz: 1
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!
remaining string-piped: filename