3
\$\begingroup\$

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):

  1. 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:blebla456@my.esx.srv/vm_1 path/vm_1/vm_1.ovf
ovftool --powerOffSource vi://itsme:blebla456@my.esx.srv/vm_2 path/vm_2/vm_2.ovf
ovftool --powerOffSource vi://itsme:blebla456@my.esx.srv/vm_win3 path/vm_win3/vm_win3.ovf
  1. 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 me@instance1.gcp -N
ssh -l -F 7043:localhost:443 me@instance2.gcp -N
ssh -l -F 8043:localhost:443 me@instance3.gcp -N

Cons:

  1. It's over bloated, hard to read, hard to modify.
  2. 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.
  3. Name yours

Problems:

  1. I couldn't use select case, because of mixed up and alternating actions for various types of flags, this decreases script readability drastically.

  2. I was unable to iterate array keys inside name_of_arg function, thus manual counter inc

  3. 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.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I don't have time right now for a full review, but I spotted this minor oversight:

      echo "Illegal option $cur_arg"; exit 1

I think that ought to be echo >&2. We should always use the standard error stream for error messages, rather than the standard output stream.

\$\endgroup\$
1
  • \$\begingroup\$ Hi, tanks for the tip! \$\endgroup\$
    – Vsevolod
    Apr 21 at 18:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.