argshell.argshell

  1import argparse
  2import cmd
  3import shlex
  4import sys
  5from functools import wraps
  6from typing import Any, Callable
  7
  8
  9class Namespace(argparse.Namespace):
 10    """Simple object for storing attributes.
 11
 12    Implements equality by attribute names and values, and provides a simple string representation."""
 13
 14
 15class ArgShellParser(argparse.ArgumentParser):
 16    """***Overrides exit, error, and parse_args methods***
 17
 18    Object for parsing command line strings into Python objects.
 19
 20    Keyword Arguments:
 21        - prog -- The name of the program (default:
 22            ``os.path.basename(sys.argv[0])``)
 23        - usage -- A usage message (default: auto-generated from arguments)
 24        - description -- A description of what the program does
 25        - epilog -- Text following the argument descriptions
 26        - parents -- Parsers whose arguments should be copied into this one
 27        - formatter_class -- HelpFormatter class for printing help messages
 28        - prefix_chars -- Characters that prefix optional arguments
 29        - fromfile_prefix_chars -- Characters that prefix files containing
 30            additional arguments
 31        - argument_default -- The default value for all arguments
 32        - conflict_handler -- String indicating how to handle conflicts
 33        - add_help -- Add a -h/-help option
 34        - allow_abbrev -- Allow long options to be abbreviated unambiguously
 35        - exit_on_error -- Determines whether or not ArgumentParser exits with
 36            error info when an error occurs
 37    """
 38
 39    def exit(self, status=0, message=None):
 40        """Override to prevent shell exit when passing -h/--help switches."""
 41        if message:
 42            self._print_message(message, sys.stderr)
 43
 44    def error(self, message):
 45        raise Exception(f"prog: {self.prog}, message: {message}")
 46
 47    def parse_args(self, *args, **kwargs) -> Namespace:
 48        parsed_args: Namespace = super().parse_args(*args, **kwargs)
 49        return parsed_args
 50
 51
 52class ArgShell(cmd.Cmd):
 53    """Subclass this to create custom ArgShells."""
 54
 55    intro = "Entering argshell..."
 56    prompt = "argshell>"
 57
 58    def do_quit(self, command: str):
 59        """Quit shell."""
 60        return True
 61
 62    def do_help(self, arg):
 63        """List available commands with "help" or detailed help with "help cmd".
 64        If using 'help cmd' and the cmd is decorated with a parser, the parser help will also be printed."""
 65        if arg:
 66            # XXX check arg syntax
 67            try:
 68                func = getattr(self, "help_" + arg)
 69            except AttributeError:
 70                try:
 71                    func = getattr(self, "do_" + arg)
 72                    doc = func.__doc__
 73                    if doc:
 74                        self.stdout.write("%s\n" % str(doc))
 75                    # Check for decorator and call decorated function with "--help"
 76                    if hasattr(func, "__wrapped__"):
 77                        self.stdout.write(
 78                            f"Parser help for {func.__name__.replace('do_','')}:\n"
 79                        )
 80                        func("--help")
 81                    if doc or hasattr(func, "__wrapped__"):
 82                        return
 83                except AttributeError:
 84                    pass
 85                self.stdout.write("%s\n" % str(self.nohelp % (arg,)))
 86                return
 87            func()
 88        else:
 89            names = self.get_names()
 90            cmds_doc = []
 91            cmds_undoc = []
 92            topics = set()
 93            for name in names:
 94                if name[:5] == "help_":
 95                    topics.add(name[5:])
 96            names.sort()
 97            # There can be duplicates if routines overridden
 98            prevname = ""
 99            for name in names:
100                if name[:3] == "do_":
101                    if name == prevname:
102                        continue
103                    prevname = name
104                    cmd = name[3:]
105                    if cmd in topics:
106                        cmds_doc.append(cmd)
107                        topics.remove(cmd)
108                    elif getattr(self, name).__doc__:
109                        cmds_doc.append(cmd)
110                    else:
111                        cmds_undoc.append(cmd)
112            self.stdout.write("%s\n" % str(self.doc_leader))
113            self.print_topics(self.doc_header, cmds_doc, 15, 80)
114            self.print_topics(self.misc_header, sorted(topics), 15, 80)
115            self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
116
117
118def with_parser(
119    parser: Callable[..., ArgShellParser],
120    post_parsers: list[Callable[[Namespace], Namespace]] = [],
121) -> Callable[[Callable[[Any, Namespace], Any]], Callable[[Any, str], Any]]:
122    """Decorate a 'do_*' function in an argshell.ArgShell class with this function to pass an argshell.Namespace object to the decorated function instead of a string.
123
124    :param parser: A function that creates an argshell.ArgShellParser instance, adds arguments to it, and returns the parser.
125
126    :param post_parsers: An optional list of functions to execute where each function takes an argshell.Namespace instance and returns an argshell.Namespace instance.
127        'post_parser' functions are executed in the order they are supplied.
128
129    >>> def get_parser() -> argshell.ArgShellParser:
130    >>>     parser = argshell.ArgShellParser()
131    >>>     parser.add_argument("names", type=str, nargs="*", help="A list of first and last names to print.")
132    >>>     parser.add_argument("-i", "--initials", action="store_true", help="Print the initials instead of the full name.")
133    >>>     return parser
134    >>>
135    >>> # Convert list of first and last names to a list of tuples
136    >>> def names_list_to_tuples(args: argshell.Namespace) -> argshell.Namespace:
137    >>>     args.names = [(first, last) for first, last in zip(args.names[::2], args.names[1::2])]
138    >>>     if args.initials:
139    >>>         args.names = [(name[0][0], name[1][0]) for name in args.names]
140    >>>     return args
141    >>>
142    >>> def capitalize_names(args: argshell.Namespace) -> argshell.Namespace:
143    >>>     args.names = [name.capitalize() for name in args.names]
144    >>>     return args
145    >>>
146    >>> class NameShell(ArgShell):
147    >>>     intro = "Entering nameshell..."
148    >>>     prompt = "nameshell>"
149    >>>
150    >>>     @with_parser(get_parser, [capitalize_names, names_list_to_tuples])
151    >>>     def do_printnames(self, args: argshell.Namespace):
152    >>>         print(*[f"{name[0]} {name[1]}" for name in args.names], sep="\\n")
153    >>>
154    >>> NameShell().cmdloop()
155    >>> Entering nameshell...
156    >>> nameshell>printnames karl marx fred hampton emma goldman angela davis nestor makhno
157    >>> Karl Marx
158    >>> Fred Hampton
159    >>> Emma Goldman
160    >>> Angela Davis
161    >>> Nestor Makhno
162    >>> nameshell>printnames karl marx fred hampton emma goldman angela davis nestor makhno -i
163    >>> K M
164    >>> F H
165    >>> E G
166    >>> A D
167    >>> N M"""
168
169    def decorator(
170        func: Callable[[Any, Namespace], Any | None]
171    ) -> Callable[[Any, str], Any]:
172        @wraps(func)
173        def inner(self: Any, command: str) -> Any:
174            try:
175                args = parser().parse_args(shlex.split(command))
176            except Exception as e:
177                # On parser error, print help and skip post_parser and func execution
178                print(e)
179                command = "--help"
180                # Parsers with required positional arguments will crash shell
181                # without wrapping this in a try/except
182                try:
183                    args = parser().parse_args(shlex.split(command))
184                except Exception as e:
185                    ...
186                return None
187            # Don't execute function, only print parser help
188            if "-h" in command or "--help" in command:
189                return None
190            for post_parser in post_parsers:
191                args = post_parser(args)
192
193            return func(self, args)
194
195        return inner
196
197    return decorator
class Namespace(argparse.Namespace):
10class Namespace(argparse.Namespace):
11    """Simple object for storing attributes.
12
13    Implements equality by attribute names and values, and provides a simple string representation."""

Simple object for storing attributes.

Implements equality by attribute names and values, and provides a simple string representation.

Inherited Members
argparse.Namespace
Namespace
class ArgShellParser(argparse.ArgumentParser):
16class ArgShellParser(argparse.ArgumentParser):
17    """***Overrides exit, error, and parse_args methods***
18
19    Object for parsing command line strings into Python objects.
20
21    Keyword Arguments:
22        - prog -- The name of the program (default:
23            ``os.path.basename(sys.argv[0])``)
24        - usage -- A usage message (default: auto-generated from arguments)
25        - description -- A description of what the program does
26        - epilog -- Text following the argument descriptions
27        - parents -- Parsers whose arguments should be copied into this one
28        - formatter_class -- HelpFormatter class for printing help messages
29        - prefix_chars -- Characters that prefix optional arguments
30        - fromfile_prefix_chars -- Characters that prefix files containing
31            additional arguments
32        - argument_default -- The default value for all arguments
33        - conflict_handler -- String indicating how to handle conflicts
34        - add_help -- Add a -h/-help option
35        - allow_abbrev -- Allow long options to be abbreviated unambiguously
36        - exit_on_error -- Determines whether or not ArgumentParser exits with
37            error info when an error occurs
38    """
39
40    def exit(self, status=0, message=None):
41        """Override to prevent shell exit when passing -h/--help switches."""
42        if message:
43            self._print_message(message, sys.stderr)
44
45    def error(self, message):
46        raise Exception(f"prog: {self.prog}, message: {message}")
47
48    def parse_args(self, *args, **kwargs) -> Namespace:
49        parsed_args: Namespace = super().parse_args(*args, **kwargs)
50        return parsed_args

Overrides exit, error, and parse_args methods

Object for parsing command line strings into Python objects.

Keyword Arguments: - prog -- The name of the program (default: os.path.basename(sys.argv[0])) - usage -- A usage message (default: auto-generated from arguments) - description -- A description of what the program does - epilog -- Text following the argument descriptions - parents -- Parsers whose arguments should be copied into this one - formatter_class -- HelpFormatter class for printing help messages - prefix_chars -- Characters that prefix optional arguments - fromfile_prefix_chars -- Characters that prefix files containing additional arguments - argument_default -- The default value for all arguments - conflict_handler -- String indicating how to handle conflicts - add_help -- Add a -h/-help option - allow_abbrev -- Allow long options to be abbreviated unambiguously - exit_on_error -- Determines whether or not ArgumentParser exits with error info when an error occurs

def exit(self, status=0, message=None):
40    def exit(self, status=0, message=None):
41        """Override to prevent shell exit when passing -h/--help switches."""
42        if message:
43            self._print_message(message, sys.stderr)

Override to prevent shell exit when passing -h/--help switches.

def error(self, message):
45    def error(self, message):
46        raise Exception(f"prog: {self.prog}, message: {message}")

error(message: string)

Prints a usage message incorporating the message to stderr and exits.

If you override this in a subclass, it should not return -- it should either exit or raise an exception.

def parse_args(self, *args, **kwargs) -> argshell.argshell.Namespace:
48    def parse_args(self, *args, **kwargs) -> Namespace:
49        parsed_args: Namespace = super().parse_args(*args, **kwargs)
50        return parsed_args
Inherited Members
argparse.ArgumentParser
ArgumentParser
add_subparsers
parse_known_args
convert_arg_line_to_args
parse_intermixed_args
parse_known_intermixed_args
format_usage
format_help
print_usage
print_help
argparse._ActionsContainer
register
set_defaults
get_default
add_argument
add_argument_group
add_mutually_exclusive_group
class ArgShell(cmd.Cmd):
 53class ArgShell(cmd.Cmd):
 54    """Subclass this to create custom ArgShells."""
 55
 56    intro = "Entering argshell..."
 57    prompt = "argshell>"
 58
 59    def do_quit(self, command: str):
 60        """Quit shell."""
 61        return True
 62
 63    def do_help(self, arg):
 64        """List available commands with "help" or detailed help with "help cmd".
 65        If using 'help cmd' and the cmd is decorated with a parser, the parser help will also be printed."""
 66        if arg:
 67            # XXX check arg syntax
 68            try:
 69                func = getattr(self, "help_" + arg)
 70            except AttributeError:
 71                try:
 72                    func = getattr(self, "do_" + arg)
 73                    doc = func.__doc__
 74                    if doc:
 75                        self.stdout.write("%s\n" % str(doc))
 76                    # Check for decorator and call decorated function with "--help"
 77                    if hasattr(func, "__wrapped__"):
 78                        self.stdout.write(
 79                            f"Parser help for {func.__name__.replace('do_','')}:\n"
 80                        )
 81                        func("--help")
 82                    if doc or hasattr(func, "__wrapped__"):
 83                        return
 84                except AttributeError:
 85                    pass
 86                self.stdout.write("%s\n" % str(self.nohelp % (arg,)))
 87                return
 88            func()
 89        else:
 90            names = self.get_names()
 91            cmds_doc = []
 92            cmds_undoc = []
 93            topics = set()
 94            for name in names:
 95                if name[:5] == "help_":
 96                    topics.add(name[5:])
 97            names.sort()
 98            # There can be duplicates if routines overridden
 99            prevname = ""
100            for name in names:
101                if name[:3] == "do_":
102                    if name == prevname:
103                        continue
104                    prevname = name
105                    cmd = name[3:]
106                    if cmd in topics:
107                        cmds_doc.append(cmd)
108                        topics.remove(cmd)
109                    elif getattr(self, name).__doc__:
110                        cmds_doc.append(cmd)
111                    else:
112                        cmds_undoc.append(cmd)
113            self.stdout.write("%s\n" % str(self.doc_leader))
114            self.print_topics(self.doc_header, cmds_doc, 15, 80)
115            self.print_topics(self.misc_header, sorted(topics), 15, 80)
116            self.print_topics(self.undoc_header, cmds_undoc, 15, 80)

Subclass this to create custom ArgShells.

def do_quit(self, command: str):
59    def do_quit(self, command: str):
60        """Quit shell."""
61        return True

Quit shell.

def do_help(self, arg):
 63    def do_help(self, arg):
 64        """List available commands with "help" or detailed help with "help cmd".
 65        If using 'help cmd' and the cmd is decorated with a parser, the parser help will also be printed."""
 66        if arg:
 67            # XXX check arg syntax
 68            try:
 69                func = getattr(self, "help_" + arg)
 70            except AttributeError:
 71                try:
 72                    func = getattr(self, "do_" + arg)
 73                    doc = func.__doc__
 74                    if doc:
 75                        self.stdout.write("%s\n" % str(doc))
 76                    # Check for decorator and call decorated function with "--help"
 77                    if hasattr(func, "__wrapped__"):
 78                        self.stdout.write(
 79                            f"Parser help for {func.__name__.replace('do_','')}:\n"
 80                        )
 81                        func("--help")
 82                    if doc or hasattr(func, "__wrapped__"):
 83                        return
 84                except AttributeError:
 85                    pass
 86                self.stdout.write("%s\n" % str(self.nohelp % (arg,)))
 87                return
 88            func()
 89        else:
 90            names = self.get_names()
 91            cmds_doc = []
 92            cmds_undoc = []
 93            topics = set()
 94            for name in names:
 95                if name[:5] == "help_":
 96                    topics.add(name[5:])
 97            names.sort()
 98            # There can be duplicates if routines overridden
 99            prevname = ""
100            for name in names:
101                if name[:3] == "do_":
102                    if name == prevname:
103                        continue
104                    prevname = name
105                    cmd = name[3:]
106                    if cmd in topics:
107                        cmds_doc.append(cmd)
108                        topics.remove(cmd)
109                    elif getattr(self, name).__doc__:
110                        cmds_doc.append(cmd)
111                    else:
112                        cmds_undoc.append(cmd)
113            self.stdout.write("%s\n" % str(self.doc_leader))
114            self.print_topics(self.doc_header, cmds_doc, 15, 80)
115            self.print_topics(self.misc_header, sorted(topics), 15, 80)
116            self.print_topics(self.undoc_header, cmds_undoc, 15, 80)

List available commands with "help" or detailed help with "help cmd". If using 'help cmd' and the cmd is decorated with a parser, the parser help will also be printed.

Inherited Members
cmd.Cmd
Cmd
cmdloop
precmd
postcmd
preloop
postloop
parseline
onecmd
emptyline
default
completedefault
completenames
complete
get_names
complete_help
print_topics
columnize
def with_parser( parser: Callable[..., argshell.argshell.ArgShellParser], post_parsers: list[typing.Callable[[argshell.argshell.Namespace], argshell.argshell.Namespace]] = []) -> Callable[[Callable[[Any, argshell.argshell.Namespace], Any]], Callable[[Any, str], Any]]:
119def with_parser(
120    parser: Callable[..., ArgShellParser],
121    post_parsers: list[Callable[[Namespace], Namespace]] = [],
122) -> Callable[[Callable[[Any, Namespace], Any]], Callable[[Any, str], Any]]:
123    """Decorate a 'do_*' function in an argshell.ArgShell class with this function to pass an argshell.Namespace object to the decorated function instead of a string.
124
125    :param parser: A function that creates an argshell.ArgShellParser instance, adds arguments to it, and returns the parser.
126
127    :param post_parsers: An optional list of functions to execute where each function takes an argshell.Namespace instance and returns an argshell.Namespace instance.
128        'post_parser' functions are executed in the order they are supplied.
129
130    >>> def get_parser() -> argshell.ArgShellParser:
131    >>>     parser = argshell.ArgShellParser()
132    >>>     parser.add_argument("names", type=str, nargs="*", help="A list of first and last names to print.")
133    >>>     parser.add_argument("-i", "--initials", action="store_true", help="Print the initials instead of the full name.")
134    >>>     return parser
135    >>>
136    >>> # Convert list of first and last names to a list of tuples
137    >>> def names_list_to_tuples(args: argshell.Namespace) -> argshell.Namespace:
138    >>>     args.names = [(first, last) for first, last in zip(args.names[::2], args.names[1::2])]
139    >>>     if args.initials:
140    >>>         args.names = [(name[0][0], name[1][0]) for name in args.names]
141    >>>     return args
142    >>>
143    >>> def capitalize_names(args: argshell.Namespace) -> argshell.Namespace:
144    >>>     args.names = [name.capitalize() for name in args.names]
145    >>>     return args
146    >>>
147    >>> class NameShell(ArgShell):
148    >>>     intro = "Entering nameshell..."
149    >>>     prompt = "nameshell>"
150    >>>
151    >>>     @with_parser(get_parser, [capitalize_names, names_list_to_tuples])
152    >>>     def do_printnames(self, args: argshell.Namespace):
153    >>>         print(*[f"{name[0]} {name[1]}" for name in args.names], sep="\\n")
154    >>>
155    >>> NameShell().cmdloop()
156    >>> Entering nameshell...
157    >>> nameshell>printnames karl marx fred hampton emma goldman angela davis nestor makhno
158    >>> Karl Marx
159    >>> Fred Hampton
160    >>> Emma Goldman
161    >>> Angela Davis
162    >>> Nestor Makhno
163    >>> nameshell>printnames karl marx fred hampton emma goldman angela davis nestor makhno -i
164    >>> K M
165    >>> F H
166    >>> E G
167    >>> A D
168    >>> N M"""
169
170    def decorator(
171        func: Callable[[Any, Namespace], Any | None]
172    ) -> Callable[[Any, str], Any]:
173        @wraps(func)
174        def inner(self: Any, command: str) -> Any:
175            try:
176                args = parser().parse_args(shlex.split(command))
177            except Exception as e:
178                # On parser error, print help and skip post_parser and func execution
179                print(e)
180                command = "--help"
181                # Parsers with required positional arguments will crash shell
182                # without wrapping this in a try/except
183                try:
184                    args = parser().parse_args(shlex.split(command))
185                except Exception as e:
186                    ...
187                return None
188            # Don't execute function, only print parser help
189            if "-h" in command or "--help" in command:
190                return None
191            for post_parser in post_parsers:
192                args = post_parser(args)
193
194            return func(self, args)
195
196        return inner
197
198    return decorator

Decorate a 'do_*' function in an argshell.ArgShell class with this function to pass an argshell.Namespace object to the decorated function instead of a string.

Parameters
  • parser: A function that creates an argshell.ArgShellParser instance, adds arguments to it, and returns the parser.

  • post_parsers: An optional list of functions to execute where each function takes an argshell.Namespace instance and returns an argshell.Namespace instance. 'post_parser' functions are executed in the order they are supplied.

>>> def get_parser() -> argshell.ArgShellParser:
>>>     parser = argshell.ArgShellParser()
>>>     parser.add_argument("names", type=str, nargs="*", help="A list of first and last names to print.")
>>>     parser.add_argument("-i", "--initials", action="store_true", help="Print the initials instead of the full name.")
>>>     return parser
>>>
>>> # Convert list of first and last names to a list of tuples
>>> def names_list_to_tuples(args: argshell.Namespace) -> argshell.Namespace:
>>>     args.names = [(first, last) for first, last in zip(args.names[::2], args.names[1::2])]
>>>     if args.initials:
>>>         args.names = [(name[0][0], name[1][0]) for name in args.names]
>>>     return args
>>>
>>> def capitalize_names(args: argshell.Namespace) -> argshell.Namespace:
>>>     args.names = [name.capitalize() for name in args.names]
>>>     return args
>>>
>>> class NameShell(ArgShell):
>>>     intro = "Entering nameshell..."
>>>     prompt = "nameshell>"
>>>
>>>     @with_parser(get_parser, [capitalize_names, names_list_to_tuples])
>>>     def do_printnames(self, args: argshell.Namespace):
>>>         print(*[f"{name[0]} {name[1]}" for name in args.names], sep="\n")
>>>
>>> NameShell().cmdloop()
>>> Entering nameshell...
>>> nameshell>printnames karl marx fred hampton emma goldman angela davis nestor makhno
>>> Karl Marx
>>> Fred Hampton
>>> Emma Goldman
>>> Angela Davis
>>> Nestor Makhno
>>> nameshell>printnames karl marx fred hampton emma goldman angela davis nestor makhno -i
>>> K M
>>> F H
>>> E G
>>> A D
>>> N M