Without introducing getopts, I'd like to parse expressions like so:
./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"
And variations/combinations thereof:
./cli.sh -file="./path/to/file.txt" data --flags="--a --b" -f i
Unfortunately, I am stuck with reading all optional values of the long options submitted, for example: --flags="--a --b" only returns --a as value for the long option --flags.
I have created a minimalized version of a parser which exhibits my current issue:
#!/usr/bin/env bash
__init_system () {
CMD_SED="sed"
CMD_SED_EXT="sed -E"
}
__init_system_Darwin () {
__init_system
CMD_SED="gsed"
CMD_SED_EXT="gsed -E"
}
invoke_func () {
SYSTEM=$(uname)
FUNC=$1 && shift 1
declare -f ${FUNC}_${SYSTEM} >/dev/null
[ $? -eq 0 ] && { ${FUNC}_${SYSTEM} "$#"; return $?; } || { ${FUNC} "$#"; return $?; }
}
handle_cli_harmonizing () {
cli_adjusted=$(echo "$#" | ${CMD_SED_EXT} \
-e 's#([ ])+# #g'\
-e 's#\<h\>#help#g'\
-e 's#\<e\>#enable#g'\
-e 's#\<d\>#disable#g'\
-e 's#\<i\>#import#g'\
-e 's#\<sl\>#showlog#g'\
-e 's#\<st\>#status#g'\
| tr ' ' '\n' | sort -u | xargs
)
#echo ">>> Original: <$#>"
#echo ">>> Adjusted: <$cli_adjusted>"
}
handle_cli_parsing () {
for param in $#; do
case ${param} in
start|stop|status|enable|disable|data|showlog)
CMD=$param
;;
help|app|stats|import|export|dbinfo|exec)
SPEC=$param
;;
--*|-*)
#echo ">>> Found param: <$param>"
OPT="$OPT $param"
;;
*)
echo ">>> Parsing mismatch: $param"
;;
esac
done
}
handle_cli_optparams () {
for opt in $OPT; do
case "$opt" in
--force|-f)
OPT_FORCE=1
;;
--file=*|-f=*)
OPT_FILE=1
FILE=${opt#*=}
;;
--flags=*)
OPT_FLAGS=1
FLAGS=${opt#*=}
;;
*)
echo ">>> Unknown/unimplemented option specifier: $opt"
;;
esac
done
}
invoke_func __init_system
invoke_func handle_cli_harmonizing "$#"
invoke_func handle_cli_parsing ${cli_adjusted}
invoke_func handle_cli_optparams
echo "DEBUG[ CLI]: CMD=$CMD SPEC=$SPEC"
echo "DEBUG[TOGGLE]: FORCE=$OPT_FORCE FILE=$OPT_FILE FLAGS=$OPT_FLAGS"
echo "DEBUG[ OPTS]: FILE=$FILE FLAGS=$FLAGS"
Expected ouput:
$ ./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"
DEBUG[ CLI]: CMD=data SPEC=import
DEBUG[TOGGLE]: FORCE=1 FILE=1 FLAGS=1
DEBUG[ OPTS]: FILE=./path/to/file.txt FLAGS=--a --b
Current output:
$ ./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"
>>> Unknown/unimplemented option specifier: --b
DEBUG[ CLI]: CMD=data SPEC=import
DEBUG[TOGGLE]: FORCE=1 FILE=1 FLAGS=1
DEBUG[ OPTS]: FILE=./path/to/file.txt FLAGS=--a
It's probably some quoting issue or a missing set, however I seem to be stuck here, and I'd really prefer to keep the simplistic approach I have taken.
I'd be especially stocked if someone would additionally offer a shell-only (no bashims) version for portability concerns, but that's icing on the cake. Mind you, this is only a very simple part of the whole parser I have written; enough to exhibit my current challenge.
I have read all the suggested linked issues while writing this question, and I have pondered for a while on How do I parse command line arguments in Bash?.
Update (2020/09/12): Even though this question has prematurely been downvoted, I found an elegant and more flexible solution, and have posted it as an answer below. It still beats getopt or any other approach I have seen concerning my specific requirements to command line parsing.
The root of the problem is here:
OPT="$OPT $param"
This is where the spaces embedded in $param become indistinguishable from the space used to append to the end of $OPT.
You can avoid this problem by making OPT an array, append values to it correctly:
OPT+=("$param")
... and then use array syntax to iterate over the values of the array:
for opt in "${OPT[#]}"; do
... and so on, consistently, everywhere, and watch out for correct double-quoting.
Even after years of abandonment, I'd like to describe the solution to the question asked above.
While janos gave me the pointer, the solution is actually much simpler and maintains the original flexibility envisioned:
#!/usr/bin/env bash
handle_cli_parsing () {
while [ $# -ne 0 ]; do
param=$1
[ x"$param" = x"sl" ] && param=showlog
[ x"$param" = x"sh" ] && param=showlog
[ x"$param" = x"h" ] && param=help
[ x"$param" = x"i" ] && param=import
[ x"$param" = x"e" ] && param=enable
[ x"$param" = x"d" ] && param=disable
case ${param} in
start|stop|status|enable|disable|data|showlog)
CMD=$param
;;
help|app|stats|import|export|dbinfo|exec)
SPEC=$param
;;
--force|-f)
OPT_FORCE=1
;;
--file=*|-f=*)
OPT_FILE=1
FILE=${param#*=}
;;
--flags=*)
OPT_FLAGS=1
FLAGS=${param#*=}
;;
--*|-*)
echo ">>> New param: <$param>"
;;
*)
echo ">>> Parsing mismatch: $param"
;;
esac
shift 1
done
}
handle_cli_parsing "$#"
echo "DEBUG[ CLI]: CMD=$CMD SPEC=$SPEC"
echo "DEBUG[TOGGLE]: FORCE=$OPT_FORCE FILE=$OPT_FILE FLAGS=$OPT_FLAGS"
echo "DEBUG[ OPTS]: FILE=$FILE FLAGS=$FLAGS"
The output is now as expected and the parsing as flexible as intended:
$ ./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"
DEBUG[ CLI]: CMD=data SPEC=import
DEBUG[TOGGLE]: FORCE=1 FILE=1 FLAGS=1
DEBUG[ OPTS]: FILE=./path/to/file.txt FLAGS=--a --b
Related
I'm trying to make a getopt command such that when I pass the "-ab" parameter to a script,
that script will treat -ab as a single parameter.
#!/bin/sh
args=`getopt "ab":fc:d $*`
set -- $args
for i in $args
do
case "$i" in
-ab) shift;echo "You typed ab $1.";shift;;
-c) shift;echo "You typed a c $1";shift;;
esac
done
However, this does not seem to work. Can anyone offer any assistance?
getopt doesn't support what you are looking for. You can either use single-letter (-a) or long options (--long). Something like -ab is treated the same way as -a b: as option a with argument b. Note that long options are prefixed by two dashes.
i was struggling with this for long - then i got into reading about getopt and getopts
single char options and long options .
I had similar requirement where i needed to have number of multichar input arguments.
so , i came up with this - it worked in my case - hope this helps you
function show_help {
echo "usage: $BASH_SOURCE --input1 <input1> --input2 <input2> --input3 <input3>"
echo " --input1 - is input 1 ."
echo " --input2 - is input 2 ."
echo " --input3 - is input 3 ."
}
# Read command line options
ARGUMENT_LIST=(
"input1"
"input2"
"input3"
)
# read arguments
opts=$(getopt \
--longoptions "$(printf "%s:," "${ARGUMENT_LIST[#]}")" \
--name "$(basename "$0")" \
--options "" \
-- "$#"
)
echo $opts
eval set --$opts
while true; do
case "$1" in
h)
show_help
exit 0
;;
--input1)
shift
empId=$1
;;
--input2)
shift
fromDate=$1
;;
--input3)
shift
toDate=$1
;;
--)
shift
break
;;
esac
shift
done
Note - I have added help function as per my requirement, you can remove it if not needed
That's not the unix way, though some do it e.g. java -cp classpath.
Hack: instead of -ab arg, have -b arg and a dummy option -a.
That way, -ab arg does what you want. (-b arg will too; hopefully that's not a bug, but a shortcut feature...).
The only change is your line:
-ab) shift;echo "You typed ab $1.";shift;;
becomes
-b) shift;echo "You typed ab $1.";shift;;
GNU getopt have --alternative option
-a, --alternative
Allow long options to start with a single '-'.
Example:
#!/usr/bin/env bash
SOPT='a:b'
LOPT='ab:'
OPTS=$(getopt -q -a \
--options ${SOPT} \
--longoptions ${LOPT} \
--name "$(basename "$0")" \
-- "$#"
)
if [[ $? > 0 ]]; then
exit 2
fi
A=
B=false
AB=
eval set -- $OPTS
while [[ $# > 0 ]]; do
case ${1} in
-a) A=$2 && shift ;;
-b) B=true ;;
--ab) AB=$2 && shift ;;
--) ;;
*) ;;
esac
shift
done
printf "Params:\n A=%s\n B=%s\n AB=%s\n" "${A}" "${B}" "${AB}"
$ ./test.sh -a aaa -b -ab=test
Params:
A=aaa
B=true
AB=test
getopt supports long format. You can search SO for such examples.
See here, for example
Here is the code, I want to run this command
Getting the test.log input, then convert it to text type, then store it on the output.txt file
./small.sh test.log -t text -o output.txt
#!/usr/bin/env bash
usage() { echo "$0 usage:" && grep " .)\ #" $0; exit 0; }
[ $# -eq 0 ] && usage
parse()
{
local file=$1
local type=$2
local output=$3
echo "file: ${file}, type: ${type}, output: ${output}"
}
while getopts ":ht:o:" arg; do
case $arg in
t) # specify type.
type=${OPTARG} ;;
o) # specify directory.
output=${OPTARG} ;;
h | *) # Display help.
usage
exit 0
;;
esac
done
shift $(( OPTIND - 1))
parse "${file}" "${type}" "${output}"
with this code i can only get filename when i put it in unsort order like this
./small.sh -t text -o output.txt test.log
How can I get filename and argument with getopts
./small.sh test.log -t text -o output.txt
It is not possible. getopts only supports positional arguments after option arguments.
You can:
accept the limitation
use something elsem, like GNU getopt.
write your own option parsing code
In the following script:
#!/usr/bin/env bash
func_usage ()
{
cat <<EOF \
USAGE: ${0} \
EOF
}
## Defining_Version
version=1.0
## Defining_Input
options=$(getopt -o "t:" -l "h,help,v,version,taxonomy:" -a -- "$#")
eval set -- "$options"
while true;do
case $1 in
-h|--h|-help|--help)
func_usage
exit 0
;;
-v|--v|-version|--version)
echo $version
;;
-t|--t|-taxonomy|--taxonomy)
echo "Option t = $2 ";
Taxonomy_ID=$2
echo $Taxonomy_ID
shift
;;
--)
shift
break;;
esac
shift
done
## Defining Taxonomy Default Value (in case is not provided)
TaxonomyID=${Taxonomy_ID:=9606};
echo $TaxonomyID
exit 0
The commands:
./script.sh -v
./script.sh --v
./script.sh -version
./script.sh --version
Work as expected. But what I do not understand is why the commands:
./script.sh -ver
./script.sh --ver
work at all. An equivalent unexpected behavior is also observed for the commands:
./script.sh -tax 22
./script.sh --tax 22
I would be grateful to get an explanation and/or a way to correct this unexpected behavior.
Note that getopt is an external utility unrelated to Bash.
what I do not understand is why the commands: .. work at all.
Because getopt was designed to support it, there is no other explanation. From man getopt:
[...] Long options may be abbreviated, as long as the abbreviation is not ambiguous.
Unambiguous abbreviations of long options are converted to long options.
Based on the comments I have received, specially from #CharlesDuffy, I have modified my code to what I believe is a more robust and compatible version. Importantly, the code below addresses the pitfalls of the original code
#!/usr/bin/env bash
func_usage ()
{
cat <<EOF
USAGE: ${0}
EOF
## Defining_Version
version=1.0
## Defining_Input
while true;do
case $1 in
-h|--h|-help|--help|-\?|--\?)
func_usage
exit 0
;;
-v|--v|-version|--version)
echo $version
;;
-t|--t|-taxonomy|--taxonomy)
echo "Option t = $2 ";
Taxonomy_ID=$2
echo $Taxonomy_ID
shift
;;
--)
shift
break;;
-?*)
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
;;
*)
break
esac
shift
done
TaxonomyID=${Taxonomy_ID:=9606};
echo $TaxonomyID
exit 0
The code above behaves as expected in that the commands:
./script -tax 22
Gives the warning:
WARN: Unknown option (ignored): -tax
9606
As expected
I have a shell script that I use to launch some ROS launcher file (not that important for my question). My requirement is to use some arguments and therefore I am using getopts.
Basically, I am getting one file or a few, playing them with ROS and simultaneously recording all the files back to a single file. After that, if I have provided an argument -r or -c, I would like to run two additional operations (reindex and compress) on the recorded file. But it is required that the other process are done, before I can run the -r and -c. I am using the wait keyword, but I am not sure I really understand the flow. In other words, the playing and recording should be done and only then -r and -c should be run if provided as arguments.
Second question is related to how do I get or pass the same file that was outputted to these two functions (reindex and compress)?
So my desired format is:
./myscript -i file1 -i file2 -o /home/output -r -c
#!/bin/bash
OUTPUT_FILE_NAME="output_$(date +%Y.%m.%d--%H_%M_%S)"
usage="
$(basename "$0") [-i] [-o] [-r] [-c] [-h] -- to be done"
# Reset is necessary if getopts was used previously in the script.
OPTIND=1
while getopts ":i:orch:" opt; do
case $opt in
i ) echo "INPUT file - argument = $OPTARG"; input_files+=($OPTARG) ;;
o ) echo "OUTPUT dir - argument = $OPTARG"; output_dir=($OPTARG) ;;
r ) echo "REINDEX"; reindex ;;
c ) echo "COMPRESS"; compress ;;
h ) echo "$usage"
graceful_exit ;;
* ) echo "$usage"
exit 1
esac
done
# Shift off the options
shift $((OPTIND-1))
roslaunch myLauncher.launch &
echo "Number of loaded files: ${#input_files[#]}"
echo -n "FILES are:"
rosbag play ${input_files[#]} &
rosbag record -o $output_dir/$OUTPUT_FILE_NAME -a &
wait
function reindex{
rosbag reindex $output_dir/$OUTPUT_FILE_NAME
}
function compress{
rosbag reindex $output_dir/$OUTPUT_FILE_NAME
}
Thank you in advance!
You're very close to where you need to be — and using getopts puts you firmly on the correct track, too. Note whether or not you need to reindex or compress in the option parsing loop. Then, after the music has been played and the output file written, run the code from the functions if you need to:
#!/bin/bash
OUTPUT_FILE_NAME="output_$(date +%Y.%m.%d--%H_%M_%S)"
usage="$(basename "$0") [-i file] [-o file] [-r] [-c] [-h]"
input_files=() # Empty list of files/tracks
output_dir="." # Default output directory
r_flag="no" # No reindex by default
c_flag="no" # No compress by default
while getopts ":i:orch:" opt; do
case $opt in
(i) echo "INPUT file - argument = $OPTARG"; input_files+=("$OPTARG");;
(o) echo "OUTPUT dir - argument = $OPTARG"; output_dir="$OPTARG";;
(r) echo "REINDEX"; r_flag="yes";;
(c) echo "COMPRESS"; c_flag="yes";;
(h) echo "$usage" >&2; exit 0;;
(*) echo "$usage" >&2; exit 1;;
esac
done
# Shift off the options
shift $((OPTIND-1))
if [ $# != 0 ] || [ "${#input_files[#]}" = 0 ] ||
then
echo "$usage" >&2
exit 1
fi
roslaunch myLauncher.launch &
echo "Number of loaded files: ${#input_files[#]}"
echo -n "FILES are:"
rosbag play "${input_files[#]}" &
rosbag record -o "$output_dir/$OUTPUT_FILE_NAME" -a &
wait
if [ "$r_flag" = "yes" ]
then
rosbag reindex "$output_dir/$OUTPUT_FILE_NAME"
fi
if [ "$c_flag" = "yes" ]
then
rosbag compress "$output_dir/$OUTPUT_FILE_NAME"
fi
I didn't keep the functions since they didn't provide any value in the rewritten code.
I want to design a shell script as a wrapper for a couple of scripts. I would like to specify parameters for myshell.sh using getopts and pass the remaining parameters in the same order to the script specified.
If myshell.sh is executed like:
myshell.sh -h hostname -s test.sh -d waittime param1 param2 param3
myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh
myshell.sh param1 -h hostname -d waittime -s test.sh param2 param3
All of the above should be able to call as
test.sh param1 param2 param3
Is it possible to utilize the options parameters in the myshell.sh and post remaining parameters to underlying script?
I wanted to do something similar to the OP, and I found the relevant information I required here and here
Essentially if you want to do something like:
script.sh [options] ARG1 ARG2
Then get your options like this:
while getopts "h:u:p:d:" flag; do
case "$flag" in
h) HOSTNAME=$OPTARG;;
u) USERNAME=$OPTARG;;
p) PASSWORD=$OPTARG;;
d) DATABASE=$OPTARG;;
esac
done
And then you can get your positional arguments like this:
ARG1=${#:$OPTIND:1}
ARG2=${#:$OPTIND+1:1}
More information and details are available through the link above.
myshell.sh:
#!/bin/bash
script_args=()
while [ $OPTIND -le "$#" ]
do
if getopts h:d:s: option
then
case $option
in
h) host_name="$OPTARG";;
d) wait_time="$OPTARG";;
s) script="$OPTARG";;
esac
else
script_args+=("${!OPTIND}")
((OPTIND++))
fi
done
"$script" "${script_args[#]}"
test.sh:
#!/bin/bash
echo "$0 $#"
Testing the OP's cases:
$ PATH+=:. # Use the cases as written without prepending ./ to the scripts
$ myshell.sh -h hostname -s test.sh -d waittime param1 param2 param3
./test.sh param1 param2 param3
$ myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh
./test.sh param1 param2 param3
$ myshell.sh param1 -h hostname -d waittime -s test.sh param2 param3
./test.sh param1 param2 param3
What's going on:
getopts will fail if it encounters a positional parameter. If it's used as a loop condition, the loop would break prematurely whenever positional parameters appear before options, as they do in two of the test cases.
So instead, this loop breaks only once all parameters have been processed. If getopts doesn't recognize something, we just assume it's a positional parameter, and stuff it into an array while manually incrementing getopts's counter.
Possible improvements:
As written, the child script can't accept options (only positional parameters), since getopts in the wrapper script will eat those and print an error message, while treating any argument like a positional parameter:
$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./myshell.sh: illegal option -- a
./test.sh param1 param2 opt1 param3
If we know the child script can only accept positional parameters, then myshell.sh should probably halt on an unrecognized option. That could be as simple as adding a default last case at the end of the case block:
\?) exit 1;;
$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./myshell.sh: illegal option -- a
If the child script needs to accept options (as long as they don't collide with the options in myshell.sh), we could switch getopts to silent error reporting by prepending a colon to the option string:
if getopts :h:d:s: option
Then we'd use the default last case to stuff any unrecognized option into script_args:
\?) script_args+=("-$OPTARG");;
$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./test.sh param1 param2 -a opt1 param3
Mix opts and args :
ARGS=""
echo "options :"
while [ $# -gt 0 ]
do
unset OPTIND
unset OPTARG
while getopts as:c: options
do
case $options in
a) echo "option a no optarg"
;;
s) serveur="$OPTARG"
echo "option s = $serveur"
;;
c) cible="$OPTARG"
echo "option c = $cible"
;;
esac
done
shift $((OPTIND-1))
ARGS="${ARGS} $1 "
shift
done
echo "ARGS : $ARGS"
exit 1
Result:
bash test.sh -a arg1 arg2 -s serveur -c cible arg3
options :
option a no optarg
option s = serveur
option c = cible
ARGS : arg1 arg2 arg3
getopts won't parse the mix of param1 and -n options.
It is much better to put param1-3 into options like others.
Furthermore you can use already existing libraries such as shflags. It is pretty smart and it is easy to use.
And the last way is to write your own function to parse params without getopts, just iterating all params through case construction. It is the hardest way but it is the only way to match your expectations exactly.
I thought up one way that getopts can be extended to truly mix options and positional parameters. The idea is to alternate between calling getopts and assigning any positional parameters found to n1, n2, n3, etc.:
parse_args() {
_parse_args 1 "$#"
}
_parse_args() {
local n="$1"
shift
local options_func="$1"
shift
local OPTIND
"$options_func" "$#"
shift $(( OPTIND - 1 ))
if [ $# -gt 0 ]; then
eval test -n \${n$n+x}
if [ $? -eq 0 ]; then
eval n$n="\$1"
fi
shift
_parse_args $(( n + 1 )) "$options_func" "$#"
fi
}
Then in the OP's case, you could use it like:
main() {
local n1='' n2='' n3=''
local duration hostname script
parse_args parse_main_options "$#"
echo "n1 = $n1"
echo "n2 = $n2"
echo "n3 = $n3"
echo "duration = $duration"
echo "hostname = $hostname"
echo "script = $script"
}
parse_main_options() {
while getopts d:h:s: opt; do
case "$opt" in
d) duration="$OPTARG" ;;
h) hostname="$OPTARG" ;;
s) script="$OPTARG" ;;
esac
done
}
main "$#"
Running it shows the output:
$ myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh
n1 = param1
n2 = param2
n3 = param3
duration = waittime
hostname = hostname
script = test.sh
Just a proof of concept, but maybe it's useful to someone.
Note: there's a gotcha if one function that uses parse_args calls another function that uses parse_args and the outer function declares e.g. local n4='', but the inner one doesn't and 4 or more positional parameters are passed to the inner function
Just mashed up a quickie, which easily handles a mixture of options and positional-parameters (leaving only positional-params in $#):
#!/bin/bash
while [ ${#} -gt 0 ];do OPTERR=0;OPTIND=1;getopts "p:o:hvu" arg;case "$arg" in
p) echo "Path: [$OPTARG]" ;;
o) echo "Output: [$OPTARG]" ;;
h) echo "Help" ;;
v) echo "Version" ;;
\?) SET+=("$1") ;;
*) echo "Coding error: '-$arg' is not handled by case">&2 ;;
esac;shift;[ "" != "$OPTARG" ] && shift;done
[ ${#SET[#]} -gt 0 ] && set "" "${SET[#]}" && shift
echo -e "=========\nLeftover (positional) parameters (count=$#) are:"
for i in `seq $#`;do echo -e "\t$i> [${!i}]";done
Sample output:
[root#hots:~]$ ./test.sh 'aa bb' -h -v -u -q 'cc dd' -p 'ee ff' 'gg hh' -o ooo
Help
Version
Coding error: '-u' is not handled by case
Path: [ee ff]
Output: [ooo]
=========
Leftover (positional) parameters (count=4) are:
1> [aa bb]
2> [-q]
3> [cc dd]
4> [gg hh]
[root#hots:~]$
Instead of using getopts, you can directly implement your own bash argument parser. Take this as a working example. It can handle simultaneously name and position arguments.
#!/bin/bash
function parse_command_line() {
local named_options;
local parsed_positional_arguments;
yes_to_all_questions="";
parsed_positional_arguments=0;
named_options=(
"-y" "--yes"
"-n" "--no"
"-h" "--help"
"-s" "--skip"
"-v" "--version"
);
function validateduplicateoptions() {
local item;
local variabletoset;
local namedargument;
local argumentvalue;
variabletoset="${1}";
namedargument="${2}";
argumentvalue="${3}";
if [[ -z "${namedargument}" ]]; then
printf "Error: Missing command line option for named argument '%s', got '%s'...\\n" "${variabletoset}" "${argumentvalue}";
exit 1;
fi;
for item in "${named_options[#]}";
do
if [[ "${item}" == "${argumentvalue}" ]]; then
printf "Warning: Named argument '%s' got possible invalid option '%s'...\\n" "${namedargument}" "${argumentvalue}";
exit 1;
fi;
done;
if [[ -n "${!variabletoset}" ]]; then
printf "Warning: Overriding the named argument '%s=%s' with '%s'...\\n" "${namedargument}" "${!variabletoset}" "${argumentvalue}";
else
printf "Setting '%s' named argument '%s=%s'...\\n" "${thing_name}" "${namedargument}" "${argumentvalue}";
fi;
eval "${variabletoset}='${argumentvalue}'";
}
# https://stackoverflow.com/questions/2210349/test-whether-string-is-a-valid-integer
function validateintegeroption() {
local namedargument;
local argumentvalue;
namedargument="${1}";
argumentvalue="${2}";
if [[ -z "${2}" ]];
then
argumentvalue="${1}";
fi;
if [[ -n "$(printf "%s" "${argumentvalue}" | sed s/[0-9]//g)" ]];
then
if [[ -z "${2}" ]];
then
printf "Error: The %s positional argument requires a integer, but it got '%s'...\\n" "${parsed_positional_arguments}" "${argumentvalue}";
else
printf "Error: The named argument '%s' requires a integer, but it got '%s'...\\n" "${namedargument}" "${argumentvalue}";
fi;
exit 1;
fi;
}
function validateposisionaloption() {
local variabletoset;
local argumentvalue;
variabletoset="${1}";
argumentvalue="${2}";
if [[ -n "${!variabletoset}" ]]; then
printf "Warning: Overriding the %s positional argument '%s=%s' with '%s'...\\n" "${parsed_positional_arguments}" "${variabletoset}" "${!variabletoset}" "${argumentvalue}";
else
printf "Setting the %s positional argument '%s=%s'...\\n" "${parsed_positional_arguments}" "${variabletoset}" "${argumentvalue}";
fi;
eval "${variabletoset}='${argumentvalue}'";
}
while [[ "${#}" -gt 0 ]];
do
case ${1} in
-y|--yes)
yes_to_all_questions="${1}";
printf "Named argument '%s' for yes to all questions was triggered.\\n" "${1}";
;;
-n|--no)
yes_to_all_questions="${1}";
printf "Named argument '%s' for no to all questions was triggered.\\n" "${1}";
;;
-h|--help)
printf "Print help here\\n";
exit 0;
;;
-s|--skip)
validateintegeroption "${1}" "${2}";
validateduplicateoptions g_installation_model_skip_commands "${1}" "${2}";
shift;
;;
-v|--version)
validateduplicateoptions branch_or_tag "${1}" "${2}";
shift;
;;
*)
parsed_positional_arguments=$((parsed_positional_arguments+1));
case ${parsed_positional_arguments} in
1)
validateposisionaloption branch_or_tag "${1}";
;;
2)
validateintegeroption "${1}";
validateposisionaloption g_installation_model_skip_commands "${1}";
;;
*)
printf "ERROR: Extra positional command line argument '%s' found.\\n" "${1}";
exit 1;
;;
esac;
;;
esac;
shift;
done;
if [[ -z "${g_installation_model_skip_commands}" ]];
then
g_installation_model_skip_commands="0";
fi;
}
You would call this function as:
#!/bin/bash
source ./function_file.sh;
parse_command_line "${#}";
Usage example:
./test.sh as 22 -s 3
Setting the 1 positional argument 'branch_or_tag=as'...
Setting the 2 positional argument 'skip_commands=22'...
Warning: Overriding the named argument '-s=22' with '3'...
References:
example_installation_model.sh.md
Checking for the correct number of arguments
https://unix.stackexchange.com/questions/129391/passing-named-arguments-to-shell-scripts
An example of how to use getopts in bash
There are some standards for unix option processing, and in shell programming, getopts is the best way of enforcing them. Almost any modern language (perl, python) has a variant on getopts.
This is just a quick example:
command [ options ] [--] [ words ]
Each option must start with a dash, -, and must consist of a single character.
The GNU project introduced Long Options, starting with two dashes --,
followed by a whole word, --long_option. The AST KSH project has a getopts that also supports long options, and long options starting with a single dash, -, as in find(1) .
Options may or may not expect arguments.
Any word not starting with a dash, -, will end option processing.
The string -- must be skipped and will end option processing.
Any remaining arguments are left as positional parameters.
The Open Group has a section on Utility Argument Syntax
Eric Raymond's The Art of Unix Programming has a chapter on traditional unix choices for option letters and their meaning.
You can try this trick: after while loop with optargs, just use this snippet
#shift away all the options so that only positional agruments
#remain in $#
for (( i=0; i<OPTIND-1; i++)); do
shift
done
POSITIONAL="$#"
However, this approach has a bug:
all the options after the first positional argument are ingored by getopts and are considered as positional arguments - event those that are correct (see sample output: -m and -c are among positional arguments)
Maybe it has even more bugs...
Look at the whole example:
while getopts :abc opt; do
case $opt in
a)
echo found: -a
;;
b)
echo found: -b
;;
c)
echo found: -c
;;
\?) echo found bad option: -$OPTARG
;;
esac
done
#OPTIND-1 now points to the first arguments not beginning with -
#shift away all the options so that only positional agruments
#remain in $#
for (( i=0; i<OPTIND-1; i++)); do
shift
done
POSITIONAL="$#"
echo "positional: $POSITIONAL"
Output:
[root#host ~]# ./abc.sh -abc -de -fgh -bca haha blabla -m -c
found: -a
found: -b
found: -c
found bad option: -d
found bad option: -e
found bad option: -f
found bad option: -g
found bad option: -h
found: -b
found: -c
found: -a
positional: haha blabla -m -c