I've tried my best to write argument parser to cover all my needs and I won't need to edit the body of parsing loop, never again(:)) to add/remove parsable flags. The resulting code seem to cover's my needs but there are things I don't like about it.
What it does:
It can parse any quantity of positional args, short flags(-f) and long flags(--flags/--flags=) provided at any order, no need to edit parsing loop. All values after flags (short n long) will set the corresponding var value (flagged_args_var_names), variable can have any name, only position matters. e.g.
-u admin --host localhost --default_database=users
echo $user #yields 'admin'
echo $host #yields 'localhost'
echo $default_db #yields 'users'
first_arg another_arg last_arg
posarg[0]=first_arg
posarg[1]=another_arg
posarg[2]=last_arg
Here it is:
# set flags and names of vars to be set [move to worker scipt]
flags_short=() # e.g. -u -h -d
flags_long=() # e.g. --user --host --database
flagged_args_var_names=() # e.g. user/user_name, host, database
posargs=()
function name_of_arg() {
haystack="${@:1:$#-1}";needle="${@: -1}"; i=0
for cur_hs in ${haystack[@]}; do
if [[ "${cur_hs}" == "${needle}" ]]; then echo ${flagged_args_var_names[${i}]} && exit 0; fi
i=$((i+1));
done
echo '' && exit 1;
}
while [ $# -gt 0 ]; do
cur_arg=$1; unset farg_name
if [[ $cur_arg == -* ]]; then # flag
if [[ $cur_arg == --*=* ]];then #--flag=arg_val
cur_val="$(echo $cur_arg|tr "=" "\n"|tail -n 1)"
cur_arg="$(echo $cur_arg|tr "=" "\n"|head -n 1)"
else #-f arg_val or --flag arg_val
cur_val=$2
fi
if [[ $(name_of_arg ${flags_short[@]} "$cur_arg") ]]; then #arg name for -f arg_val
farg_name=$(name_of_arg ${flags_short[@]} "$cur_arg")
elif [[ $(name_of_arg ${flags_long[@]} "$cur_arg") ]]; then #arg name for --flag arg_val/--flag=arg_val
farg_name=$(name_of_arg ${flags_long[@]} "$cur_arg")
fi
if [[ ! -z ${farg_name+x} ]]; then #found arg_name for flag
declare $farg_name="${cur_val}";
if [[ $cur_val == $2 ]]; then shift; fi
else # unknown flag
echo "Illegal option $cur_arg"; exit 1
fi
else # positional arg
posargs+=("$1")
fi
shift || true
done || true
done
check:
# echo "user: $user"
# echo "host: $host"
# echo "database: $database"
# echo "Pos args: ${posargs[@]}"
usage examples (assuming script resides in collect_args.sh file in project folder):
- Backup specified vms to folder
flags_short=(-u -p -h -d) # set flags and names of vars to be set
flags_long=(--user --password --host --destination)
flagged_args_var_names=(user password esx_host destination_path)
posargs=()
. collect_args.sh #that's my arg parser
vm_names=("${posargs[@]}")
for vm_name in ${vm_names[@]}
do
#bad idea, I know
ovftool --powerOffSource vi://${user}:${password}@${esx_host}/${vm_name} ${destination_path}/${vm_name}/${vm_name}.ovf"
done
result
./export_vms_new.sh --user itsme --password blebla456 -d path -h my.esx.srv vm_1 vm_2 vm_win3
ovftool --powerOffSource vi://itsme:[email protected]/vm_1 path/vm_1/vm_1.ovf
ovftool --powerOffSource vi://itsme:[email protected]/vm_2 path/vm_2/vm_2.ovf
ovftool --powerOffSource vi://itsme:[email protected]/vm_win3 path/vm_win3/vm_win3.ovf
- Establish ssh tunnels
flags_short=(-u -h -p -f -s) # set flags and names of vars to be set
flags_long=(--user --host --port --flags --shift)
flagged_args_var_names=(user host remote_port extra_flags port_shift)
posargs=()
. collect_args.sh
remote_hosts=("${posargs[@]}")
# for remote_host in ${remote_hosts[@]}
for i in "${!remote_hosts[@]}"
do
ssh ${extra_flags} $((i+port_shift))043:${host}:${remote_port} ${user}@${remote_hosts[$i]} -N
done
result
./ssh_forward_ports.sh -u me -h localhost --port=443 --flags '-l -F' --shift=6 instance1.gcp instance2.gcp instance3.gcp
ssh -l -F 6043:localhost:443 [email protected] -N
ssh -l -F 7043:localhost:443 [email protected] -N
ssh -l -F 8043:localhost:443 [email protected] -N
Cons:
- It's over bloated, hard to read, hard to modify.
- It doesn't process boolean flags without arg value, e.g. if I'd like to make --powerOffSource from 1st example optional. I need not only to add array, but add different logic to already bloated script. Also the reason to modify arg processing loop.
- Name yours
Problems:
I couldn't use select case, because of mixed up and alternating actions for various types of flags, this decreases script readability drastically.
I was unable to iterate array keys inside name_of_arg function, thus manual counter inc
I couldn't write proper "ternary operator" for the part were script getting arg name for -f and --flag arg_vals and I can't move it to function, because I can't pass array in nested function call.