NICKPODD

Simple Shell CLI for Python


Through the work that I do I often find the need to have a simple shell script that can have commands run that I wish. Sometimes though I also need to play around with a set of functions and data in python and a CLI is needed.

Unfortunately in python we have the "cmd" module for the CLI interface and "argparse" for the shell script interface but there isn't a unified way to have the same functions work for both.

Yes, it is possible to have a separate module be referred to by both. And yes, there is the "click" python module that does provide alot of this functionality.

But, if you want to have a lightweight wrapper over the "cmd" module using the "argparse" input over the same defined functions then the code below provides just that.

CliShell.py

The following python module allows you to use both shell and CLI functionality combined in python. If no arguments are provided to the script then it will run in CLI mode. If an argument is supplied then it will run in shell mode and work like a simple shell script.

This has the benefit of allowing the CLI platground and the simplicity of the shell work scripts without a large overhead that other modules have.

import cmd
import argparse
import sys

def help_func_builder(text):
    def help_func(args):
        text()
        return False
    return help_func


class Shell(cmd.Cmd, object):
    prompt = ">> "

    def cmdloop(self, intro=None):
        while True:
            try:
                super(Shell, self).cmdloop(intro)
                break
            except KeyboardInterrupt:
                print("^C\n   Exiting...")
                break

    def preloop(self):
        """Initialization before prompting user for commands.
           Despite the claims in the Cmd documentaion, Cmd.preloop() is not a stub.
        """
        cmd.Cmd.preloop(self)   ## sets up command completion
        self._hist    = []      ## No history yet

    def precmd(self, line):
        """ This method is called after the line has been input but before
            it has been interpreted. If you want to modifdy the input line
            before execution (for example, variable substitution) do it here.
        """
        self._hist += [ line.strip() ]
        return line

    def postloop(self):
        """Take care of any unfinished business.
           Despite the claims in the Cmd documentaion, Cmd.postloop() is not a stub.
        """
        cmd.Cmd.postloop(self)   ## Clean up command completion
        print("   Exiting...")

    ## Command definitions ##
    def do_hist(self, args):
        """Print a list of commands that have been entered"""
        print("\n".join(self._hist))

    def do_exit(self, args):
        """Exits from the console"""
        return True


class CliShell():
    def __init__(self):
        self.commands = []

    def command(self, cmd_args=None):
        c_args = {} if cmd_args is None else cmd_args

        def wrapper(func):

            parser = argparse.ArgumentParser(prog=func.__name__ ,description=func.__doc__, add_help=False)
            for xk, xv in c_args.items():
                parser.add_argument(xk, **xv)

            def anon(self, args):
                try:
                    out = parser.parse_args(args.split())
                    func(**vars(out))
                except SystemExit as e:
                    pass

            self.commands.append({
                "name":func.__name__,
                "method": anon,
                "doc": parser.print_help,
                "parser": parser
            })
            return func

        return wrapper

    def get_shell(self):

        methods = self.commands

        d = {}
        for func_desc in methods:
            d['do_' + func_desc['name']] = func_desc['method']
            d['help_' + func_desc['name']] = help_func_builder(func_desc['doc'])

        return type(self.__class__.__name__ + '_Shell',(Shell,),d)()

    def run_cli(self):
        the_args = sys.argv # first arg is file
        # Run shell if no second arguments
        if len(the_args) == 1:
            self.get_shell().cmdloop()
            return 0

        # Get function name
        func_name = the_args[1]

        # First argument is function name
        for func_desc in self.commands:
            if func_name == func_desc['name']:
                func_desc['method'](None," ".join(the_args[2:]))
                return 0

        print('No such command')

Example

The following example is a CLI/Shell script that allows the entering of a numeric value that is then converted to a 2 decimal place price.

from shell import CliShell

CLI = CliShell()

@CLI.command(cmd_args={'price':{'type':float, 'default':0, 'nargs':'?'}})
def aaa(price:float):
    "Hurrah"
    print("price is $" + '{0:.2f}'.format(price))




See me at Linked In See me at Google Plus

Page by: Nicholas Podd , Nick Podd
Love you Emilia