# -*- coding: UTF-8 -*- ''' Variant on standard library's cmd with extra features. Simply import cmd2.Cmd instead of cmd.Cmd. Use exactly as you would use the standard library cmd, while enjoying the extra features. • Searchable command history (commands: "hi", "li", "run") • Load commands from file, save to file, edit commands in file • Multi-line commands • Case-insensitive commands • Special-character shortcut commands (beyond cmd's "@" and "!") • Settable environment parameters • Optional _onchange_{paramname} called when environment parameter changes • Parsing commands with `optparse` options (flags) • Redirection to file with >, >>; input from file with < • Easy transcript-based testing of applications (see example/example.py) • Bash-style ``select`` available NOTE: Redirection with > and | will only work if `self.stdout.write()` is used instead of `print`. (The standard library's `cmd` module is written to use `self.stdout.write()`.) --Catherine Devlin catherinedevlin.blogspot.com Jan 03 2008 Mercurial repository: http://www.assembla.com/wiki/show/python-cmd2 ''' #============================ # STANDARD LIBRARY MODULES #============================ import cmd #---------------------------- # General OS/system helpers #---------------------------- import os, \ io, \ platform, \ subprocess, \ tempfile from io import * try: import sys except ImportError as e: pass #---------------------------- # Python code helpers #---------------------------- import copy from optparse import * #---------------------------- # ??? #---------------------------- import datetime, \ urllib try: from urllib import urlopen as urlopen except ImportError: # Python 3 from urllib.request import urlopen as urlopen #---------------------------- # Strings & friends #---------------------------- import glob, \ re try: import unicode except ImportError: # Python 3 basestring = unicode = str #---------------------------- # Testing & Debugging #---------------------------- import doctest, \ unittest, \ traceback from code import (InteractiveConsole, InteractiveInterpreter) #============================ # THIRD-PARTY MODULES #============================ import pyparsing #============================ # LOCAL MODULES #============================ from . import (support, parsers, errors, tests) from .support import * from .parsers import * from .errors import * # (commands.py is imported in the Cmd class) __version__ = '0.7.0' # Refactoring constitutes a version bump...right? if sys.version_info[0] == 2: pyparsing.ParserElement.enablePackrat() # Packrat is causing Python3 errors that I don't understand. # # > /usr/local/Cellar/python3/3.2/lib/python3.2/site-packages/pyparsing-1.5.6-py3.2.egg/pyparsing.py(999)scanString() # -> nextLoc,tokens = parseFn( instring, preloc, callPreParse=False ) # (Pdb) n # NameError: global name 'exc' is not defined # # (Pdb) parseFn # # # Bug report filed: https://sourceforge.net/tracker/?func=detail&atid=617311&aid=3381439&group_id=97203 class Cmd(cmd.Cmd): from . import commands # I *think* this lets commands get their own file # while still limiting their scope to this class... _STOP_AND_EXIT = True # distinguish script's end from actual exit _STOP_SCRIPT_NO_EXIT = -999 # @TODO # Refactor all shell settings into a module or single # data structure (list/dict/whatever). # # (Perhaps call it "shopt"?) # # Currently, the "settable" StubbornDict collects user-settable # options. But wouldn't it make more sense to simply declare # them within the container to start with? abbrev = True # Abbreviated commands recognized case_insensitive = True # Commands recognized regardless of case continuation_prompt = '> ' current_script_dir = None debug = False default_file_name = 'command.txt' # For ``save``, ``load``, etc. default_to_shell = False defaultExtension = 'txt' # For ``save``, ``load``, etc. echo = False excludeFromHistory = '''run r list l history hi ed edit li eof'''.split() feedback_to_output = False # Include nonessentials in >, | output kept_state = None # make sure your terminators are not in legalChars! legalChars = '!#$%.:?@_' + pyparsing.alphanums + pyparsing.alphas8bit locals_in_py = True noSpecialParse = 'set ed edit exit'.split() quiet = False # Do not suppress nonessential output redirector = '>' # For redirecting output to a file reserved_words = [] shortcuts = { '?' : 'help', '!' : 'shell', '@' : 'load', '@@': '_relative_load' } timing = False # Prints elapsed time for each command settable = stubbornDict(''' abbrev Accept abbreviated commands case_insensitive upper- and lower-case both OK colors Colorized output (*nix only) continuation_prompt On 2nd+ line of input debug Show full error stack on error default_file_name for ``save``, ``load``, etc. echo Echo command issued into output editor Program used by ``edit`` feedback_to_output include nonessentials in `|`, `>` results prompt quiet Don't print nonessential feedback timing Report execution times ''') editor = os.environ.get('EDITOR') if not editor: if sys.platform[:3] == 'win': editor = 'notepad' else: for editor in ['gedit', 'kate', 'vim', 'emacs', 'nano', 'pico']: if subprocess.Popen(['which', editor], stdout=subprocess.PIPE).communicate()[0]: break prefixParser = pyparsing.Empty() commentGrammars = pyparsing.Or([pyparsing.pythonStyleComment, pyparsing.cStyleComment]) commentGrammars.addParseAction(lambda x: '') commentInProgress = pyparsing.Literal('/*') + pyparsing.SkipTo(pyparsing.stringEnd ^ '*/') terminators = [';'] blankLinesAllowed = False multilineCommands = [] def __init__(self, *args, **kwargs): # @FIXME # # Add docstring describing what gets __init__'ed, # and what a cmd2 subclass might want to do with # its own __init__. # cmd.Cmd.__init__(self, *args, **kwargs) self.initial_stdout = sys.stdout self.history = History() self.pystate = {} self.shortcuts = sorted(self.shortcuts.items(), reverse=True) self.keywords = self.reserved_words + [fname[3:] for fname in dir(self) if fname.startswith('do_')] self._init_parser() def _cmdloop(self, intro=None): ''' Repeatedly issue a prompt, accept input, parse an initial prefix off the received input, and dispatch to action methods, passing them the remainder of the line as argument. ''' # An almost-perfect copy from Cmd. # # However, the `pseudo_raw_input` portion has been # split out so that it can be called separately. self.preloop() if self.use_rawinput and self.completekey: try: import readline self.old_completer = readline.get_completer() readline.set_completer(self.complete) readline.parse_and_bind(self.completekey + ": complete") except ImportError: pass try: if intro is not None: self.intro = intro if self.intro: self.stdout.write(str(self.intro) + "\n") stop = None while not stop: if self.cmdqueue: line = self.cmdqueue.pop(0) else: line = self.pseudo_raw_input(self.prompt) if (self.echo) and (isinstance(self.stdin, TextIOWrapper)): self.stdout.write(line + '\n') stop = self.onecmd_plus_hooks(line) self.postloop() finally: if self.use_rawinput and self.completekey: try: import readline readline.set_completer(self.old_completer) except ImportError: pass return stop def _default(self, statement): # @FIXME # Add docstring description arg = statement.full_parsed_statement() if self.default_to_shell: result = os.system(arg) if not result: return self.postparsing_postcmd(None) return self.postparsing_postcmd(self.default(arg)) # @TODO # Could this be moved to parser.py? # # That WOULD move it outside this class. But this is an # unbelievably huge chunk block of code dedicated to parsing. # My gut instinct is to keep such large codeblocks separated according # to their role. # # So, that would mean this method lives with other parsing stuff # in parsers.py. def _init_parser(self): r''' >>> c = Cmd() >>> c.multilineCommands = ['multiline'] >>> c.case_insensitive = True >>> c._init_parser() >>> print (c.parser.parseString('').dump()) [] >>> print (c.parser.parseString('').dump()) [] >>> print (c.parser.parseString('/* empty command */').dump()) [] >>> print (c.parser.parseString('plainword').dump()) ['plainword', ''] - command: plainword - statement: ['plainword', ''] - command: plainword >>> print (c.parser.parseString('termbare;').dump()) ['termbare', '', ';', ''] - command: termbare - statement: ['termbare', '', ';'] - command: termbare - terminator: ; - terminator: ; >>> print (c.parser.parseString('termbare; suffx').dump()) ['termbare', '', ';', 'suffx'] - command: termbare - statement: ['termbare', '', ';'] - command: termbare - terminator: ; - suffix: suffx - terminator: ; >>> print (c.parser.parseString('barecommand').dump()) ['barecommand', ''] - command: barecommand - statement: ['barecommand', ''] - command: barecommand >>> print (c.parser.parseString('COMmand with args').dump()) ['command', 'with args'] - args: with args - command: command - statement: ['command', 'with args'] - args: with args - command: command >>> print (c.parser.parseString('command with args and terminator; and suffix').dump()) ['command', 'with args and terminator', ';', 'and suffix'] - args: with args and terminator - command: command - statement: ['command', 'with args and terminator', ';'] - args: with args and terminator - command: command - terminator: ; - suffix: and suffix - terminator: ; >>> print (c.parser.parseString('simple | piped').dump()) ['simple', '', '|', ' piped'] - command: simple - pipeTo: piped - statement: ['simple', ''] - command: simple >>> print (c.parser.parseString('double-pipe || is not a pipe').dump()) ['double', '-pipe || is not a pipe'] - args: -pipe || is not a pipe - command: double - statement: ['double', '-pipe || is not a pipe'] - args: -pipe || is not a pipe - command: double >>> print (c.parser.parseString('command with args, terminator;sufx | piped').dump()) ['command', 'with args, terminator', ';', 'sufx', '|', ' piped'] - args: with args, terminator - command: command - pipeTo: piped - statement: ['command', 'with args, terminator', ';'] - args: with args, terminator - command: command - terminator: ; - suffix: sufx - terminator: ; >>> print (c.parser.parseString('output into > afile.txt').dump()) ['output', 'into', '>', 'afile.txt'] - args: into - command: output - output: > - outputTo: afile.txt - statement: ['output', 'into'] - args: into - command: output >>> print (c.parser.parseString('output into;sufx | pipethrume plz > afile.txt').dump()) ['output', 'into', ';', 'sufx', '|', ' pipethrume plz', '>', 'afile.txt'] - args: into - command: output - output: > - outputTo: afile.txt - pipeTo: pipethrume plz - statement: ['output', 'into', ';'] - args: into - command: output - terminator: ; - suffix: sufx - terminator: ; >>> print (c.parser.parseString('output to paste buffer >> ').dump()) ['output', 'to paste buffer', '>>', ''] - args: to paste buffer - command: output - output: >> - statement: ['output', 'to paste buffer'] - args: to paste buffer - command: output >>> print (c.parser.parseString('ignore the /* commented | > */ stuff;').dump()) ['ignore', 'the /* commented | > */ stuff', ';', ''] - args: the /* commented | > */ stuff - command: ignore - statement: ['ignore', 'the /* commented | > */ stuff', ';'] - args: the /* commented | > */ stuff - command: ignore - terminator: ; - terminator: ; >>> print (c.parser.parseString('has > inside;').dump()) ['has', '> inside', ';', ''] - args: > inside - command: has - statement: ['has', '> inside', ';'] - args: > inside - command: has - terminator: ; - terminator: ; >>> print (c.parser.parseString('multiline has > inside an unfinished command').dump()) ['multiline', ' has > inside an unfinished command'] - multilineCommand: multiline >>> print (c.parser.parseString('multiline has > inside;').dump()) ['multiline', 'has > inside', ';', ''] - args: has > inside - multilineCommand: multiline - statement: ['multiline', 'has > inside', ';'] - args: has > inside - multilineCommand: multiline - terminator: ; - terminator: ; >>> print (c.parser.parseString('multiline command /* with comment in progress;').dump()) ['multiline', ' command /* with comment in progress;'] - multilineCommand: multiline >>> print (c.parser.parseString('multiline command /* with comment complete */ is done;').dump()) ['multiline', 'command /* with comment complete */ is done', ';', ''] - args: command /* with comment complete */ is done - multilineCommand: multiline - statement: ['multiline', 'command /* with comment complete */ is done', ';'] - args: command /* with comment complete */ is done - multilineCommand: multiline - terminator: ; - terminator: ; >>> print (c.parser.parseString('multiline command ends\n\n').dump()) ['multiline', 'command ends', '\n', '\n'] - args: command ends - multilineCommand: multiline - statement: ['multiline', 'command ends', '\n', '\n'] - args: command ends - multilineCommand: multiline - terminator: ['\n', '\n'] - terminator: ['\n', '\n'] >>> print (c.parser.parseString('multiline command "with term; ends" now\n\n').dump()) ['multiline', 'command "with term; ends" now', '\n', '\n'] - args: command "with term; ends" now - multilineCommand: multiline - statement: ['multiline', 'command "with term; ends" now', '\n', '\n'] - args: command "with term; ends" now - multilineCommand: multiline - terminator: ['\n', '\n'] - terminator: ['\n', '\n'] >>> print (c.parser.parseString('what if "quoted strings /* seem to " start comments?').dump()) ['what', 'if "quoted strings /* seem to " start comments?'] - args: if "quoted strings /* seem to " start comments? - command: what - statement: ['what', 'if "quoted strings /* seem to " start comments?'] - args: if "quoted strings /* seem to " start comments? - command: what ''' #outputParser = (pyparsing.Literal('>>') | (pyparsing.WordStart() + '>') | pyparsing.Regex('[^=]>'))('output') outputParser = (pyparsing.Literal(self.redirector *2) | \ (pyparsing.WordStart() + self.redirector) | \ pyparsing.Regex('[^=]' + self.redirector))('output') terminatorParser = pyparsing.Or([(hasattr(t, 'parseString') and t) or pyparsing.Literal(t) for t in self.terminators])('terminator') stringEnd = pyparsing.stringEnd ^ '\nEOF' self.multilineCommand = pyparsing.Or([pyparsing.Keyword(c, caseless=self.case_insensitive) for c in self.multilineCommands])('multilineCommand') oneLineCommand = (~self.multilineCommand + pyparsing.Word(self.legalChars))('command') pipe = pyparsing.Keyword('|', identChars='|') self.commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '') doNotParse = self.commentGrammars | self.commentInProgress | pyparsing.quotedString afterElements = \ pyparsing.Optional( \ pipe + \ pyparsing.SkipTo(outputParser ^ stringEnd, ignore=doNotParse)('pipeTo') ) + \ pyparsing.Optional( \ outputParser + \ pyparsing.SkipTo(stringEnd, ignore=doNotParse).setParseAction(lambda x: x[0].strip())('outputTo') ) if self.case_insensitive: self.multilineCommand.setParseAction(lambda x: x[0].lower()) oneLineCommand.setParseAction(lambda x: x[0].lower()) if self.blankLinesAllowed: self.blankLineTerminationParser = pyparsing.NoMatch else: self.blankLineTerminator = (pyparsing.lineEnd + pyparsing.lineEnd)('terminator') self.blankLineTerminator.setResultsName('terminator') self.blankLineTerminationParser = ((self.multilineCommand ^ oneLineCommand) + pyparsing.SkipTo(self.blankLineTerminator, ignore=doNotParse).setParseAction(lambda x: x[0].strip())('args') + self.blankLineTerminator)('statement') self.multilineParser = (((self.multilineCommand ^ oneLineCommand) + pyparsing.SkipTo(terminatorParser, ignore=doNotParse).setParseAction(lambda x: x[0].strip())('args') + terminatorParser)('statement') + pyparsing.SkipTo(outputParser ^ pipe ^ stringEnd, ignore=doNotParse).setParseAction(lambda x: x[0].strip())('suffix') + afterElements) self.multilineParser.ignore(self.commentInProgress) self.singleLineParser = ((oneLineCommand + pyparsing.SkipTo(terminatorParser ^ stringEnd ^ pipe ^ outputParser, ignore=doNotParse).setParseAction(lambda x:x[0].strip())('args'))('statement') + pyparsing.Optional(terminatorParser) + afterElements) #self.multilineParser = self.multilineParser.setResultsName('multilineParser') #self.singleLineParser = self.singleLineParser.setResultsName('singleLineParser') self.blankLineTerminationParser = self.blankLineTerminationParser.setResultsName('statement') self.parser = self.prefixParser + ( stringEnd | self.multilineParser | self.singleLineParser | self.blankLineTerminationParser | self.multilineCommand + pyparsing.SkipTo(stringEnd, ignore=doNotParse) ) self.parser.ignore(self.commentGrammars) inputMark = pyparsing.Literal('<') inputMark.setParseAction(lambda x: '') fileName = pyparsing.Word(self.legalChars + '/\\') inputFrom = fileName('inputFrom') inputFrom.setParseAction(replace_with_file_contents) # A not-entirely-satisfactory way of distinguishing < as in "import from" # from < as in "lesser than" self.inputParser = inputMark + \ pyparsing.Optional(inputFrom) + \ pyparsing.Optional('>') + \ pyparsing.Optional(fileName) + \ (pyparsing.stringEnd | '|') self.inputParser.ignore(self.commentInProgress) def preparse(self, raw, **kwargs): # @FIXME # Add docstring description return raw def postparse(self, parseResult): # @FIXME # Add docstring description return parseResult def parsed(self, raw, **kwargs): # @FIXME # Add docstring description if isinstance(raw, ParsedString): p = raw else: # preparse is an overridable hook; default makes no changes s = self.preparse(raw, **kwargs) s = self.inputParser.transformString(s.lstrip()) s = self.commentGrammars.transformString(s) for (shortcut, expansion) in self.shortcuts: if s.lower().startswith(shortcut): s = s.replace(shortcut, expansion + ' ', 1) break result = self.parser.parseString(s) result['raw'] = raw result['command'] = result.multilineCommand or result.command result = self.postparse(result) p = ParsedString(result.args) p.parsed = result p.parser = self.parsed for (key, val) in kwargs.items(): p.parsed[key] = val return p def postparsing_precmd(self, statement): # @FIXME # Add docstring description stop = 0 return stop, statement def postparsing_postcmd(self, stop): # @FIXME # Add docstring description return stop def poutput(self, msg): ''' Convenience shortcut for self.stdout.write(). Adds newline if necessary. ''' if msg: self.stdout.write(msg) if msg[-1] != '\n': self.stdout.write('\n') def perror(self, errmsg, statement=None): # @FIXME # Add docstring description if self.debug: traceback.print_exc() print( str(errmsg) ) def pfeedback(self, msg): ''' Prints nonessential feedback. Can be silenced with `quiet`. Inclusion in redirected output is controlled by `feedback_to_output`. ''' if not self.quiet: if self.feedback_to_output: self.poutput(msg) else: print (msg) colorcodes = { 'bold' :{True:'\x1b[1m' ,False:'\x1b[22m'}, 'cyan' :{True:'\x1b[36m',False:'\x1b[39m'}, 'blue' :{True:'\x1b[34m',False:'\x1b[39m'}, 'red' :{True:'\x1b[31m',False:'\x1b[39m'}, 'magenta' :{True:'\x1b[35m',False:'\x1b[39m'}, 'green' :{True:'\x1b[32m',False:'\x1b[39m'}, 'underline' :{True:'\x1b[4m' ,False:'\x1b[24m'} } colors = (platform.system() != 'Windows') def colorize(self, val, color): ''' Wraps the provided string in UNIX-style special characters that turn on (and then off) text color and style. The string is returned unchanged if the ``colors`` environment variable is ``False`` or the application is running on Windows. ``color`` should be one of the supported strings (or styles): red/blue/green/cyan/magenta, bold, underline ''' if self.colors and (self.stdout == self.initial_stdout): return self.colorcodes[color][True] + val + self.colorcodes[color][False] return val def func_named(self, arg): ''' This method is responsible for locating `do_*` for commands. ''' result = None target = 'do_' + arg if target in dir(self): result = target else: if self.abbrev: # accept shortened versions of commands funcs = [fname for fname in self.keywords if fname.startswith(arg)] if len(funcs) == 1: result = 'do_' + funcs[0] return result def complete_statement(self, line): ''' Continue accepting lines of input until the command is complete. ''' if (not line) or ( \ not pyparsing.Or(self.commentGrammars).setParseAction(lambda x: '').transformString(line) \ ): raise EmptyStatement statement = self.parsed(line) while statement.parsed.multilineCommand and (statement.parsed.terminator == ''): statement = '%s\n%s' % (statement.parsed.raw, self.pseudo_raw_input(self.continuation_prompt)) statement = self.parsed(statement) if not statement.parsed.command: raise EmptyStatement return statement def redirect_output(self, statement): # @FIXME # Add docstring description if statement.parsed.pipeTo: self.kept_state = Statekeeper(self, ('stdout',)) self.kept_sys = Statekeeper(sys, ('stdout',)) self.redirect = subprocess.Popen(statement.parsed.pipeTo, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE) sys.stdout = self.stdout = self.redirect.stdin elif statement.parsed.output: if (not statement.parsed.outputTo) and (not can_clip): raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable') self.kept_state = Statekeeper(self, ('stdout',)) self.kept_sys = Statekeeper(sys, ('stdout',)) if statement.parsed.outputTo: mode = 'w' if statement.parsed.output == 2 * self.redirector: mode = 'a' sys.stdout = self.stdout = open(os.path.expanduser(statement.parsed.outputTo), mode) else: sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+") if statement.parsed.output == '>>': self.stdout.write(get_paste_buffer()) def restore_output(self, statement): # @FIXME # Add docstring description if self.kept_state: if statement.parsed.output: if not statement.parsed.outputTo: self.stdout.seek(0) write_to_paste_buffer(self.stdout.read()) elif statement.parsed.pipeTo: for result in self.redirect.communicate(): self.kept_state.stdout.write(result or '') self.stdout.close() self.kept_state.restore() self.kept_sys.restore() self.kept_state = None def onecmd(self, line): ''' Interpret the argument as though it had been typed in response to the prompt. This may be overridden, but shouldn't normally need to be. See the precmd() and postcmd() methods for useful execution hooks. The return value is a flag indicating whether interpretation of commands by the interpreter should stop. This (`cmd2`) version of `onecmd` already override's `cmd`'s `onecmd`. ''' statement = self.parsed(line) self.lastcmd = statement.parsed.raw funcname = self.func_named(statement.parsed.command) if not funcname: return self._default(statement) try: func = getattr(self, funcname) except AttributeError: return self._default(statement) stop = func(statement) return stop def onecmd_plus_hooks(self, line): # @FIXME # Add docstring description # The outermost level of try/finally nesting can be condensed once # Python 2.4 support can be dropped. # # @TODO Do you think we can safely drop Python 2.4 support yet? :-) stop = 0 try: try: statement = self.complete_statement(line) (stop, statement) = self.postparsing_precmd(statement) if stop: return self.postparsing_postcmd(stop) if statement.parsed.command not in self.excludeFromHistory: self.history.append(statement.parsed.raw) try: self.redirect_output(statement) timestart = datetime.datetime.now() statement = self.precmd(statement) stop = self.onecmd(statement) stop = self.postcmd(stop, statement) if self.timing: self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart)) finally: self.restore_output(statement) except EmptyStatement: return 0 except Exception as e: self.perror(str(e), statement) finally: return self.postparsing_postcmd(stop) def pseudo_raw_input(self, prompt): ''' Copied from cmd's cmdloop. Similar to raw_input, but accounts for changed stdin/stdout. ''' if self.use_rawinput: try: line = input(prompt) except EOFError: line = 'EOF' else: self.stdout.write(prompt) self.stdout.flush() line = self.stdin.readline() if not len(line): line = 'EOF' else: if line[-1] == '\n': # this was always true in Cmd line = line[:-1] return line def select(self, options, prompt='Your choice? '): ''' Presents a numbered menu to the user. Modeled after the bash shell's SELECT. Returns the item chosen. Argument ``options`` can be: | a single string -> splits into one-word options | a list of strings -> will be offered as options | a list of tuples -> interpreted as (value, text), so that the return value can differ from the text advertised to the user ''' if isinstance(options, basestring): options = zip(options.split(), options.split()) fulloptions = [] for opt in options: if isinstance(opt, basestring): fulloptions.append((opt, opt)) else: try: fulloptions.append((opt[0], opt[1])) except IndexError: fulloptions.append((opt[0], opt[0])) for (idx, (value, text)) in enumerate(fulloptions): self.poutput(' %2d. %s\n' % (idx+1, text)) while True: response = input(prompt) try: response = int(response) result = fulloptions[response - 1][0] break except ValueError: pass # loop and ask again return result def last_matching(self, arg): # @FIXME # Add docstring description try: if arg: return self.history.get(arg)[-1] else: return self.history[-1] except IndexError: return None def read_file_or_url(self, fname): # @FIXME # Add docstring description # @TODO : not working on localhost if isinstance(fname, TextIOWrapper): result = open(fname, 'r') else: match = self.urlre.match(fname) if match: result = urlopen(match.group(1)) else: fname = os.path.expanduser(fname) try: result = open(os.path.expanduser(fname), 'r') except IOError: result = open('{}.{}'.format(os.path.expanduser(fname) , self.defaultExtension) , 'r') return result def fileimport(self, statement, source): # @FIXME # Add docstring try: f = open(os.path.expanduser(source)) except IOError: self.stdout.write("Couldn't read from file {}\n".format(source)) return '' data = f.read() f.close() return data def runTranscriptTests(self, callargs): # @FIXME # Add docstring from .tests import * class TestMyAppCase(Cmd2TestCase): CmdApp = self.__class__ self.__class__.testfiles = callargs sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main() testcase = TestMyAppCase() runner = unittest.TextTestRunner() result = runner.run(testcase) result.printErrors() def run_commands_at_invocation(self, callargs): # @FIXME # Add docstring for initial_command in callargs: if self.onecmd_plus_hooks(initial_command + '\n'): return self._STOP_AND_EXIT def cmdloop(self): # @FIXME # Add docstring description parser = optparse.OptionParser() parser.add_option('-t', '--test', dest ='test', action ='store_true', help ='Test against transcript(s) in FILE (wildcards OK)') (callopts, callargs) = parser.parse_args() if callopts.test: self.runTranscriptTests(callargs) else: if not self.run_commands_at_invocation(callargs): self._cmdloop() if __name__ == '__main__': # This is only run when this file is called as an executable. doctest.testmod(optionflags = doctest.NORMALIZE_WHITESPACE) ''' To make your application transcript-testable, replace :: app = MyApp() app.cmdloop() with :: app = MyApp() cmd2.run(app) Then run a session of your application and paste the entire screen contents into a file, ``transcript.test``, and invoke the test like:: python myapp.py --test transcript.test Wildcards can be used to test against multiple transcript files. '''