More Power with Bash Getopts
Submitted by jonesy on Tue, 2006-12-12 19:29.

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;;


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

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

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


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

if [ $attrs ]; then
ldapsearch -x -LLL "($searchattr)" $attrs
ldapsearch -x -LLL "($searchattr)" givenname sn roomnumber telephonenumber


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!