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 and quit commands to exit the shell
  • EOF handler, which exits the shell on Ctl-D
  • KeyboardInterrupt handler, which exits the shell on Ctl-C
  • cmd.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 the do_* and help_* methods
  • default, which overrides the default cmd.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 becomes my_subcommand
  • A hook is added to cmd.Cmd.default() to recognize my-subcommand and redirect it to my_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 and my_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 function
Returns: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 by clik.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
error(exit_code)[source]

Handle non-zero subcommand exit code.

By default, this prints a generic error message letting the user know the exit code.

Parameters:exit_code (int) – Exit code from the subcommand
Return type:None
prompt = None

Prompt for the command loop. If None, the prompt is set to "name> ", where name is the name of the root command object.

Type:str or None
class clik_shell.DefaultShell(command)[source]

Bases: clik_shell.BaseShell

Command loop subclass that implements commonly desire facilities.

cmdloop()[source]

Override that supports graceful handling of keyboard interrupts.

do_EOF(_)[source]

Exit the shell.

do_exit(_)[source]

Exit the shell.

do_quit(_)[source]

Exit the shell.

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 subcommands
Returns:List of commands that should be present in the shell
Return type:list of clik.command.Command instances
clik_shell.parser_for(*args, **kwds)[source]

Context manager that creates a root parser object for command.

See make_action_method() and make_help_method() for usage.

Parameters:command (clik.command.Command) – Command for which to create a parser
Returns:Argument parser for the command
Return type:argparse.ArgumentParser
clik_shell.make_action_method(command)[source]

Dynamically generate the do_ method for command.

Parameters:command (clik.command.Command) – Command for which to generate do_ method
Returns:Method that calls the given command
Return type:fn(self, line)
clik_shell.make_help_method(command)[source]

Dynamically generate the help_ method for command.

Parameters:command (clik.command.Command) – Command for which to generate help_ method
Returns:Method that prints the help for the given command
Return type:fn(self)

Changelog

0.90.0 – 2017-10-22

  • Initial public release.