# See LICENSE file for copyright and license details. # -*- coding: utf-8 -*- from __future__ import print_function import sys as _sys import os as _os import errno as _errno MAY_HAVE_ATTACHED_ARGUMENT = 0 NEED_ARGUMENT = 1 NEED_ATTACHED_ARGUMENT = 2 NEED_DETACHED_ARGUMENT = 3 NEED_NO_ARGUMENT = 4 class UsageError(Exception): pass class Parser(object): ''' Command line parser Example usage: import sys import arg def usage(): print('usage: %s [-v value] [-xy]' % sys.argv[0], file = sys.stderr) sys.exit(1) xflag = False yflag = False vflag = None parser = arg.Parser(usage = usage) for c in parser.flags: if c == 'x': xflag = True elif c == 'y': yflag = True elif c == 'v': vflag = parser.arg else: usage() ''' def __init__(self, argv = None, symbols = '-', keep_dashdash = False, store_nonflags = False, usage = None): ''' @param argv A list with the arguments to parse, if `None`, `sys.argv[1:]` is used @param symbols A string or list contain the characters that will cause an argument to be parsed as being a flag, multicharacter strings will be ignored @param keep_dashdash If `True`, `--` will be returned in `self.argv` @param keep_nonflags During parsing, non-flag arguments are ignored, and after the last flag has been parsed, `self.argv` will have the ignored arguments at the beginning @param usage Function (without arguments) that is to be called if parsing fails due to usage error, if `None`, `UsageError` will be raised instead on usage error ''' if argv is None: argv = _sys.argv[1:] self._argv = list(argv) self._usage_func = usage self._symbols = symbols self._keep_ddash = keep_dashdash self._stored = [] if store_nonflags else None def _usage(self): if self._usage_func is None: raise UsageError() else: self._usage_func() @property def flags(self): ''' Return a generator that returns the flags in the command line Properties and functions in `self` can be used to after each time a flag is returned Each returned value is a single character ''' self._brk = False self._accept_none_arg = False while len(self._argv): if len(self._argv[0]) < 2: if self._stored is not None: self._stored.append(self._argv[0]) self._argv = self._argv[1:] continue break self._symbol = self._argv[0][0] self._lflag = self._argv[0] if self._symbol not in self._symbols: if self._stored is not None: self._stored.append(self._argv[0]) self._argv = self._argv[1:] continue break if self._argv[0] == 2 * self._symbol: if self._symbol == '-': if not self._keep_ddash: self._argv = self._argv[1:] elif self._stored is not None: self._stored.append(self._argv[0]) self._argv = self._argv[1:] continue break self._i = 1 n = len(self._argv[0]) while self._i < n: self._flag = self._argv[0][self._i] if self._flag == self._symbol and self._i != 1: self._usage() if self._i + 1 < n: self._arg = self._argv[0][self._i + 1:] elif len(self._argv) > 1: self._arg = self._argv[1] else: self._arg = None yield self._flag self._lflag = None self._accept_none_arg = False if self._brk: if len(self._argv) > 1 and self._arg is self._argv[1]: self._argv = self._argv[1:] self._brk = False break self._i += 1 self._argv = self._argv[1:] if self._stored: self._argv = self._stored + self._argv self._stored = None @property def flag(self): ''' Return the current short flag, for example '-a', this string is always two characters long ''' return self._symbol + self._flag @property def symbol(self): ''' Get the first character in the flag, normally '-' ''' return self._symbol @property def lflag(self): ''' Get the entire current argument, for example '--mode=755', `None` not at the beginning ''' return self._lflag @property def argv(self): ''' Return a list of all remaining arguments ''' return self._argv @property def argc(self): ''' Return the number of remaining arguments ''' return len(self._argv) @property def arg(self): ''' Get the argument specified for the flag, can only be `None` (no argument) if `self.testlong` has returned `True` with `MAY_HAVE_ATTACHED_ARGUMENT` as the second argument for the currnet flag Reading this property will cause the parser to assume that the flag should have an argument, if these is no argument UsageError will be raised (or the specified usage function will be called) unless `self.testlong` has returned `True` with `MAY_HAVE_ATTACHED_ARGUMENT` as the second argument for the currnet flag ''' if self._arg is None and not self._accept_none_arg: self._usage() self._brk = True return self._arg @property def arghere(self): ''' Return the current argument with an offset such that the first character is the character associated with the current flag, for example, if the current argument is '-123' and the current flag is '1', '123' is returned Reading this property will cause the parser to continue with the next argument when getting the next flag ''' self._brk = True self._arg = None return self._argv[0][self._i:] @property def isargnum(self): ''' Check whether the value returned by `arghere` will be a number Calling this function does not affect the parser ''' return self._argv[0][self._i:].isdigit() @property def argnum(self): ''' Identical to `arghere`, except the returned value will be converted to an integer If the value returned by `self.arghere` is not a number, UsageError will be raised (or the specified usage function will be called) ''' if not self.isargnum: self._usage() return int(self.arghere) def consume(self): ''' Cause the parser to skip the rest of current argument and continue with the next argument If `self.arg` has been read and returned the next argument, the parser will still read that argument ''' self._arg = None self._brk = True def testlong(self, flag, argument = 0): ''' Check whether the current flag is specific long flag It is important to use this function, because it will set the parser's state appropriately whe it finds a match If the flag is the specified long flag, but its argument status does not match `argument`, UsageError will be raised (or the specified usage function will be called) @param flag The long flag, should start with its symbol prefix (usually '--') @param argument arg.MAY_HAVE_ATTACHED_ARGUMENT (default): The flag may have an argument attached to the flag separated by a '=', for example '--value=2', but it is not necessary, so just '--value' is also accepted, but the next argument may not be parsed as a value associated with the flag arg.NEED_ARGUMENT: The flag must have an attached argument or a detached argument, for example '--value=2' is accepted, but '--value' is only accepted if it is not the last argument, if the current argument does not contain a '=' arg.NEED_ATTACHED_ARGUMENT: The flag must have an attached argument, for example '--value=2' is accepted but '--value' is not arg.NEED_DETACHED_ARGUMENT: The flag must have a detached argument, for example '--value' is accepted, but only if it is not the last argument, but '--value=2' is not accepted arg.NEED_NO_ARGUMENT: The flag must not have an argument, for example '--value' is accepted but '--value=2' is not ''' if self._lflag is None: return False attached = self._lflag.startswith(flag + '=') if attached: arg = self._lflag[self._lflag.index('=') + 1:] lflag = self._lflag[:self._lflag.index('=')] else: arg = self._argv[1] if len(self._argv) > 1 else None lflag = self._lflag if lflag != flag: return False elif argument == MAY_HAVE_ATTACHED_ARGUMENT: if not attached: arg = None self._accept_none_arg = True elif argument == NEED_ARGUMENT: if arg is None: self._usage() elif argument == NEED_ATTACHED_ARGUMENT: if not attached: self._usage() elif argument == NEED_DETACHED_ARGUMENT: if attached or arg is None: self._usage() elif argument == NEED_NO_ARGUMENT: arg = None if attached: self._usage() else: raise OSError(_os.strerror(_errno.EINVAL), _errno.EINVAL) self._arg = arg self._brk = True return True