#!/usr/bin/env python
# -*- coding: utf-8; mode: python -*-
# pylint: disable=invalid-name
u"""
A command line interface made simple.
"""
# ==============================================================================
# imports
# ==============================================================================
import os
import textwrap
import sys
import argparse
import inspect
import code
import linecache
from . import compat
from .os_env import OS_ENV
VERBOSE = False
DEBUG = bool(OS_ENV.get("DEBUG", False))
QUIET = False
[docs]class CLIApplError(Exception):
"""Exception raised for errors in the application"""
# pylint: disable=super-init-not-called
def __init__(self, exitCode, message):
self.exitCode = exitCode
self.message = message
# ==============================================================================
[docs]class CLI(object):
# ==============================================================================
u"""A comfortable command line."""
OUT = sys.__stdout__
ERR = sys.__stderr__
def __init__(self, *args, **kwargs):
kwargs["formatter_class"] = HelpFormatter
self.cmdFunc = kwargs.pop("cmdFunc", None)
self.cliSubParsers = None
if self.cmdFunc is not None:
kwargs["epilog"] = kwargs.get("epilog", self.cmdFunc.__doc__)
kwargs["description"] = kwargs.get(
"description"
, kwargs["epilog"].strip().split("\n")[0] if kwargs["epilog"] else None)
self.parser = argparse.ArgumentParser(*args, **kwargs)
if self.cmdFunc is None:
self.cliSubParsers = self.parser.add_subparsers(title='commands', dest='command')
self.cliSubParsers.required = True
self.add_argument = self.parser.add_argument
self.add_argument(
'--debug'
, action = 'store_true'
, help = 'run in debug mode' )
self.add_argument(
'--verbose'
, action = 'store_true'
, help = 'run in verbose mode' )
self.add_argument(
'--quiet'
, action = 'store_true'
, help = 'run in quiet mode' )
[docs] def addCMDParser(self, func, cmdName=None):
"""Add subcommand parser"""
if self.cliSubParsers is None:
raise Exception("this command-line has no sub-commands!")
subCmd = self.cliSubParsers.add_parser(
cmdName or func.__name__
, epilog = func.__doc__
, formatter_class = HelpFormatter
, help = ((func.__doc__ or "").strip().split("\n") + [ "sorry, no help available" ])[0]
)
subCmd.set_defaults(func=func)
return subCmd
def __call__(self):
_exitCode = 0
_exception = None
_retVal = None
self.autocomplete()
cmd_args = self.parser.parse_args()
cmd_args.CLI = self
cmd_args.OUT = self.OUT
cmd_args.ERR = self.ERR
cmd_args.Error = CLIApplError
if OS_ENV.get("DEBUG", None):
cmd_args.debug = True
# pylint: disable=W0603
global DEBUG, VERBOSE, QUIET
DEBUG = cmd_args.debug
VERBOSE = cmd_args.verbose
QUIET = cmd_args.quiet
if DEBUG:
self.OUT.write(u"argparse --> %s\n" % cmd_args)
__builtins__["CONSOLE"] = CONSOLE
try:
if self.cmdFunc is None:
_retVal = cmd_args.func(cmd_args)
else:
_retVal = self.cmdFunc(cmd_args)
try:
if _retVal is None:
_exitCode = 0
else:
_exitCode = int(_retVal)
except Exception as exc: # pylint: disable=W0703
pass
except CLIApplError as exc: # pylint: disable=W0703
_exitCode = exc.exitCode
self.ERR.write(u"ERROR (%s): %s\n" % (exc.exitCode, exc.message))
except Exception as exc: # pylint: disable=W0703
if cmd_args.debug:
raise
_exitCode = 42
_exception = str(exc)
sys.stderr.write(u"FATAL ERROR: %s\n" % _exception)
sys.exit(_exitCode)
[docs] def autocomplete(self):
u"""bash completion
To get in use of bash completion, install ``argcomplete``:
.. code-block:: bash
pip install argcomplete
and add the following to your ~/.bashrc:
.. code-block:: bash
function _py_argcomplete() {
local IFS=$(echo -e '\\v')
COMPREPLY=( $(IFS="$IFS" \\
COMP_LINE="$COMP_LINE" \\
COMP_POINT="$COMP_POINT" \\
_ARGCOMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" \\
_ARGCOMPLETE=1 \\
"$1" 8>&1 9>&2 1>/dev/null) )
if [[ $? != 0 ]]; then
unset COMPREPLY
fi
}
complete -o nospace -o default -F _py_argcomplete myCommandName
..
"""
# only complete when called from _py_argcomplete()
if '_ARGCOMPLETE' not in os.environ:
return
try:
import argcomplete # pylint: disable=import-error
except ImportError:
self.ERR.write("TAB-completion, python-argcomplete not installed.")
sys.exit(1)
argcomplete.autocomplete(self.parser)
[docs]def CONSOLE(arround=5, frame=None):
u"""
2cent debugging & introspection
"""
# pylint: disable=C0321,C0410
sys.stderr.flush()
sys.stdout.flush()
frame = frame or inspect.currentframe().f_back
fName = frame.f_code.co_filename
lineNo = frame.f_lineno
ns = dict(**frame.f_globals)
ns.update(**frame.f_locals)
histfile = os.path.join(os.path.expanduser("~"), ".kernel-doc-history")
try:
import readline, rlcompleter # pylint: disable=W0612
readline.set_completer(rlcompleter.Completer(namespace=ns).complete)
readline.parse_and_bind("tab: complete")
readline.set_history_length(1000)
if os.path.exists(histfile):
readline.read_history_file(histfile)
except ImportError:
readline = None
lines = []
for c in range(lineNo - arround, lineNo + arround):
if c > 0:
prefix = "%-04s|" % c
if c == lineNo: prefix = "---->"
line = linecache.getline(fName, c, frame.f_globals)
if line != '': lines.append(prefix + line)
else:
if lines: lines[-1] = lines[-1] + "<EOF>\n"
break
banner = "".join(lines) + "file: %s:%s\n" % (fName, lineNo)
try:
code.interact(banner=banner, local=ns)
finally:
if readline is not None:
readline.write_history_file(histfile)
[docs]def DUMMY_CONSOLE(*_x, **_y):
u"""A dummy console, spid out warnings for usage of 'CONSOLE' without activated DEBUG
environment.
"""
frame = inspect.currentframe().f_back
fName = frame.f_code.co_filename
lineNo = frame.f_lineno
sys.stderr.write("%s:%s: [WARNING] usage of CONSOLE / debug not activated!\n" % (fName, lineNo))
__builtins__["CONSOLE"] = DUMMY_CONSOLE
if DEBUG:
__builtins__["CONSOLE"] = CONSOLE