[getopt-sig] my current argument parser

Russ Cox rsc@plan9.bell-labs.com
Wed, 13 Feb 2002 16:32:23 -0500


[The docstring at the beginning explains all.  Comments welcome.]

from __future__ import generators
import sys, copy

"""Simple command-line argument iterator.

A typical example looks like:

	arg = ArgParser('usage: example [-d] [-r root] database\n')
	for o in arg:
		if o=='-d':
			debug=1
		elif o=='-r':
			root = arg.nextarg()
		else:
			arg.error('unknown option '+o)
	if len(arg.argv) != 1:
		arg.error()

This illustrates all the important points:

	* arg is a generator that returns the next argument.
	  >>>The behavior of the generator depends on the behavior
	  of the body of your for loop.<<<  This is why you don't have
	  to tell the ArgParser about your options a priori.  If an option
	  has an argument, you call arg.nextarg() to fetch it.  (If you
	  want an option to have two arguments, call arg.nextarg twice.)

	 * After the loop has finished, arg.argv contains the 
	  remaining command-line arguments.

	 * Calling arg.error prints the optional passed string,
	  then prints the usage message, then exits.

A few other points:

	* Consistent with typical Unix conventions, option parsing ends
	  after seeing '--', before seeing '-', or before any command-line argument
	  not beginning with a - (that wasn't gobbled as an option argument).
	  Assuming that -c does not take an argument but -f does, the following
	  set of command lines and parsings illustrates the rules:
		foo -c -- -a
			opts: -c
			args: -a
		foo -c - -a
			opts: -c
			args: - -a
		foo -c bar baz -a
			opts: -c
			args: bar baz -a
		foo -cfbar baz -a
			opts: -c -f
				(-f took bar)
			args: baz -a
		foo -cf bar baz -a
			opts: -c -f
				(-f took bar)
			args: baz -a
		foo -fc bar baz -a
			opts: -f
				(-f took c)
			args: bar baz -a

	* Single character short options begin with - and take arguments from the
	  rest of the current argument or from the next argument.  For example,
	  	foo -cbar
	  	foo -c bar
	  are equivalent.  There is no support for optional arguments, since that
	  introduces command-line parsing ambiguities.

	* Long options begin with -- and take arguments from an optional
	  ``=ARG'' suffix.  For example,
	  	foo --long=bar
	  is the only way to specify an argument to the --long option.
	  If the handler for '--long' does not fetch the argument with arg.nextarg,
	  the parser will call arg.error automatically.  There is support for optional
	  arguments to long options, since that does not introduce any ambiguities.
	  To fetch an optional argument call arg.nextarg(allownone=1).  If there
	  is no argument, None will be returned.

"""

class ArgError(Exception):
	def __init__(self, msg=None):
		if msg:
			self.msg = msg
		
class ArgParser:
	def __init__(self, usage, argv=sys.argv):
		self.argv0 = argv[0]
		self.argv = argv[1:]
		self.usage = usage
		self.waitingarg = ''

	def __iter__(self):
		# this assumes the "
		while self.argv:
			if self.argv[0]=='-' or self.argv[0][0]!='-':
				break
			a = self.argv.pop(0)
			if a=='--':
				break
			if a[0:2]=='--':
				i = a.find('=')
				if i==-1:
					self.waitingarg=None
					self.option = a
					yield self.option
					self.option = None
				else:
					self.waitingarg = a[i+1:]
					self.option = a[0:i]
					yield self.option
					if self.waitingarg:		# wasn't fetched using optarg
						self.error(self.option+' does not take an argument')
					self.option = None
				continue
			self.waitingarg = a[1:]
			while self.waitingarg:
				a = self.waitingarg[0:1]
				self.waitingarg = self.waitingarg[1:]
				self.option = '-'+a
				yield self.option
				self.option = None

	def nextarg(self, allownone=0):
		if self.waitingarg==None:
			if allownone:
				return None
			self.error(self.option+' requires an argument')
		elif self.waitingarg:
			ret = self.waitingarg
			self.waitingarg=''
		else:
			try:
				ret = self.argv.pop(0)
			except IndexError:
				self.error(self.option+' requires an argument')
		return ret

	def error(self, msg=None):
		if msg:
			sys.stderr.write('argument error: '+msg+'\n')
		sys.stderr.write(self.usage)
		sys.stderr.flush()
		sys.exit(1)