Welcome, guest | Sign In | My Account | Store | Cart

The getopt module, like GNU getopt/getopt_long, separates short and long options, and makes no way to coordinate options with usage messages. This leads to an opportunity for usage info and the actual options to diverge, and makes it harder to see, at a glance, what short and long options correspond to one another. It also leaves handling of preset or default options up to ad-hoc solutions, often the same or similar code implemented time and time again. Xgetopt addresses all of these concerns in a simple, easy to use module.This code requires my word_wrap.py module, which is also posted to the Python Cookbook following this module.

Python, 200 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#!/usr/bin/env python
#
# xgetopt.py
#
# Copyright (c) 2001 Alan Eldridge. All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the "Artistic License" which is
# distributed with the software in the file LICENSE.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the Artistic
# License for more details.
#
# Alan Eldridge 2001-09-15 alane@wwweasel.geeksrus.net
#
# $Id: xgetopt.py,v 1.13 2001/10/14 04:57:58 alane Exp $
#
# 2001-09-15 alane@wwweasel.geeksrus.net
#

import os
import sys
import string
import getopt

import word_wrap

class parser:
    """Simple wrapper around getopt to specify short and long options
    together by function, to create a usage message from the options.
    """
    __usage_lwid=30
    __usage_rwid=40
    __usage_width=70

    def __init__(self):
        """Set up empty tables for parser."""
        self.__app = None
        self.__args = None
        self.__note = None
        self.__s = []   # short opts for getopt
        self.__l = []   # long opts for getopt
        self.__olist = []       # list of opt def'ns
        self.__odict = {}       # opt def'ns indexed by opt
        self.__usage = None     # cached option help message

    def set_app(self, app):
        self.__app = app

    def set_args(self, args):
        self.__args = args

    def set_note(self, note):
        self.__note = note

    def add_opt(self, opt, long, arg, key, usage, multi=0):
        """Add a new option to the parser's tables.

        opt     => short option, e.g., '-s', *not* 's' or 's:'.
        long    => long option, e.g., '--spam', *not* '--spam='.
        arg     => descriptive name for arg to option, if it has an
                   an arg, e.g., 'how-much-spam'.
        key     => a unique key to identify this option; see
                   xgetopt.getopt().
        usage   => usage text for this option; may contain references
                   to members of the opt_info dictionary such as %(opt)s
                   and %(arg)s; '\n' is a paragraph separator.
        multi   => if true, multiple values for the option are distinct;
                   if false, only the last value is significant. See
                   xgetopt.getopt() for detail.
        """
        self.__usage = None
        # Build opt def'n dict
        opt_info = {}
        opt_info['opt'] = opt
        opt_info['long'] = long
        opt_info['arg'] = arg
        opt_info['key'] = key
        opt_info['usage'] = usage
        opt_info['multi'] = multi
        # Setup for getopt
        # Make index entries
        if opt:
            self.__odict[opt] = opt_info
            if arg:
                opt = opt + ':'
            self.__s.append(opt[1:])
        if long:
            self.__odict[long] = opt_info
            if arg:
                long = long + '='
            self.__l.append(long[0:])
        # Split usage into paragraphs and substitute vars.
        # This is gonna die a horrible death if the string
        # has bad variable substitutions in it.
        if usage:
            opt_info['usage'] = string.split(usage % opt_info, '\n\n')
        # Add to list of options
        self.__olist.append(opt_info)
        return opt_info

    def getopt(self, args, app_opts = None):
        """Parse string 'args' using saved option info.

        Returns a 3-tuple (opts, args, app_opts). Opts and args
        are the conventional values returned by getopt.getopt().

        App_opts is a dictionary mapping the 'key' given to add_opt()
        for each option to the value present on the command line.

        1. If the app_opts dictionary is passed in, this gives the
        default values for options for the application.

        2. Options not present in a passed-in app_opts, and not present
        on the command line have a mapped value of None.

        2. Options that have the 'multi' flag set have a mapped value
        of a list containing all values for the option that were given
        on the command line, in the order they were given.

        """
        # set up dict for caller
        if app_opts is None:
            app_opts = {}
        for oinfo in self.__olist:
            if not app_opts.has_key(oinfo['key']):
                app_opts[oinfo['key']] = None

        # Do the getopt thing
        opts, args = getopt.getopt(args,
                                   string.join(self.__s, ''),
                                   string.join(self.__l, ' '))

        # now fill in caller dict
        for k, v in opts:
            app_key = self.__odict[k]['key']
            app_multi = self.__odict[k]['multi']
            if app_multi:
                if not app_opts[app_key]:
                    app_opts[app_key] = []
                app_opts[app_key].append(v)
            else:
                app_opts[app_key] = v

        # return 3-tuple to caller
        return opts, args, app_opts

    def usage_msg(self):
        """Generate a help message using saved option info."""
        if self.__usage:
            return self.__usage
        app = self.__app
        if not app:
            app = sys.argv[0].split(os.sep)[-1]
        str = 'Usage: ' + app
        if len(self.__olist):
            str = str + ' [options]'
        if self.__args:
            str = str + ' ' + self.__args
        self.__usage = map(lambda s: s + '\n', str.split('\n'))
        if len(self.__olist):
            self.__usage = self.__usage + ['\n', 'Options:\n']
        # Loop over options
        for oinfo in self.__olist:
            lside = rside = ''
            # Left side is -x,--xthing xarg
            if oinfo['opt']:
                lside = lside + oinfo['opt']
            if oinfo['long']:
                if oinfo['opt']:
                    lside = lside + ','
                lside = lside + oinfo['long']
            if oinfo['arg']:
                lside = lside + ' ' + oinfo['arg']
            lside = [ string.ljust(lside, self.__usage_lwid) ]
            # Right side is usage, word wrapped to fit
            rside = word_wrap.wrap_list(oinfo['usage'], self.__usage_rwid)
            # Pad out extra blank lines on left side
            for i in range(len(rside) - 1):
                lside.append(string.ljust('', self.__usage_lwid))
            # Tack 'em together
            self.__usage = self.__usage +  map(lambda l, r: l + r + '\n',
                                               lside, rside)
        if self.__note:
            self.__usage.append('\n')
            tmp = map(lambda s: s + '\n',
                      word_wrap.wrap_str(self.__note, self.__usage_width, '\n\n'))
            self.__usage = self.__usage + tmp
        return self.__usage

    def usage(self, rc):
        map(sys.stdout.write, self.usage_msg())
        sys.exit(rc)
 
#
#EOF
##
       self.__usage = self.__usage + tmp

Command line parsing is one of those necessary evils that we all dislike, and I imagine that most of us has tried to tackle the beast one or more times over our career. GNU's getopt and getopt_long, and the GNU C Library's argp represent some of the high profile solutions. The popt library, used by RedHat's RPM system, is another highly evolved system. Of course, all of these are targeted at the C/C++ programmer. In "scripting" languages, such as Perl and Python, we either use interfaces to these C facilities, or, as in this case, attempt to manage some of the larger issues of complexity in processing program invocation parameters by building on these implementation-language solutions.

I have tackled this problem in various languages over the 20 years of my career, and each new language I learn to use presents new "opportunities" in this area. Python's support for simple, yet extraordinarily powerful, data structures gave me a new look at an old problem.

Xgetopt is a simple solution, designed to address what I perceive as the most important issues facing the programmer in parsing the command line:

  1. Keeping track of differing ways (long and short options) of specifiying the same information.
  2. Keeping usage information updated, and in sync with the command line options the program actually accepts.
  3. Specifying default values for non-specified options.
  4. Minimizing the impact on other parts of the application by changes in the command line behavior of the application.

Xgetopt tries to follow the approach of doing the simplest thing that will work. I went through at least three major iterations of this module, each time having introduced complexity that was unwarranted. I had settled on only satisfying the first 2 of the 4 objectives I set out to satisfy. I added the associative array for looking up values as a means of addressing (4) above. I then saw that a 2 line change allowed me to hit the last target, (3), default values.

Xgetopt is released under the Artistic License, as found in the latest stable version of Perl (5.6.1). I hope it proves to be a useful addition to Python developers' toolboxes.

Modified 2001/10/14: Uses the new configurable separator in word_wrap to process help text and notes as formatted text with blank lines separating paragraphs.

4 comments

Alan Eldridge (author) 22 years, 5 months ago  # | flag

The latter half of this code is missing. The last half of this code module has been cut off from the display. I am sorry for this. I did not know the interface would do this when actually posting the code. -- Alan Eldridge, author of xgetopt.py

Alan Eldridge (author) 22 years, 5 months ago  # | flag

In order to use xgetopt.py. 1. You must get the source using the "Text Source" link near the top of the listing. It contains the full source code.

  1. You must get the source code for word_wrap.py, also posted to the Python Cookbook.
Alan Eldridge (author) 22 years, 5 months ago  # | flag

EXAMPLE OF USE.

#!/usr/bin/env python

import os
import sys
import string

import xgetopt

APPNAME = sys.argv[0].split(os.sep)[-1]

######################################################################
# comand line parser
######################################################################

G_opt = {}
G_optparser = xgetopt.parser()

G_optparser.set_app(APPNAME)
G_optparser.set_args('filename [...]')

G_optparser.add_opt('-v', '--verbose', None, 'v',
                  "verbose messages")
G_optparser.add_opt('-h', '--help', None, 'help',
                  'display this help message')
G_optparser.add_opt('-f', '--force', None, 'force',
                  "don't ask before overwriting files")
G_optparser.add_opt('-l', '--lang', 'language', 'lang',
                  'specify source language for output file')
G_optparser.add_opt('-d', '--dir', 'conf-dir', 'conf-dir',
                  'configuration and skeleton-file directory')
G_optparser.add_opt('-a', '--author', 'author', 'author',
                  'author\'s name')
G_optparser.add_opt('-e', '--email', 'email-address', 'email',
                  'author\'s email address')
G_optparser.add_opt('-L', '--license', 'license-dir', 'license',
                  'use this license/copyright type')
print G_optparser.add_opt('-z', '--desc', 'text', 'desc',
                  'summary text [used in RPM spec files]')

######################################################################
# set built-in defaults
######################################################################

# skel dir => ~/.$APPNAME.d
G_opt['conf-dir'] = os.environ['HOME'] + os.sep + '.' + APPNAME + '.d'

######################################################################
# now get our setup
######################################################################

try:
    opts, args, G_opt = G_optparser.getopt(sys.argv[1:], G_opt)
except:
    G_optparser.usage(1)

print 'Options given:\n'

for key in G_opt.keys():
    print key, G_opt[key]

if G_opt['help'] is not None:
    print
    G_optparser.usage(0)


#
#EOF
##
Radovan Chytracek 20 years, 7 months ago  # | flag

2 bugs prevent from using long options, fix included... Hi,

  I quite like your xgetopts, but long options were not wroking. The fix is:
  1. In method add_opt() line 94 change to:

    self.__l.append(long[2:]) # in order to remove leading hypens --

  2. In method getopt() line 137 change to:

    self.__l # geoopt.getopt expect list of strings # not a single string

<p> Cheers

Created by Alan Eldridge on Sun, 7 Oct 2001 (PSF)
Python recipes (4591)
Alan Eldridge's recipes (2)

Required Modules

  • (none specified)

Other Information and Tasks