Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 59 additions & 15 deletions cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@
from six.moves import zip

# Python 2 urllib2.urlopen() or Python3 urllib.request.urlopen()
# noinspection PyUnresolvedReferences
from six.moves.urllib.request import urlopen

# Python 3 compatability hack due to no built-in file keyword in Python 3
# Due to one occurence of isinstance(<foo>, file) checking to see if something is of file type
try:
# noinspection PyUnboundLocalVariable,PyUnresolvedReferences
file
except NameError:
import io
Expand All @@ -72,6 +74,7 @@
# Detect whether IPython is installed to determine if the built-in "ipy" command should be included
ipython_available = True
try:
# noinspection PyUnresolvedReferences
from IPython import embed
except ImportError:
ipython_available = False
Expand Down Expand Up @@ -124,6 +127,16 @@ def set_use_arg_list(val):


class OptionParser(optparse.OptionParser):
"""Subclase of optparse.OptionParser which stores a reference to the function/method it is parsing options for.

Used mostly for getting access to the do_* method's docstring when printing help.
"""
def __init__(self):
# Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility
optparse.OptionParser.__init__(self)
# The do_* method this class is parsing options for. Used for accessing docstring help.
self._func = None

def exit(self, status=0, msg=None):
self.values._exit = True
if msg:
Expand Down Expand Up @@ -303,6 +316,7 @@ def __init__(self):
if sys.platform == "win32":
# Running on Windows
try:
# noinspection PyUnresolvedReferences
import win32clipboard

def get_paste_buffer():
Expand Down Expand Up @@ -388,6 +402,13 @@ def get_paste_buffer(*args):


class ParsedString(str):
"""Subclass of str which also stores a pyparsing.ParseResults object containing structured parse results."""
# pyarsing.ParseResults - structured parse results, to provide multiple means of access to the parsed data
parsed = None

# Function which did the parsing
parser = None

def full_parsed_statement(self):
new = ParsedString('%s %s' % (self.parsed.command, self.parsed.args))
new.parsed = self.parsed
Expand Down Expand Up @@ -514,7 +535,6 @@ class Cmd(cmd.Cmd):
default_to_shell = False
defaultExtension = 'txt' # For ``save``, ``load``, etc.
excludeFromHistory = '''run r list l history hi ed edit li eof'''.split()
kept_state = None
# make sure your terminators are not in legalChars!
legalChars = u'!#$%.:?@_-' + pyparsing.alphanums + pyparsing.alphas8bit
multilineCommands = []
Expand All @@ -524,7 +544,7 @@ class Cmd(cmd.Cmd):
reserved_words = []
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
terminators = [';']
urlre = re.compile('(https?://[-\\w\\./]+)')
urlre = re.compile('(https?://[-\\w./]+)')

# Attributes which ARE dynamicaly settable at runtime
abbrev = True # Abbreviated commands recognized
Expand Down Expand Up @@ -589,11 +609,11 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
self.initial_stdout = sys.stdout
self.history = History()
self.pystate = {}
# noinspection PyUnresolvedReferences
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()
self._temp_filename = None
self._transcript_files = transcript_files

# Used to enable the ability for a Python script to quit the application
Expand All @@ -606,6 +626,13 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
self._last_result = None

# Used to save state during a redirection
self.kept_state = None
self.kept_sys = None

# Used for a temp file during a pipe (needed tempfile instead of real pipe for Python 3.x prior to 3.5)
self._temp_filename = None

def poutput(self, msg):
"""Convenient shortcut for self.stdout.write(); adds newline if necessary."""
if msg:
Expand Down Expand Up @@ -748,18 +775,23 @@ def _init_parser(self):
pyparsing.Optional(fileName) + (pyparsing.stringEnd | '|')
self.inputParser.ignore(self.commentInProgress)

def preparse(self, raw, **kwargs):
def preparse(self, raw):
return raw

def postparse(self, parseResult):
return parseResult

def parsed(self, raw, **kwargs):
def parsed(self, raw):
""" This function is where the actual parsing of each line occurs.

:param raw: str - the line of text as it was entered
:return: ParsedString - custom subclass of str with extra attributes
"""
if isinstance(raw, ParsedString):
p = raw
else:
# preparse is an overridable hook; default makes no changes
s = self.preparse(raw, **kwargs)
s = self.preparse(raw)
s = self.inputParser.transformString(s.lstrip())
s = self.commentGrammars.transformString(s)
for (shortcut, expansion) in self.shortcuts:
Expand All @@ -777,8 +809,6 @@ def parsed(self, raw, **kwargs):
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):
Expand Down Expand Up @@ -981,6 +1011,7 @@ def _cmdloop(self):
# has been split out so that it can be called separately
if self.use_rawinput and self.completekey:
try:
# noinspection PyUnresolvedReferences
import readline
self.old_completer = readline.get_completer()
readline.set_completer(self.complete)
Expand All @@ -1000,6 +1031,7 @@ def _cmdloop(self):
finally:
if self.use_rawinput and self.completekey:
try:
# noinspection PyUnresolvedReferences
import readline
readline.set_completer(self.old_completer)
except ImportError:
Expand Down Expand Up @@ -1514,6 +1546,7 @@ def run_commands_at_invocation(self, callargs):
return self._STOP_AND_EXIT

def cmdloop(self, intro=None):
callargs = None
if self.allow_cli_args:
parser = optparse.OptionParser()
parser.add_option('-t', '--test', dest='test',
Expand Down Expand Up @@ -1578,6 +1611,15 @@ def to_index(self, raw):
return result

def search(self, target):
""" Search the history for a particular term.

:param target: str - term to search for
:return: List[str] - list of matches
"""
# If there is no history yet, don't try to search through a non-existent list, just return an empty list
if len(self) < 1:
return []

target = target.strip()
if target[0] == target[-1] == '/' and len(target) > 1:
target = target[1:-1]
Expand All @@ -1586,7 +1628,7 @@ def search(self, target):
pattern = re.compile(target, re.IGNORECASE)
return [s for s in self if pattern.search(s)]

spanpattern = re.compile(r'^\s*(?P<start>\-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>\-?\d+)?\s*$')
spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$')

def span(self, raw):
if raw.lower() in ('*', '-', 'all'):
Expand All @@ -1609,7 +1651,7 @@ def span(self, raw):
result.reverse()
return result

rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*\-\s*(?P<end>[\d]+)?\s*$')
rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*-\s*(?P<end>[\d]+)?\s*$')

def append(self, new):
new = HistoryItem(new)
Expand Down Expand Up @@ -1642,6 +1684,7 @@ def get(self, getme=None, fromEnd=False):
end = int(end)
return self[start:end]

# noinspection PyUnresolvedReferences
getme = getme.strip()

if getme.startswith(r'/') and getme.endswith(r'/'):
Expand Down Expand Up @@ -1702,19 +1745,19 @@ def restore(self):


class Borg(object):
'''All instances of any Borg subclass will share state.
from Python Cookbook, 2nd Ed., recipe 6.16'''
"""All instances of any Borg subclass will share state.
from Python Cookbook, 2nd Ed., recipe 6.16"""
_shared_state = {}

def __new__(cls, *a, **k):
obj = object.__new__(cls, *a, **k)
obj = object.__new__(cls)
obj.__dict__ = cls._shared_state
return obj


class OutputTrap(Borg):
'''Instantiate an OutputTrap to divert/capture ALL stdout output. For use in unit testing.
Call `tearDown()` to return to normal output.'''
"""Instantiate an OutputTrap to divert/capture ALL stdout output. For use in unit testing.
Call `tearDown()` to return to normal output."""

def __init__(self):
self.contents = ''
Expand Down Expand Up @@ -1855,4 +1898,5 @@ def __bool__(self):
if __name__ == '__main__':
# If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality.
app = Cmd()
app.debug = True
app.cmdloop()