aboutsummaryrefslogtreecommitdiffstats
path: root/arg.py
diff options
context:
space:
mode:
Diffstat (limited to 'arg.py')
-rw-r--r--arg.py307
1 files changed, 307 insertions, 0 deletions
diff --git a/arg.py b/arg.py
new file mode 100644
index 0000000..bb83df1
--- /dev/null
+++ b/arg.py
@@ -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