diff options
Diffstat (limited to 'arg.py')
-rw-r--r-- | arg.py | 307 |
1 files changed, 307 insertions, 0 deletions
@@ -0,0 +1,307 @@ +# 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 |