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.
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')
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))