diff options
Diffstat (limited to 'cmd2/parsing.py')
-rw-r--r-- | cmd2/parsing.py | 108 |
1 files changed, 70 insertions, 38 deletions
diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 8d59aedb..b220f1c4 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -5,7 +5,7 @@ import os import re import shlex -from typing import List, Tuple +from typing import List, Tuple, Dict from . import constants from . import utils @@ -21,6 +21,10 @@ class Statement(str): need a place to capture the additional output of the command parsing, so we add our own attributes to this subclass. + Instances of this class should not be created by anything other than the + `StatementParser.parse()` method, nor should any of the attributes be modified + once the object is created. + The string portion of the class contains the arguments, but not the command, nor the output redirection clauses. @@ -54,18 +58,39 @@ class Statement(str): :type output_to: str or None """ - def __init__(self, obj): - super().__init__() - self.raw = str(obj) - self.command = None - self.multiline_command = None - self.args = None - self.argv = None - self.terminator = None - self.suffix = None - self.pipe_to = None - self.output = None - self.output_to = None + def __new__(cls, + obj: object, + *, + raw: str = None, + command: str = None, + args: str = None, + argv: List[str] = None, + multiline_command: str = None, + terminator: str = None, + suffix: str = None, + pipe_to: str = None, + output: str = None, + output_to:str = None + ): + """Create a new instance of Statement + + We must override __new__ because we are subclassing `str` which is + immutable. + """ + stmt = str.__new__(cls, obj) + object.__setattr__(stmt, "raw", raw) + object.__setattr__(stmt, "command", command) + object.__setattr__(stmt, "args", args) + if argv is None: + argv = [] + object.__setattr__(stmt, "argv", argv) + object.__setattr__(stmt, "multiline_command", multiline_command) + object.__setattr__(stmt, "terminator", terminator) + object.__setattr__(stmt, "suffix", suffix) + object.__setattr__(stmt, "pipe_to", pipe_to) + object.__setattr__(stmt, "output", output) + object.__setattr__(stmt, "output_to", output_to) + return stmt @property def command_and_args(self): @@ -82,6 +107,13 @@ class Statement(str): rtn = None return rtn + def __setattr__(self, name, value): + """Statement instances should feel immutable; raise ValueError""" + raise ValueError + + def __delattr__(self, name): + """Statement instances should feel immutable; raise ValueError""" + raise ValueError class StatementParser: """Parse raw text into command components. @@ -90,11 +122,11 @@ class StatementParser: """ def __init__( self, - allow_redirection=True, - terminators=None, - multiline_commands=None, - aliases=None, - shortcuts=None, + allow_redirection: bool = True, + terminators: List[str] = None, + multiline_commands: List[str] = None, + aliases: Dict[str, str] = None, + shortcuts: Dict[str, str] = None, ): self.allow_redirection = allow_redirection if terminators is None: @@ -228,7 +260,7 @@ class StatementParser: tokens = self._split_on_punctuation(list(lexer)) return tokens - def parse(self, rawinput: str) -> Statement: + def parse(self, line: str) -> Statement: """Tokenize the input and parse it into a Statement object, stripping comments, expanding aliases and shortcuts, and extracting output redirection directives. @@ -240,7 +272,7 @@ class StatementParser: # we have to do this before we tokenize because tokenizing # destroys all unquoted whitespace in the input terminator = None - if rawinput[-1:] == LINE_FEED: + if line[-1:] == LINE_FEED: terminator = LINE_FEED command = None @@ -248,7 +280,7 @@ class StatementParser: argv = None # lex the input into a list of tokens - tokens = self.tokenize(rawinput) + tokens = self.tokenize(line) # of the valid terminators, find the first one to occur in the input terminator_pos = len(tokens) + 1 @@ -360,19 +392,18 @@ class StatementParser: # build the statement # string representation of args must be an empty string instead of # None for compatibility with standard library cmd - statement = Statement('' if args is None else args) - statement.raw = rawinput - statement.command = command - # if there are no args we will use None since we don't have to worry - # about compatibility with standard library cmd - statement.args = args - statement.argv = list(map(lambda x: utils.strip_quotes(x), argv)) - statement.terminator = terminator - statement.output = output - statement.output_to = output_to - statement.pipe_to = pipe_to - statement.suffix = suffix - statement.multiline_command = multiline_command + statement = Statement('' if args is None else args, + raw=line, + command=command, + args=args, + argv=list(map(lambda x: utils.strip_quotes(x), argv)), + multiline_command=multiline_command, + terminator=terminator, + suffix=suffix, + pipe_to=pipe_to, + output=output, + output_to=output_to, + ) return statement def parse_command_only(self, rawinput: str) -> Statement: @@ -422,10 +453,11 @@ class StatementParser: # build the statement # string representation of args must be an empty string instead of # None for compatibility with standard library cmd - statement = Statement('' if args is None else args) - statement.raw = rawinput - statement.command = command - statement.args = args + statement = Statement('' if args is None else args, + raw=rawinput, + command=command, + args=args, + ) return statement def _expand(self, line: str) -> str: |