October 20, 2004

SysAdmin to SysAdmin: More power with bash getopts

Author: Preston St. Pierre

You can do an enormous amount of work using nothing more than the commands that come with your system, sprinkled with some Bash syntax to
glue them together. But how do you move from the plateau of Bash beginner to reach new heights, and nicknames like "the baron of Bash," "the sheriff of shell county," "prince of the prompt," "sultan of scripting"? A good friend of
mine has even earned the title and nickname "root"! Oh yes, wondrous things await, and one tool the PFY will need along this journey to greatness is the ability to parse command-line flags fed to his scripts using getopts.

Once you learn to parse flags to your scripts, a whole new world opens up. You start approaching problems from a slightly different mindset. Gone are the days of hardcoding values based on one scenario, and changing them for each instance. Forget about editing the same script for slight variations in application, or worse, writing two separate scripts for almost the same exact
thing! This is a sure path to ambiguous script naming, vague documentation, a lack of maintainability, and scripts that are usable only by the author. The whole point of scripting something is to automate it, not to create a
monster that has to be manually edited every time the wind blows!

This is where getopts comes in. getopts, by the way, is not to be confused with the getopt command. No, getopts is built into Bash "to parse command line arguments to a script." However, the practical purpose is to save you from writing all kinds of code to allow for every conceivable special instance that can arise when you pass arguments to a script. Some error handling and variable assignment stuff is
already handled for you by getopts, which makes getting started with it extremely easy. So let's have a look.

I'm going to share a script that I've just started writing. It's not quite what I'd call "advanced," but it serves as a decent example of fairly simple usage. The script is called ldaplist, and it's meant to take the place of the ypmatch and ypcat tools in the NIS environment, and give the end user a bit more control and power at the same time. For
example, since LDAP labels every attribute it stores about a user, we can use
that in searches. For example, typing ldaplist -s roomnumber=1*
returns records for every user on the first floor (well, in my building, anyway -- YMMV.) This would be a tough job, at best, using a NIS map and
ypmatch. Here's the code at the top which sets up the parsing of the flags:

 
while getopts ":u:a:s:v" options; do
  case $options in
    u ) uname=$OPTARG;;
    a ) attrs=$OPTARG;;
    s ) searchattr=$OPTARG;;
    v ) att=ALL;;
    h ) echo $usage;;
    \? ) echo $usage
         exit 1;;
    * ) echo $usage
          exit 1;;

  esac
done

The getopts function takes two arguments. The first is a list of the flags
accepted by the program. This should be in quotes. The colon in front of the accepted flags suppresses some errors that getopts can generate which may or may not be of any value to you (never has been for me). Colons appearing after a flag indicate that getopts should expect an argument with that flag. Using the flag without an argument causes an error:

[jonesy@livid jonesy]$ ./ldaplist -u
./ldaplist: option requires an argument -- u
Usage: ./ldaplist [-h] [-u user] [-a attr1 attr2...] [-s searchattr=value] [-v]
[jonesy@livid jonesy]$

It even spit out my usage statement! This is because it set the value for that flag to "?", which I've accounted for in my case statement. I've seen it called a bug, but really, I'd rather have this fairly predictable
behavior than just spit out that the flag needs an argument, leaving the user
to guess what that argument should look like.

The second argument is the name of the variable where all of the options will be stored. Call it whatever you like. In 90% of
the cases, I find that this variable name is only used once, just where I used it above -- in the case statement, which I almost always use to
parse the flags and assign the arguments to the flags to some variable that I can use later in the script. The arguments to each flag are stored in a
built-in variable called OPTARG.
Since there is an instance of this variable for each flag that takes an argument, the only way to use that argument is to assign it to something that will still be around once the while loop finishes. This is pretty much all I do in my case statement. As I mentioned earlier, the
"?" case takes care of cases where a flag requiring an argument comes in
without one, and the "*" is a catch-all to account for things like non-existent
flags. I've also allowed for -v as a "verbose" flag. In this case, instead of returning just a few attributes for each record returned in a search, this flag causes the entire LDAP object entry to be returned.

Now for s'more code:

if [ $# -eq 0 ]; then
  ldapsearch -x -LLL -s one
fi

Here I'm just allowing for a default behavior. If ldaplist is called with no arguments, it returns the records of the organizationalUnit objects, or whatever is one level down from the actual directory base. This is useful if you're in unfamiliar territory and want to know what type of information you have access to in a given directory.


if [ $uname ]; then
  if [ $attrs ]; then
    echo attrs is "$attrs"
    ldapsearch -x -LLL "(uid=$uname)" $attrs
    exit
  fi
                                                                                
  if [ -z $att ]; then
    ldapsearch -x -LLL "(uid=$uname)" givenname sn roomnumber telephonenumber ui 
    exit
  elif [ $att = "ALL" ]; then
    ldapsearch -x -LLL "(uid=$uname)"
  fi
                                                                                
fi

Above, I first check to see if the user is searching for a username. If he is, then I also want to know if there are particular attributes he'd like back from any matching objects in the directory. If not, then I also check for
the "verbose" flag, which sets the att variable.

As you can now see, the key to this script is the ldapsearch command, and what
I'm really accomplishing here is allowing the user to interface with that command without typing the rather lengthy commands. With funny search criteria, a list of attributes to return, a random LDAP server, credentials, and LDIF
verbosity options, you could (and I have) had ldapsearch commands that look something like this:


ldapsearch -x -W -D"cn=manager,dc=my,dc=domain,dc=org" -h ldap.my.domain.org -b
dc=my,dc=domain,dc=org -s sub -ZZZ -LLL '(&(objectclass=person)(roomnumber=101*)
(|(givenname=Brian)(sn=Jones)))' loginshell telephonenumber

No, really -- that bit in single quotes is a valid LDAP search string. See how
this could be more straightforward?

So that covers simply asking for some basic user information. But to be more
generic, I wanted to have the ability to send along a simple search string that
falls outside the realm of simple user information. Or not! What if I want to
know all of the groups a user is in? This isn't stored in a user's LDAP entry
-- it's stored in what is typically a Group tree, which contains an entry for
each group. Each entry has a list of the users that belong to the group, and
the attribute in the entry that specifies the UID (in a "posixGroup" entry) is memberUID. Here's more code:

if [ $searchattr ]; then
  if [ $uname ]; then
    echo "Use -u to find a username"
    echo $usage
    exit 1
  fi
                                                                                
  if [ $attrs ]; then
    ldapsearch -x -LLL "($searchattr)" $attrs
  else
    ldapsearch -x -LLL "($searchattr)" givenname sn roomnumber telephonenumber
  fi
                                                                                
fi

So, we can send this along to ldaplist:

ldaplist -s memberuid=jonesy

This will return the relative distinguished name (or RDN, which uniquely identifies a record within an entire directory) for each group I belong to.
With this type of flexibility, it doesn't much matter what you store in the
directory, you now have the ability to thoroughly probe the information.

In closing

In this article, I've tried to drive home the notion that writing scripts that use flags and arguments is as easy as it is powerful. I used a simple, but nonetheless real-life (and working!) example script to illustrate the basics of
using Bash's built-in getopts construct. This script is one that
isn't uncommon in administration: an easier interface to difficult-to-remember
commands. Other candidates for simplification might be the snmpwalk or snmpget commands, which can also take
various options and parameters, some of which are incredibly long. How about a
wrapper to nmap that allows you to specify your favorite
set of flags with just one flag (I can never remember the
meanings of all those flags!) The possibilities are endless.

As usual, I've probably omitted some really cool shortcut, or messed something
up. While I did make a couple of concessions for the sake of clarity and
simplicity, shortcuts and tips from the readership are always welcome. I've
learned much by reading comments to my articles, and articles like this in
general, so I always urge you to share your favorite hacks in the comments
here. Enjoy!

Click Here!