Oct 2007

Options Parsing vs Natural(ish) Processing

Most system oriented scripters and programmers tend to rely upon the classic Unix model of options and/or arguments; conversely, many programs and scripts use a subcommand structure that is similar to language processing (or lexical checks). Consider that APIs generally use a lexical style interface - in a sense the natural language idea makes sense if the script or program is going to be called by another program.

The Script

The example script is ultra simplified to make the example easier to follow and done in the bash shell for the same reason. The script creates a directory that must either be:

  • Shared by a particular group or ...
  • Have the GID bit set.

If a shared group is not needed; a default will be assumed. The basic main pseudo-code looks like:


get arguments
parse arguments
setting a group?
  yes 
    make sure the group arg is set
      group arg is set
        create directory 
        set group perms  
      group arg is not set
        error no group
  no
    assume it needs GID set
      create directory using native user group 
      set GID

The assumption is if any of the actions fail - there will be an exception handler.

From the pseudo-code there are several functions that can be extrapolated:

  • A generic directory creation function; possibly one that just takes a few arguments like the groupname and whether or not the GID bit needs to be set.
  • A generic error handler.

Extraordinarily simple for the example; but enough to get one thinking...

The Functions

Excepting the main function; there are at least two sub functions that need to be written before the main is even started.

Bombing out

Systhread of course has the tradition of using the same bomb() routine over and over - why change? :

#---------------------------------------------------------------------
# bomb - bomb and bail
#
# requires: An error message.
# returns : 1
#---------------------------------------------------------------------
bomb()
{
  cat >&2 <<ERRORMESSAGE

ERROR: $@
*** ${PROG} aborted ***
ERRORMESSAGE
  kill ${TOPPID}      # in case we were invoked from a subshell
  exit 1
}
Creating the directory
#---------------------------------------------------------------------
# create_directory - create the directory using specifications
#
# requires: directory name, group name, gid flag
# returns : $?
#---------------------------------------------------------------------
create_directory()
{
  dirname=$1
  groupname=$2
  gidflag=$2

  mkdir -p $dirname ||
    bomb "Cannot create ${dirname} for some reason"
        
  chgrp -R $groupname ||
    bomb "Cannot set ${dirname} to Unix group ${groupname}"

        [ "$gidflag" -gt 0 ] &&
                chmod 2774 $dirname
}

Using Keywords and Options at Once

Examine the shell code below:

#---------------------------------------------------------------------
# Main - 
#  - Default DIRNAME, GRPNAME, SETGID
#  - parse 
#  - call create_directory
#  - exit
#---------------------------------------------------------------------
DIRNAME="nothing" # default is a ficticous group 
GRPNAME="users"   # default is users group       
SETGID=0          # default is false             
while [ "$#" -gt "0" ]
do
  opt="${1//-}"
  opt="${1//-}"
  opt=$(echo "${opt}" | cut -c 1 2>/dev/null)
  case $opt in
    d) shift; DIRNAME=$1;; # Assign a real directory
    g) shift; GRPNAME=$1;; # Assign a group name if needed
    s) SETGID=1;;         # GID flag becomes set
    u) usage;;
    *) usage; exit 1;;
  esac
  shift
done

[ ${DIRNAME} = "nothing" ] &&
        bomb "No directory specified"

create_directory $DIRNAME $GRPNAME $SETGID

exit 0

Example operation, create /depts/pusers owned by the group prod with setgid:

        ezdir -d /dept/pusers -g prod -s

Note how the script will accept any combination matching -*A**... where A is any alphanumeric to match into the case statement such as -d, --d, -dir, --dir, directory and so on will trigger the d) case in. The script is off to a good start, it already is very flexible and has the capability to act naturally. Using the first example operation, here is a more natural example:

        ezdir dir /dept/pusers group prod setgid

The above will end up behaving the same way as the first example.

Adding Fall Through Logic

Some might think ezdir dir pusers group prod setgid is too cumbersome. By using fall through logic the options can be dropped and simple keyword order takes over:

while [ "$#" -gt "0" ]
do
  opt="${1//-}"
  opt="${1//-}"
  opt=$(echo "${opt}" | cut -c 1 2>/dev/null)
  case $opt in
    d) shift; DIRNAME=$1;; # Assign a real directory
    g) shift; GRPNAME=$1;; # Assign a group name if needed
    s) SETGID=1;;          # GID flag becomes set
    u) usage;;
    *) 
      DIRNAME=$1
      GRPNAME=$2
      [ ${GRPNAME} = "setgid" ] &&
        GRPNAME="users"                   
                ;;
  esac
  shift
done

Now the script can operate in one of two modes, options and args or straight up keywords:

   ezdir -g prod -d /dept/pusers -s

... or ...

   ezdir /dept/pusers prod setgid

... and using the default group ...

   ezdir /dept/pusers setgid

Making Sure it is Clear

Of course making sure usage is explained correctly is the important part of the script:

usage()
{
  cat <<_usage_
Usage: ${progname} [options arg][keywords ... ]
Usage: ${progname} [-d|--dir dirname][-g|--group groupname][-s|--set]
Usage: ${progname} [dir dirname][group groupname][setgid]
Usage: ${progname} [dirname groupname][setgid]
Options:
  -d|--dir|dir dirname          Set Directory name.
  -g|--group|group groupname    Set groupname.
  -s|--set|setgid               Set the GID bit on the directory.
Examples:
  Create directory using defaults and no setgid -
    ${progname} -d /shar/foo
    ${progname} /shar/foo
  Create directory using groupname with setgid -
    ${progname} -d /shar/foo -g bars -s
    ${progname} /shar/foo bars  setgid
_usage_
}

Full Listing

#!/bin/bash
# Script -------------------------------------------------------------
# ezdir - Create a directory using default or specified group
#         and/or setgid.
#---------------------------------------------------------------------
progname=${0##*/}

#---------------------------------------------------------------------
# bomb - bomb and bail
#
# requires: An error message.
# returns : 1
#---------------------------------------------------------------------
bomb()
{
  cat >&2 <<ERRORMESSAGE

ERROR: $@
*** ${PROG} aborted ***
ERRORMESSAGE
  kill ${TOPPID}      # in case we were invoked from a subshell
  exit 1
}

#---------------------------------------------------------------------
# create_directory - create the directory using specifications
#
# requires: directory name, group name, gid flag
# returns : $?
#---------------------------------------------------------------------
create_directory()
{
  dirname=$1
  groupname=$2
  gidflag=$2

  mkdir -p $dirname ||
    bomb "Cannot create ${dirname} for some reason"
        
  chgrp -R $groupname ||
    bomb "Cannot set ${dirname} to Unix group ${groupname}"

        [ "$gidflag" -gt 0 ] &&
                chmod 2774 $dirname
}

#---------------------------------------------------------------------
# Usage - Simple usage echo
#---------------------------------------------------------------------
usage()
{
  cat <<_usage_
Usage: ${progname} [options arg][keywords ... ]
Usage: ${progname} [-d|--dir dirname][-g|--group groupname][-s|--set]
Usage: ${progname} [dir dirname][group groupname][setgid]
Usage: ${progname} [dirname groupname][setgid]
Options:
  -d|--dir|dir dirname          Set Directory name.
  -g|--group|group groupname    Set groupname.
  -s|--set|setgid               Set the GID bit on the directory.
Examples:
  Create directory using defaults and no setgid -
    ${progname} -d /shar/foo
    ${progname} /shar/foo
  Create directory using groupname with setgid -
    ${progname} -d /shar/foo -g bars -s
    ${progname} /shar/foo bars  setgid
_usage_
}

#---------------------------------------------------------------------
# Main - 
#  - Default DIRNAME, GRPNAME, SETGID
#  - parse 
#  - call create_directory
#  - exit
#---------------------------------------------------------------------
DIRNAME="nothing" # default is a ficticous group 
GRPNAME="users"   # default is users group       
SETGID=0          # default is false             
while [ "$#" -gt "0" ]
do
  opt="${1//-}"
  opt="${1//-}"
  opt=$(echo "${opt}" | cut -c 1 2>/dev/null)
  case $opt in
    d) shift; DIRNAME=$1;; # Assign a real directory
    g) shift; GRPNAME=$1;; # Assign a group name if needed
    s) SETGID=1;;          # GID flag becomes set
    u) usage;;
    *) 
      DIRNAME=$1
      GRPNAME=$2
      [ ${GRPNAME} = "setgid" ] &&
        GRPNAME="users"                   
  esac
  shift
done

[ ${DIRNAME} = "nothing" ] &&
        bomb "No directory specified"

create_directory $DIRNAME $GRPNAME $SETGID

exit 0

Summary

Mixing and matching command line argument styles isn't too difficult. In the provided example, the operations are pretty simple. It is easy to see how additional methods like using a command-sub_command structure could be used or even lexicals.