Skip to content

Commit bfd5061

Browse files
authored
Merge pull request #110 from python-cmd2/extract_parser_methods
Extract parser methods
2 parents 387eaa2 + 3ebeefc commit bfd5061

2 files changed

Lines changed: 160 additions & 116 deletions

File tree

cmd2.py

Lines changed: 153 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -576,20 +576,24 @@ class Cmd(cmd.Cmd):
576576
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
577577
"""
578578
# Attributes which are NOT dynamically settable at runtime
579-
allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
580-
allow_redirection = True # Should output redirection and pipes be allowed
579+
580+
allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
581+
allow_redirection = True # Should output redirection and pipes be allowed
581582
blankLinesAllowed = False
582-
commentGrammars = pyparsing.Or([pyparsing.pythonStyleComment, pyparsing.cStyleComment])
583-
commentGrammars.addParseAction(lambda x: '')
583+
commentGrammars = pyparsing.Or(
584+
[pyparsing.pythonStyleComment, pyparsing.cStyleComment]
585+
)
584586
commentInProgress = pyparsing.Literal('/*') + pyparsing.SkipTo(pyparsing.stringEnd ^ '*/')
585-
default_to_shell = False
586-
defaultExtension = 'txt' # For ``save``, ``load``, etc.
587+
588+
default_to_shell = False # Attempt to run unrecognized commands as shell commands
589+
defaultExtension = 'txt' # For ``save``, ``load``, etc.
587590
excludeFromHistory = '''run r list l history hi ed edit li eof'''.split()
591+
588592
# make sure your terminators are not in legalChars!
589593
legalChars = u'!#$%.:?@_-' + pyparsing.alphanums + pyparsing.alphas8bit
590594
multilineCommands = [] # NOTE: Multiline commands can never be abbreviated, even if abbrev is True
591595
prefixParser = pyparsing.Empty()
592-
redirector = '>' # for sending output to file
596+
redirector = '>' # for sending output to file
593597
reserved_words = []
594598
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
595599
terminators = [';']
@@ -655,14 +659,18 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
655659
# Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility
656660
cmd.Cmd.__init__(self, completekey=completekey, stdin=stdin, stdout=stdout)
657661

662+
self._finalize_app_parameters()
658663
self.initial_stdout = sys.stdout
659664
self.history = History()
660665
self.pystate = {}
661666
# noinspection PyUnresolvedReferences
662-
self.shortcuts = sorted(self.shortcuts.items(), reverse=True)
663667
self.keywords = self.reserved_words + [fname[3:] for fname in dir(self)
664668
if fname.startswith('do_')]
665-
self._init_parser()
669+
self.parser_manager = ParserManager(redirector=self.redirector, terminators=self.terminators, multilineCommands=self.multilineCommands,
670+
legalChars=self.legalChars, commentGrammars=self.commentGrammars,
671+
commentInProgress=self.commentInProgress, case_insensitive=self.case_insensitive,
672+
blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser,
673+
preparse=self.preparse, postparse=self.postparse, shortcuts=self.shortcuts)
666674
self._transcript_files = transcript_files
667675

668676
# Used to enable the ability for a Python script to quit the application
@@ -700,6 +708,10 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, use_ipython=False
700708

701709
# ----- Methods related to presenting output to the user -----
702710

711+
def _finalize_app_parameters(self):
712+
self.commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '')
713+
self.shortcuts = sorted(self.shortcuts.items(), reverse=True)
714+
703715
def poutput(self, msg):
704716
"""Convenient shortcut for self.stdout.write(); adds newline if necessary."""
705717
if msg:
@@ -749,77 +761,6 @@ def colorize(self, val, color):
749761
return self._colorcodes[color][True] + val + self._colorcodes[color][False]
750762
return val
751763

752-
# ----- Methods related to pyparsing parsing logic -----
753-
754-
def _init_parser(self):
755-
""" Initializes everything related to pyparsing. """
756-
# output_parser = (pyparsing.Literal('>>') | (pyparsing.WordStart() + '>') | pyparsing.Regex('[^=]>'))('output')
757-
output_parser = (pyparsing.Literal(self.redirector * 2) |
758-
(pyparsing.WordStart() + self.redirector) |
759-
pyparsing.Regex('[^=]' + self.redirector))('output')
760-
761-
terminator_parser = pyparsing.Or(
762-
[(hasattr(t, 'parseString') and t) or pyparsing.Literal(t) for t in self.terminators])('terminator')
763-
string_end = pyparsing.stringEnd ^ '\nEOF'
764-
self.multilineCommand = pyparsing.Or(
765-
[pyparsing.Keyword(c, caseless=self.case_insensitive) for c in self.multilineCommands])('multilineCommand')
766-
oneline_command = (~self.multilineCommand + pyparsing.Word(self.legalChars))('command')
767-
pipe = pyparsing.Keyword('|', identChars='|')
768-
self.commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '')
769-
do_not_parse = self.commentGrammars | self.commentInProgress | pyparsing.quotedString
770-
after_elements = \
771-
pyparsing.Optional(pipe + pyparsing.SkipTo(output_parser ^ string_end, ignore=do_not_parse)('pipeTo')) + \
772-
pyparsing.Optional(output_parser +
773-
pyparsing.SkipTo(string_end,
774-
ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('outputTo'))
775-
if self.case_insensitive:
776-
self.multilineCommand.setParseAction(lambda x: x[0].lower())
777-
oneline_command.setParseAction(lambda x: x[0].lower())
778-
if self.blankLinesAllowed:
779-
self.blankLineTerminationParser = pyparsing.NoMatch
780-
else:
781-
self.blankLineTerminator = (pyparsing.lineEnd + pyparsing.lineEnd)('terminator')
782-
self.blankLineTerminator.setResultsName('terminator')
783-
self.blankLineTerminationParser = ((self.multilineCommand ^ oneline_command) +
784-
pyparsing.SkipTo(self.blankLineTerminator,
785-
ignore=do_not_parse).setParseAction(
786-
lambda x: x[0].strip())('args') +
787-
self.blankLineTerminator)('statement')
788-
self.multilineParser = (((self.multilineCommand ^ oneline_command) +
789-
pyparsing.SkipTo(terminator_parser,
790-
ignore=do_not_parse).setParseAction(
791-
lambda x: x[0].strip())('args') + terminator_parser)('statement') +
792-
pyparsing.SkipTo(output_parser ^ pipe ^ string_end, ignore=do_not_parse).setParseAction(
793-
lambda x: x[0].strip())('suffix') + after_elements)
794-
self.multilineParser.ignore(self.commentInProgress)
795-
self.singleLineParser = ((oneline_command +
796-
pyparsing.SkipTo(terminator_parser ^ string_end ^ pipe ^ output_parser,
797-
ignore=do_not_parse).setParseAction(
798-
lambda x: x[0].strip())('args'))('statement') +
799-
pyparsing.Optional(terminator_parser) + after_elements)
800-
# self.multilineParser = self.multilineParser.setResultsName('multilineParser')
801-
# self.singleLineParser = self.singleLineParser.setResultsName('singleLineParser')
802-
self.blankLineTerminationParser = self.blankLineTerminationParser.setResultsName('statement')
803-
self.parser = self.prefixParser + (
804-
string_end |
805-
self.multilineParser |
806-
self.singleLineParser |
807-
self.blankLineTerminationParser |
808-
self.multilineCommand + pyparsing.SkipTo(string_end, ignore=do_not_parse)
809-
)
810-
self.parser.ignore(self.commentGrammars)
811-
812-
input_mark = pyparsing.Literal('<')
813-
input_mark.setParseAction(lambda x: '')
814-
file_name = pyparsing.Word(self.legalChars + '/\\')
815-
input_from = file_name('inputFrom')
816-
input_from.setParseAction(replace_with_file_contents)
817-
# a not-entirely-satisfactory way of distinguishing < as in "import from" from <
818-
# as in "lesser than"
819-
self.inputParser = input_mark + pyparsing.Optional(input_from) + pyparsing.Optional('>') + \
820-
pyparsing.Optional(file_name) + (pyparsing.stringEnd | '|')
821-
self.inputParser.ignore(self.commentInProgress)
822-
823764
# ----- Methods which override stuff in cmd -----
824765

825766
def precmd(self, statement):
@@ -952,45 +893,15 @@ def _complete_statement(self, line):
952893
"""Keep accepting lines of input until the command is complete."""
953894
if not line or (not pyparsing.Or(self.commentGrammars).setParseAction(lambda x: '').transformString(line)):
954895
raise EmptyStatement()
955-
statement = self._parsed(line)
896+
statement = self.parser_manager.parsed(line)
956897
while statement.parsed.multilineCommand and (statement.parsed.terminator == ''):
957898
statement = '%s\n%s' % (statement.parsed.raw,
958899
self.pseudo_raw_input(self.continuation_prompt))
959-
statement = self._parsed(statement)
900+
statement = self.parser_manager.parsed(statement)
960901
if not statement.parsed.command:
961902
raise EmptyStatement()
962903
return statement
963904

964-
def _parsed(self, raw):
965-
""" This function is where the actual parsing of each line occurs.
966-
967-
:param raw: str - the line of text as it was entered
968-
:return: ParsedString - custom subclass of str with extra attributes
969-
"""
970-
if isinstance(raw, ParsedString):
971-
p = raw
972-
else:
973-
# preparse is an overridable hook; default makes no changes
974-
s = self.preparse(raw)
975-
s = self.inputParser.transformString(s.lstrip())
976-
s = self.commentGrammars.transformString(s)
977-
for (shortcut, expansion) in self.shortcuts:
978-
if s.lower().startswith(shortcut):
979-
s = s.replace(shortcut, expansion + ' ', 1)
980-
break
981-
try:
982-
result = self.parser.parseString(s)
983-
except pyparsing.ParseException:
984-
# If we have a parsing failure, treat it is an empty command and move to next prompt
985-
result = self.parser.parseString('')
986-
result['raw'] = raw
987-
result['command'] = result.multilineCommand or result.command
988-
result = self.postparse(result)
989-
p = ParsedString(result.args)
990-
p.parsed = result
991-
p.parser = self._parsed
992-
return p
993-
994905
def _redirect_output(self, statement):
995906
"""Handles output redirection for >, >>, and |.
996907
@@ -1086,7 +997,7 @@ def onecmd(self, line):
1086997
:param line: ParsedString - subclass of string including the pyparsing ParseResults
1087998
:return: bool - a flag indicating whether the interpretation of commands should stop
1088999
"""
1089-
statement = self._parsed(line)
1000+
statement = self.parser_manager.parsed(line)
10901001
self.lastcmd = statement.parsed.raw
10911002
funcname = self._func_named(statement.parsed.command)
10921003
if not funcname:
@@ -2011,6 +1922,135 @@ def cmdloop(self, intro=None):
20111922
self.postloop()
20121923

20131924

1925+
class ParserManager:
1926+
1927+
def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars,
1928+
commentInProgress, case_insensitive, blankLinesAllowed, prefixParser,
1929+
preparse, postparse, shortcuts):
1930+
"Creates and uses parsers for user input according to app's paramters."
1931+
1932+
self.commentGrammars = commentGrammars
1933+
self.preparse = preparse
1934+
self.postparse = postparse
1935+
self.shortcuts = shortcuts
1936+
1937+
self.main_parser = self._build_main_parser(
1938+
redirector=redirector, terminators=terminators, multilineCommands=multilineCommands,
1939+
legalChars=legalChars,
1940+
commentInProgress=commentInProgress, case_insensitive=case_insensitive,
1941+
blankLinesAllowed=blankLinesAllowed, prefixParser=prefixParser)
1942+
self.input_source_parser = self._build_input_source_parser(legalChars=legalChars, commentInProgress=commentInProgress)
1943+
1944+
def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars,
1945+
commentInProgress, case_insensitive, blankLinesAllowed, prefixParser):
1946+
"Builds a PyParsing parser for interpreting user commands."
1947+
1948+
# Build several parsing components that are eventually compiled into overall parser
1949+
output_destination_parser = (pyparsing.Literal(redirector * 2) |
1950+
(pyparsing.WordStart() + redirector) |
1951+
pyparsing.Regex('[^=]' + redirector))('output')
1952+
1953+
terminator_parser = pyparsing.Or(
1954+
[(hasattr(t, 'parseString') and t) or pyparsing.Literal(t) for t in terminators])('terminator')
1955+
string_end = pyparsing.stringEnd ^ '\nEOF'
1956+
multilineCommand = pyparsing.Or(
1957+
[pyparsing.Keyword(c, caseless=case_insensitive) for c in multilineCommands])('multilineCommand')
1958+
oneline_command = (~multilineCommand + pyparsing.Word(legalChars))('command')
1959+
pipe = pyparsing.Keyword('|', identChars='|')
1960+
do_not_parse = self.commentGrammars | commentInProgress | pyparsing.quotedString
1961+
after_elements = \
1962+
pyparsing.Optional(pipe + pyparsing.SkipTo(output_destination_parser ^ string_end, ignore=do_not_parse)('pipeTo')) + \
1963+
pyparsing.Optional(output_destination_parser +
1964+
pyparsing.SkipTo(string_end,
1965+
ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('outputTo'))
1966+
if case_insensitive:
1967+
multilineCommand.setParseAction(lambda x: x[0].lower())
1968+
oneline_command.setParseAction(lambda x: x[0].lower())
1969+
if blankLinesAllowed:
1970+
blankLineTerminationParser = pyparsing.NoMatch
1971+
else:
1972+
blankLineTerminator = (pyparsing.lineEnd + pyparsing.lineEnd)('terminator')
1973+
blankLineTerminator.setResultsName('terminator')
1974+
blankLineTerminationParser = ((multilineCommand ^ oneline_command) +
1975+
pyparsing.SkipTo(blankLineTerminator,
1976+
ignore=do_not_parse).setParseAction(
1977+
lambda x: x[0].strip())('args') +
1978+
blankLineTerminator)('statement')
1979+
1980+
multilineParser = (((multilineCommand ^ oneline_command) +
1981+
pyparsing.SkipTo(terminator_parser,
1982+
ignore=do_not_parse).setParseAction(
1983+
lambda x: x[0].strip())('args') + terminator_parser)('statement') +
1984+
pyparsing.SkipTo(output_destination_parser ^ pipe ^ string_end, ignore=do_not_parse).setParseAction(
1985+
lambda x: x[0].strip())('suffix') + after_elements)
1986+
multilineParser.ignore(commentInProgress)
1987+
1988+
singleLineParser = ((oneline_command +
1989+
pyparsing.SkipTo(terminator_parser ^ string_end ^ pipe ^ output_destination_parser,
1990+
ignore=do_not_parse).setParseAction(
1991+
lambda x: x[0].strip())('args'))('statement') +
1992+
pyparsing.Optional(terminator_parser) + after_elements)
1993+
1994+
blankLineTerminationParser = blankLineTerminationParser.setResultsName('statement')
1995+
1996+
parser = prefixParser + (
1997+
string_end |
1998+
multilineParser |
1999+
singleLineParser |
2000+
blankLineTerminationParser |
2001+
multilineCommand + pyparsing.SkipTo(string_end, ignore=do_not_parse)
2002+
)
2003+
parser.ignore(self.commentGrammars)
2004+
return parser
2005+
2006+
def _build_input_source_parser(self, legalChars, commentInProgress):
2007+
"Builds a PyParsing parser for alternate user input sources (from file, pipe, etc.)"
2008+
2009+
input_mark = pyparsing.Literal('<')
2010+
input_mark.setParseAction(lambda x: '')
2011+
file_name = pyparsing.Word(legalChars + '/\\')
2012+
input_from = file_name('inputFrom')
2013+
input_from.setParseAction(replace_with_file_contents)
2014+
# a not-entirely-satisfactory way of distinguishing < as in "import from" from <
2015+
# as in "lesser than"
2016+
inputParser = input_mark + pyparsing.Optional(input_from) + pyparsing.Optional('>') + \
2017+
pyparsing.Optional(file_name) + (pyparsing.stringEnd | '|')
2018+
inputParser.ignore(commentInProgress)
2019+
return inputParser
2020+
2021+
def parsed(self, raw):
2022+
""" This function is where the actual parsing of each line occurs.
2023+
2024+
:param raw: str - the line of text as it was entered
2025+
:return: ParsedString - custom subclass of str with extra attributes
2026+
"""
2027+
if isinstance(raw, ParsedString):
2028+
p = raw
2029+
else:
2030+
# preparse is an overridable hook; default makes no changes
2031+
s = self.preparse(raw)
2032+
s = self.input_source_parser.transformString(s.lstrip())
2033+
s = self.commentGrammars.transformString(s)
2034+
for (shortcut, expansion) in self.shortcuts:
2035+
if s.lower().startswith(shortcut):
2036+
s = s.replace(shortcut, expansion + ' ', 1)
2037+
break
2038+
try:
2039+
result = self.main_parser.parseString(s)
2040+
except pyparsing.ParseException:
2041+
# If we have a parsing failure, treat it is an empty command and move to next prompt
2042+
result = self.main_parser.parseString('')
2043+
result['raw'] = raw
2044+
result['command'] = result.multilineCommand or result.command
2045+
result = self.postparse(result)
2046+
p = ParsedString(result.args)
2047+
p.parsed = result
2048+
p.parser = self.parsed
2049+
return p
2050+
2051+
2052+
2053+
20142054
class HistoryItem(str):
20152055
"""Class used to represent an item in the History list.
20162056

tests/test_parsing.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ def parser():
2626
c = cmd2.Cmd()
2727
c.multilineCommands = ['multiline']
2828
c.case_insensitive = True
29-
c._init_parser()
30-
return c.parser
29+
c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands,
30+
legalChars=c.legalChars, commentGrammars=c.commentGrammars,
31+
commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive,
32+
blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser,
33+
preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts)
34+
return c.parser_manager.main_parser
3135

3236
@pytest.fixture
3337
def input_parser():
3438
c = cmd2.Cmd()
35-
return c.inputParser
39+
return c.parser_manager.input_source_parser
3640

3741

3842
def test_remaining_args():

0 commit comments

Comments
 (0)