diff options
-rwxr-xr-x | cmd2.py | 108 | ||||
-rw-r--r-- | tests/test_parsing.py | 3 |
2 files changed, 61 insertions, 50 deletions
@@ -577,23 +577,23 @@ class Cmd(cmd.Cmd): """ # Attributes which are NOT dynamically settable at runtime - - allow_cli_args = True # Should arguments passed on the command-line be processed as commands? - allow_redirection = True # Should output redirection and pipes be allowed + allow_cli_args = True # Should arguments passed on the command-line be processed as commands? + allow_redirection = True # Should output redirection and pipes be allowed blankLinesAllowed = False - commentGrammars = pyparsing.Or([pyparsing.pythonStyleComment, pyparsing.cStyleComment]) - commentGrammars.addParseAction(lambda x: '') + commentGrammars = pyparsing.Or( + [pyparsing.pythonStyleComment, pyparsing.cStyleComment] + ) commentInProgress = pyparsing.Literal('/*') + pyparsing.SkipTo(pyparsing.stringEnd ^ '*/') - default_to_shell = False - defaultExtension = 'txt' # For ``save``, ``load``, etc. + default_to_shell = False # Attempt to run unrecognized commands as shell commands + defaultExtension = 'txt' # For ``save``, ``load``, etc. excludeFromHistory = '''run r list l history hi ed edit li eof'''.split() # make sure your terminators are not in legalChars! legalChars = u'!#$%.:?@_-' + pyparsing.alphanums + pyparsing.alphas8bit multilineCommands = [] # NOTE: Multiline commands can never be abbreviated, even if abbrev is True prefixParser = pyparsing.Empty() - redirector = '>' # for sending output to file + redirector = '>' # for sending output to file reserved_words = [] shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'} terminators = [';'] @@ -659,17 +659,18 @@ class Cmd(cmd.Cmd): # Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility cmd.Cmd.__init__(self, completekey=completekey, stdin=stdin, stdout=stdout) + self._finalize_app_parameters() 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.parser_manager = ParserManager(redirector=self.redirector, terminators=self.terminators, multilineCommands=self.multilineCommands, legalChars=self.legalChars, commentGrammars=self.commentGrammars, commentInProgress=self.commentInProgress, case_insensitive=self.case_insensitive, - blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser) + blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser, + preparse=self.preparse, postparse=self.postparse, shortcuts=self.shortcuts) self._transcript_files = transcript_files # Used to enable the ability for a Python script to quit the application @@ -707,6 +708,10 @@ class Cmd(cmd.Cmd): # ----- Methods related to presenting output to the user ----- + def _finalize_app_parameters(self): + self.commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '') + self.shortcuts = sorted(self.shortcuts.items(), reverse=True) + def poutput(self, msg): """Convenient shortcut for self.stdout.write(); adds newline if necessary.""" if msg: @@ -888,45 +893,15 @@ class Cmd(cmd.Cmd): """Keep 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) + statement = self.parser_manager.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) + statement = self.parser_manager.parsed(statement) if not statement.parsed.command: raise EmptyStatement() return statement - 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) - s = self.parser_manager.input_source_parser.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 - try: - result = self.parser_manager.main_parser.parseString(s) - except pyparsing.ParseException: - # If we have a parsing failure, treat it is an empty command and move to next prompt - result = self.parser_manager.main_parser.parseString('') - 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 - return p - def _redirect_output(self, statement): """Handles output redirection for >, >>, and |. @@ -1022,7 +997,7 @@ class Cmd(cmd.Cmd): :param line: ParsedString - subclass of string including the pyparsing ParseResults :return: bool - a flag indicating whether the interpretation of commands should stop """ - statement = self._parsed(line) + statement = self.parser_manager.parsed(line) self.lastcmd = statement.parsed.raw funcname = self._func_named(statement.parsed.command) if not funcname: @@ -1950,17 +1925,23 @@ Script should contain one command per line, just like command would be typed in class ParserManager: def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars, - commentInProgress, case_insensitive, blankLinesAllowed, prefixParser): + commentInProgress, case_insensitive, blankLinesAllowed, prefixParser, + preparse, postparse, shortcuts): "Creates and uses parsers for user input according to app's paramters." + self.commentGrammars = commentGrammars + self.preparse = preparse + self.postparse = postparse + self.shortcuts = shortcuts + self.main_parser = self._build_main_parser( redirector=redirector, terminators=terminators, multilineCommands=multilineCommands, - legalChars=legalChars, commentGrammars=commentGrammars, + legalChars=legalChars, commentInProgress=commentInProgress, case_insensitive=case_insensitive, blankLinesAllowed=blankLinesAllowed, prefixParser=prefixParser) self.input_source_parser = self._build_input_source_parser(legalChars=legalChars, commentInProgress=commentInProgress) - def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars, commentGrammars, + def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars, commentInProgress, case_insensitive, blankLinesAllowed, prefixParser): "Builds a PyParsing parser for interpreting user commands." @@ -1976,8 +1957,7 @@ class ParserManager: [pyparsing.Keyword(c, caseless=case_insensitive) for c in multilineCommands])('multilineCommand') oneline_command = (~multilineCommand + pyparsing.Word(legalChars))('command') pipe = pyparsing.Keyword('|', identChars='|') - commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '') - do_not_parse = commentGrammars | commentInProgress | pyparsing.quotedString + do_not_parse = self.commentGrammars | commentInProgress | pyparsing.quotedString after_elements = \ pyparsing.Optional(pipe + pyparsing.SkipTo(output_destination_parser ^ string_end, ignore=do_not_parse)('pipeTo')) + \ pyparsing.Optional(output_destination_parser + @@ -2020,7 +2000,7 @@ class ParserManager: blankLineTerminationParser | multilineCommand + pyparsing.SkipTo(string_end, ignore=do_not_parse) ) - parser.ignore(commentGrammars) + parser.ignore(self.commentGrammars) return parser def _build_input_source_parser(self, legalChars, commentInProgress): @@ -2038,6 +2018,36 @@ class ParserManager: inputParser.ignore(commentInProgress) return inputParser + 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) + s = self.input_source_parser.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 + try: + result = self.main_parser.parseString(s) + except pyparsing.ParseException: + # If we have a parsing failure, treat it is an empty command and move to next prompt + result = self.main_parser.parseString('') + 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 + return p + diff --git a/tests/test_parsing.py b/tests/test_parsing.py index ef225e23..a3b601db 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -29,7 +29,8 @@ def parser(): c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive, - blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser) + blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, + preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts) return c.parser_manager.main_parser @pytest.fixture |