Wednesday, July 2, 2014

A bash example of processing both short and long command line arguments without using getopts or getopt

Processing command line arguments from bash getopts is limited to single character flags.  When writing scripts, I find it useful and/or necessary to use longer flag names both with and without arguments.

The external command getopt works, but its got some issues with quoted arguments.  Plus, it creates an external dependency that may or may not be on the system.

The easiest way to process more complex command line arguments from within bash is this way ... read the script comments for more details.  Some test cases and example usage follows.

--cut getops-alternative--
#!/bin/bash

# joseph.tingiris@gmail.com

# The bash built in 'getopts' does not allow the parsing of long command line
# arguments.  However, it is possible to parse and process both short and long
# command line arguments using a for loop combined with shifted case statements
# and contingencies.
#
# This shell script provides a working example of how to use this alternative
# from within bash (sh, or ksh).

# if there are no arguments, echo a usage message and/or exit
if [ $# -eq 0 ]; then echo "usage: $0 [-a <argument>|-b|--a-long <argument>|--b-long]"; exit 1; fi

# because the arguments get shifted each time, make sure to set and use a previously declared variable
declare -i total_arguments=$#

# for each command line argument, evaluate them case by case, process them, and shift to the next
for ((i=1; i <= $total_arguments; i++))
do
 arguments=0
 case "$1" in
 -a)
  # short -a flag, requires an argument
  arguments=1
  A_FLAG=1
  A_FLAG_VALUE="$2"
  if [ "$A_FLAG_VALUE" != "" ] && [ ${A_FLAG_VALUE:0:1} == "-" ]; then 
   echo "warning, argument value looks like a flag!"; 
  fi
  echo "-a flag value = '$A_FLAG_VALUE'"
  shift # do an extra shift for flags with arguments
  ;;
 -b)
  # short -b flag, no argument required
  B_FLAG=1
  echo "-b flag was set"
  ;;
 -a-long | --a-long)
  # long --a-flag, requires an argument
  arguments=1
  A_LONG_FLAG=1
  A_LONG_FLAG_VALUE="$2"
  if [ "$A_LONG_FLAG_VALUE" != "" ] && [ ${A_LONG_FLAG_VALUE:0:1} == "-" ]; then 
   echo "warning, argument value looks like a flag!"; 
  fi
  echo "--a-long value = '$A_LONG_FLAG_VALUE'"
  shift # do an extra shift for flags with arguments
  ;;
 -b-long | --b-long)
  # short --b-flag, no argument required
  B_LONG_FLAG=1
  echo "--b-long was set"
  ;;
 *)
  # unknown flags
  if [ $arguments -eq 0 ] && [ "$1" != "" ]; then
   echo "unknown flag '$1'"
   exit 2 # not absolutely necessary, but does enforce proper usage
  fi
  ;;
 esac
 shift
done

# HTH ... The end.

exit 0
--cut getops-alternative--

Here's my output from the above script ...

No arguments given displays a basic usage banner.  Of course, a more elaborate usage function could be created and called instead.

$ getopts-alternative 
usage: /usr/local/sbin/getopts-alternative [-a <argument>|-b|--a-long <argument>|--b-long]

A flag given that requires an argument, but none is given.

$ getopts-alternative -a
-a flag value = ''

A flag given that requires an arugment, none is given, and another flag is interpreted as its argument.  It's a corner case that may happen in practice and is not completely unavoidable.  So for this example I simply put in a warning message.  Discretionary, safer handling could be performed ...

$ getopts-alternative -a -b
warning, argument value looks like a flag!
-a flag value = '-b'

A valid flag with an argument and a valid flag without an argument.

$ getopts-alternative -a 'hello world' -b
-a flag value = 'hello world'
-b flag was set

Valid flags and an invalid flag.

$ getopts-alternative -a 'hello world' -b -c
-a flag value = 'hello world'
-b flag was set
unknown flag '-c'

Similar to the first case, but with a valid long flag that requires an argument (that isn't given).  It also contains an unknown flag (and the unknown flag case had the exit commented out).

$ getopts-alternative -a 'hello world' -b -c --a-long
-a flag value = 'hello world'
-b flag was set
unknown flag '-c'
--a-long value = ''

Again, a long flag is given that requires an argument that also isn't given.  So, the next flag is interpreted as an argument and a warning is produced.

$ getopts-alternative -a 'hello world' -b -c --a-long --b-long
-a flag value = 'hello world'
-b flag was set
unknown flag '-c'
warning, argument value looks like a flag!
--a-long value = '--b-long'

The only thing 'wrong' here is that an unknown flag was given and it didn't exit.

$ getopts-alternative -a 'hello world' -b -c --a-long 'never forget' --b-long
-a flag value = 'hello world'
-b flag was set
unknown flag '-c'
--a-long value = 'never forget'
--b-long was set

Multiple unknown flags, exit commented out.

$ getopts-alternative -a 'hello world' -b -c --a-long 'never forget' --b-long -d
-a flag value = 'hello world'
-b flag was set
unknown flag '-c'
--a-long value = 'never forget'
--b-long was set
unknown flag '-d'

A minor change, testing '-a-long' instead of '--a-long' to ensure multiple names can be used for the same case.

$ getopts-alternative -a 'hello world' -b -c -a-long 'never forget' --b-long -d
-a flag value = 'hello world'
-b flag was set
unknown flag '-c'
--a-long value = 'never forget'
--b-long was set
unknown flag '-d'

Various flags given out of order.

$ getopts-alternative -b -c -a-long 'hello world' -a 'never forget' 
-b flag was set
unknown flag '-c'
--a-long value = 'hello world'
-a flag value = 'never forget'