# https://gist.github.com/fonic/fe6cade2e1b9eaf3401cc732f48aeebd
import sys
from glxshell.lib.textwrap import indent
from glxshell.lib.textwrap import wrap
from glxshell.lib.argparse.utility_description import UtilityDescription
from glxshell.lib.utils import get_bold_text
OPTIONAL = "?"
ZERO_OR_MORE = "*"
ONE_OR_MORE = "+"
PARSER = "A..."
REMAINDER = "..."
[docs]
class WrapperCmdLineArgParser:
def __init__(self, parser):
"""Init decorator with an argparse parser to be used in parsing cmd-line options"""
self.parser = parser
def __call__(self, func):
"""Decorate 'func' to parse 'line' and pass options to decorated function"""
if not self.parser:
self.parser = func(None, None, None, True)
def wrapped_function(*args):
line = args[1].split()
try:
parsed = self.parser.parse_args(line)
except SystemExit:
return None
return func(*args, parsed)
return wrapped_function
class _ArgError(BaseException):
pass
class _Arg:
def __init__(self, names, dest, action, nargs, const, default, arg_type, arg_help):
self.names = names
self.dest = dest
self.action = action
self.nargs = nargs
self.const = const
self.default = default
self.type = arg_type
self.help = arg_help
def parse(self, optname, eq_arg, args):
# parse args for this arg
if self.action in {'store', 'append'}:
if self.nargs is None:
if eq_arg:
return self.type(eq_arg)
if args:
return self.type(args.pop(0))
raise _ArgError("expecting value for %s" % optname)
if self.nargs == OPTIONAL:
if eq_arg:
return self.type(eq_arg)
if args:
return self.type(args.pop(0))
return self.default
# Else
ret = []
if self.nargs == ZERO_OR_MORE:
n = -1
elif self.nargs == ONE_OR_MORE:
if not args:
raise _ArgError("expecting value for %s" % optname)
n = -1
elif self.nargs == REMAINDER:
n = 0
while args:
ret.append(args.pop(0))
else:
n = int(self.nargs)
stop_at_opt = True
while args and n != 0:
if stop_at_opt and args[0].startswith("-") and args[0] != "-":
if args[0] == "--":
stop_at_opt = False
args.pop(0)
else:
break
else:
ret.append(args.pop(0))
n -= 1
if n > 0:
raise _ArgError("expecting value for %s" % optname)
return ret
if self.action == "store_const":
return self.const
assert False
def _dest_from_optnames(opt_names):
dest = opt_names[0]
for name in opt_names:
if name.startswith("--"):
dest = name
break
return dest.lstrip("-").replace("-", "_")
[docs]
class ArgumentParser(UtilityDescription):
def __init__(self, **kwargs):
UtilityDescription.__init__(self)
self.columns = 79
add_help = kwargs.get("add_help", False)
self.name = kwargs.get("name", None)
self.synopsis = kwargs.get("synopsis", None)
self.description = kwargs.get("description", None)
self.options = kwargs.get("options", None)
self.operands = kwargs.get("operands", None)
self.stdin = kwargs.get("stdin", None)
self.input_files = kwargs.get("input_files", None)
self.environment_variables = kwargs.get("environment_variables", None)
self.asynchronous_events = kwargs.get("asynchronous_events", None)
self.stdout = kwargs.get("stdout", None)
self.stderr = kwargs.get("stderr", None)
self.output_files = kwargs.get("output_files", None)
self.extended_description = kwargs.get("extended_description", None)
self.exit_status = kwargs.get("exit_status", None)
self.consequences_of_errors = kwargs.get("consequences_of_errors", None)
self.application_usage = kwargs.get("application_usage", None)
self.examples = kwargs.get("examples", None)
self.rationale = kwargs.get("rationale", None)
self.future_directions = kwargs.get("future_directions", None)
self.see_also = kwargs.get("see_also", None)
self.change_history = kwargs.get("change_history", None)
self.opt = []
self.pos = []
if add_help:
self.add_argument("-h", "--help", dest="help", action="store_true", help="Show this help message and exit")
[docs]
def add_argument(self, *args, **kwargs):
action = kwargs.get("action", "store")
if action == "store_true":
action = "store_const"
const = True
default = kwargs.get("default", False)
elif action == "store_false":
action = "store_const"
const = False
default = kwargs.get("default", True)
elif action == "append":
const = None
default = kwargs.get("default", [])
else:
const = kwargs.get("const", None)
default = kwargs.get("default", None)
if args and args[0].startswith("-"):
args_list = self.opt
dest = kwargs.get("dest")
if dest is None:
dest = _dest_from_optnames(args)
else:
args_list = self.pos
dest = kwargs.get("dest")
if dest is None:
dest = args[0]
if not args:
args = [dest]
args_list.append(
_Arg(
args,
dest,
action,
kwargs.get("nargs", None),
const,
default,
kwargs.get("type", str),
kwargs.get("help", ""),
)
)
[docs]
@staticmethod
def error(msg):
sys.stderr.write("Error: %s\n" % msg)
sys.exit(2)
[docs]
def parse_args(self, args=None, namespace=None):
return self._parse_args_impl(args, namespace, False)
[docs]
def parse_known_args(self, args=None, namespace=None):
return self._parse_args_impl(args, namespace, True)
def _parse_args_impl(self, args, namespace, return_unknown):
if args is None:
args = sys.argv[1:]
else:
args = args[:]
if namespace is None:
namespace = Namespace()
try:
return self._parse_args(args, namespace, return_unknown)
except _ArgError as e:
self.print_usage()
self.error(str(e))
return None
def _parse_args(self, args, arg_holder, return_unknown):
# add optional args with defaults
for opt in self.opt:
setattr(arg_holder, opt.dest, opt.default)
# deal with unknown arguments, if needed
unknown = []
def consume_unknown():
while args and not args[0].startswith("-"):
unknown.append(args.pop(0))
# parse all args
parsed_pos = False
while args or not parsed_pos:
if args and args[0].startswith("-") and args[0] != "-" and args[0] != "--":
# optional arg
a = args.pop(0)
# optional deal with cumulative arguments (ex: -lah)
if "--" not in a and len(a) > 2:
index_focus = 0
for letter in a.replace("-", ""):
args.insert(index_focus, "-%s" % letter)
index_focus += 1
a = args.pop(0)
# if a in ("-h", "--help"):
# self.print_help()
# sys.exit(0)
eq_arg = None
if a.startswith("--") and "=" in a:
a, eq_arg = a.split("=", 1)
found = False
for _, opt in enumerate(self.opt):
if a in opt.names:
val = opt.parse(a, eq_arg, args)
if opt.action == "append":
getattr(arg_holder, opt.dest).append(val)
else:
setattr(arg_holder, opt.dest, val)
found = True
break
if not found:
if return_unknown:
unknown.append(a)
consume_unknown()
else:
raise _ArgError("unknown option %s" % a)
else:
# positional arg
if parsed_pos:
if return_unknown:
unknown = unknown + args
break
raise _ArgError("extra args: %s" % " ".join(args))
for pos in self.pos:
setattr(arg_holder, pos.dest, pos.parse(pos.names[0], None, args))
parsed_pos = True
if return_unknown:
consume_unknown()
return (arg_holder, unknown) if return_unknown else arg_holder
[docs]
@staticmethod
def render_arg(arg):
if arg.action == "store":
if arg.nargs == ONE_OR_MORE:
return " %s..." % arg.dest
if arg.nargs == ZERO_OR_MORE:
return " [%s...]" % arg.dest
if arg.nargs == OPTIONAL:
return " [%s]" % arg.dest
return " %s" % arg.dest
return ""
def _format_help_name(self):
if self.name:
sys.stdout.write("%s\n" % get_bold_text("NAME"))
for line in wrap(self.name, self.columns, replace_whitespace=False):
sys.stdout.write("%s\n" % indent(line, " "))
def _format_help_synopsis(self, application_name):
sys.stdout.write("\n%s\n" % get_bold_text("SYNOPSIS"))
if self.synopsis:
for synopsis_variation in self.synopsis:
for line in wrap(synopsis_variation, self.columns, replace_whitespace=False):
sys.stdout.write(
"%s\n" % indent(
line.replace(application_name, get_bold_text(application_name)),
" ")
)
else:
if self.name:
sys.stdout.write(" %s" % get_bold_text(self.name.split()[0]))
else:
sys.stdout.write(" %s" % get_bold_text(sys.argv[0]))
for opt in self.opt:
sys.stdout.write(" [%s]" % ", ".join(opt.names))
for pos in self.pos:
sys.stdout.write(self.render_arg(pos))
sys.stdout.write("\n")
def _format_help_description(self):
if self.description:
sys.stdout.write("\n%s\n" % get_bold_text("DESCRIPTION"))
for line in wrap(self.description, self.columns, replace_whitespace=False):
sys.stdout.write("%s\n" % indent(line, " "))
def _format_help_operands(self):
if self.pos:
sys.stdout.write("\n%s\n" % get_bold_text("OPERANDS"))
max_size = max(len(x.names[0]) for x in self.pos)
for pos in self.pos:
the_name = pos.names[0]
pos.help = pos.help[0].upper() + pos.help[1:]
the_help = wrap(pos.help, self.columns - max_size - 4)
sys.stdout.write(indent(get_bold_text(the_name), " "))
for help_line in the_help:
if help_line == the_help[0]:
sys.stdout.write(indent(help_line, " " * int(max_size - len(the_name) + 2)))
sys.stdout.write("\n")
else:
sys.stdout.write(indent(help_line, " " * int(max_size + 4)))
sys.stdout.write("\n")
def _format_help_options(self):
if self.opt:
sys.stdout.write("\n%s\n" % get_bold_text("OPTIONS"))
max_size = max(len(", ".join(x.names)) for x in self.opt)
for opt in self.opt:
the_name = ", ".join(opt.names)
opt.help = opt.help[0].upper() + opt.help[1:]
the_help = wrap(opt.help, self.columns - max_size - 4)
sys.stdout.write(indent(get_bold_text(the_name), " "))
for help_line in the_help:
if help_line == the_help[0]:
sys.stdout.write(indent(help_line, " " * int(max_size - len(the_name) + 2)))
sys.stdout.write("\n")
else:
sys.stdout.write(indent(help_line, " " * int(max_size + 4)))
sys.stdout.write("\n")
def _format_exit_status(self):
if self.exit_status:
sys.stdout.write("\n%s\n" % get_bold_text("EXIT STATUS"))
max_size = max(len(exit_code) for exit_code, description in self.exit_status.items())
for exit_code, description in self.exit_status.items():
sys.stdout.write(indent(get_bold_text(exit_code), " "))
if description:
the_help = wrap(description, self.columns - 4)
for help_line in the_help:
if help_line == the_help[0]:
sys.stdout.write(indent(help_line, " " * int(max_size - len(exit_code) + 2)))
sys.stdout.write("\n")
else:
sys.stdout.write(indent(help_line, " " * int(max_size + 4)))
sys.stdout.write("\n")
else:
sys.stdout.write("\n")
[docs]
def print_usage(self):
self.format_usage()
[docs]
def print_help(self, columns=None):
self.format_help(columns=columns)
[docs]
class FileType:
"""Factory for creating file object types
Instances of FileType are typically passed as type= arguments to the
ArgumentParser add_argument() method.
Keyword Arguments:
- mode -- A string indicating how the file is to be opened. Accepts the
same values as the builtin open() function.
- bufsize -- The file's desired buffer size. Accepts the same values as
the builtin open() function.
- encoding -- The file's encoding. Accepts the same values as the
builtin open() function.
- errors -- A string indicating how encoding and decoding errors are to
be handled. Accepts the same value as the builtin open() function.
"""
def __init__(self, mode="r", bufsize=-1, encoding=None, errors=None):
self._mode = mode
self._bufsize = bufsize
self._encoding = encoding
self._errors = errors
def __call__(self, string):
# the special argument "-" means sys.std{in,out}
if string == "-":
if "r" in self._mode:
return sys.stdin
if "w" in self._mode:
return sys.stdout
raise ValueError('argument "-" with mode %r' % self._mode)
# all other arguments are used as file names
try:
return open(string, self._mode, self._bufsize, self._encoding, self._errors)
except OSError as e:
args = {"filename": string, "error": e}
message = "can't open '%(filename)s': %(error)s"
raise TypeError(message % args) from e
def __repr__(self):
args = self._mode, self._bufsize
kwargs = [("encoding", self._encoding), ("errors", self._errors)]
args_str = ", ".join(
[repr(arg) for arg in args if arg != -1] + ["%s=%r" % (kw, arg) for kw, arg in kwargs if arg is not None]
)
return "%s(%s)" % (type(self).__name__, args_str)