clik-shell¶
clik-shell is a tiny glue library between clik and cmd:
from clik import app
from clik_shell import DefaultShell
@app
def myapp():
yield
# ... subcommands for myapp ...
@myapp
def shell():
yield
DefaultShell(myapp).cmdloop()
See the quickstart for more documentation on what clik-shell can do.
Quickstart¶
clik-shell makes it easy to add an interactive command shell to your clik application.
Example Program¶
Here’s the program we’ll be working with:
from clik import app
@app
def myapp():
"""Example application for clik-shell."""
yield
print('myapp')
@myapp
def foo():
"""Print foo."""
yield
print('foo')
@myapp
def bar():
"""Print bar."""
yield
print('bar')
@myapp
def baz():
"""A subcommand with subcommands."""
yield
print('baz')
@baz
def spam():
"""Print spam."""
yield
print('spam')
@baz
def ham():
"""Print ham."""
yield
print('ham')
@baz
def eggs():
"""Print eggs."""
yield
print('eggs')
if __name__ == '__main__':
myapp.main()
Add Shell Subcommand¶
Add a new subcommand that makes use of
clik_shell.DefaultShell
:
from clik_shell import DefaultShell
@myapp
def shell():
"""Interactive command shell for my application."""
yield
DefaultShell(myapp).cmdloop()
That’s it! The example application now has an interactive command shell:
$ ./example.py shell
myapp
myapp> help
Documented commands (type help <topic>):
========================================
EOF bar baz exit foo help quit shell
myapp> help foo
usage: foo [-h]
Print foo.
optional arguments:
-h, --help show this help message and exit
myapp> help baz
usage: baz [-h] {spam,ham,eggs} ...
A subcommand with subcommands.
optional arguments:
-h, --help show this help message and exit
subcommands:
{spam,ham,eggs}
spam Print spam.
ham Print ham.
eggs Print eggs.
myapp> foo
foo
myapp> baz
usage: baz [-h] {spam,ham,eggs} ...
baz: error: the following arguments are required: {spam,ham,eggs}
myapp> qux
error: unregonized command: qux (enter ? for help)
myapp> baz spam
baz
spam
myapp> exit
$
Intended Usage¶
In practice, the base shell is designed to be subclassed:
class Shell(DefaultShell):
def __init__(self):
super(Shell, self).__init__(myapp)
@myapp
def shell():
"""Interactive command shell for my application."""
yield
Shell().cmdloop()
DefaultShell
is a subclass of
Cmd
, so subclasses of DefaultShell
can make use of everything in Cmd
. This is useful for things like customizing the prompt and
adding introductory text:
class Shell(DefaultShell):
intro = 'Welcome to the myapp shell. Enter ? for a list of commands.\n\n'
prompt = '(myapp)% '
With those updates:
$ ./example.py shell
myapp
Welcome to the myapp shell. Enter ? for a list of commands.
(myapp)%
Excluding Commands from the Shell¶
As implemented, the shell
command is available from within the
shell:
$ ./example.py shell
myapp
myapp> ?
Documented commands (type help <topic>):
========================================
EOF bar baz exit foo help quit shell
myapp> shell
myapp> exit
myapp> exit
$
This works, but isn’t the desired behavior. There’s no reason for
users to start a “subshell.” For this case,
clik_shell.exclude_from_shell()
is available:
from clik_shell import DefaultShell, exclude_from_shell
@exclude_from_shell
@myapp
def shell():
"""Interactive command shell for my application."""
yield
Shell().cmdloop()
Now users cannot call shell
from within the shell:
$ ./example.py shell
myapp
myapp> ?
Documented commands (type help <topic>):
========================================
EOF bar baz exit foo help quit
myapp> shell
error: unregonized command: shell (enter ? for help)
myapp> exit
$
Note that exclude_from_shell
is not limited to the shell command itself – it may be used on any
subcommand to exclude that subcommand from the shell interface.
Shell-Only Commands¶
To create a command that is available only in the shell, define a new
do_*
method as outlined in the cmd
documentation:
import subprocess
class Shell(DefaultShell):
def do_clear(self, _):
"""Clear the terminal screen."""
yield
subprocess.call('clear')
Base Shell Classes¶
DefaultShell
adds a few commonly
desired facilities to the default command loop:
exit
andquit
commands to exit the shellEOF
handler, which exits the shell on Ctl-DKeyboardInterrupt
handler, which exits the shell on Ctl-Ccmd.Cmd.emptyline()
override to a no-op (by default it runs the last command entered)
If you want to implement these facilities yourself, subclass
clik_shell.BaseShell
instead of the default shell. The base
shell defines only three methods on top of cmd.Cmd
:
__init__
, which dynamically generates thedo_*
andhelp_*
methodsdefault
, which overrides the defaultcmd.Cmd.default()
implementation in order to hack in support for hyphenated command names (see below)error
, which is called when a command exits with a non-zero code
Hyphenated Commands¶
cmd
does not natively support commands with hyphenated names –
commands are defined by creating a do_*
method and methods may not
have hyphens in them. Due to this constraint, there’s not much
clik-shell can do but work around it as best as possible:
- For the purpose of defining methods, all hyphens are converted to
underscores – so
my-subcommand
becomesmy_subcommand
- A hook is added to
cmd.Cmd.default()
to recognizemy-subcommand
and redirect it tomy_subcommand
Le sigh. This sucks because:
- The underscore names aren’t the “real” command names
- The hyphen names don’t show up in the help documentation
- In theory someone could define
my-subcommand
andmy_subcommand
, which totally breaks this scheme (in practice, anyone who designs a CLI where those two commands do different things deserves to have their app broken)
But, I mean, at least my-subcommand
doesn’t bail out. And that’s
the only reason the workaround was implemented. Otherwise it’s a
pretty ugly wart on an otherwise reasonably-designed API.
API¶
-
clik_shell.
exclude_from_shell
(command_or_fn)[source]¶ Exclude command from the shell interface.
This decorator can be applied before or after the command decorator:
@exclude_from_shell @myapp def mycommand(): # is the same as @myapp @exclude_from_shell def mycommand():
Parameters: command_or_fn ( clik.command.Command
or function) – Command instance or functionReturns: Whatever was passed in
-
class
clik_shell.
BaseShell
(command)[source]¶ Bases:
cmd.Cmd
Minimal implementation to integrate clik and cmd.
-
__init__
(command)[source]¶ Instantiate the command loop.
Parameters: command ( clik.command.Command
) – “Root” command object (usually the application object created byclik.app.app()
)
-
default
(line)[source]¶ Override that hackily supports commands with hyphens.
See the quickstart in the documentation for further explanation.
Parameters: line (str) – Line whose command is unrecognized Return type: None
-
-
class
clik_shell.
DefaultShell
(command)[source]¶ Bases:
clik_shell.BaseShell
Command loop subclass that implements commonly desire facilities.
-
emptyline
()[source]¶ Override that turns an empty line into a no-op.
By default, the command loop runs the previous command when an empty line is received. This is bad default behavior because it’s not what users expect.
If “run the last command” is the desired behavior, you should extend
BaseClass
rather than this class.
-
Internals¶
Clik extension for adding an interactive command shell to an application.
author: | Joe Joyce <joe@decafjoe.com> |
---|---|
copyright: | Copyright (c) Joe Joyce and contributors, 2017-2019. |
license: | BSD |
-
clik_shell.
EXCLUDE
= <object object>¶ Unique object used to indicate that a command should not be present in the shell.
Type: object
-
clik_shell.
get_shell_subcommands_for
(parent_command)[source]¶ Return list of command objects that should be present in the shell.
This excludes the commands that have been marked with
exclude_from_shell()
.Parameters: command ( clik.command.Command
) – Command for which to get shell subcommandsReturns: List of commands that should be present in the shell Return type: list
ofclik.command.Command
instances
-
clik_shell.
parser_for
(*args, **kwds)[source]¶ Context manager that creates a root parser object for
command
.See
make_action_method()
andmake_help_method()
for usage.Parameters: command ( clik.command.Command
) – Command for which to create a parserReturns: Argument parser for the command Return type: argparse.ArgumentParser
-
clik_shell.
make_action_method
(command)[source]¶ Dynamically generate the
do_
method forcommand
.Parameters: command ( clik.command.Command
) – Command for which to generatedo_
methodReturns: Method that calls the given command Return type: fn(self, line)
-
clik_shell.
make_help_method
(command)[source]¶ Dynamically generate the
help_
method forcommand
.Parameters: command ( clik.command.Command
) – Command for which to generatehelp_
methodReturns: Method that prints the help for the given command Return type: fn(self)