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 MacPortsVersion 1: 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 $@).
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