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