Parse user input as a string into discrete command components.
| 279 | |
| 280 | |
| 281 | class StatementParser: |
| 282 | """Parse user input as a string into discrete command components.""" |
| 283 | |
| 284 | def __init__( |
| 285 | self, |
| 286 | terminators: Iterable[str] | None = None, |
| 287 | multiline_commands: Iterable[str] | None = None, |
| 288 | aliases: Mapping[str, str] | None = None, |
| 289 | shortcuts: Mapping[str, str] | None = None, |
| 290 | ) -> None: |
| 291 | """Initialize an instance of StatementParser. |
| 292 | |
| 293 | The following will get converted to an immutable tuple before storing internally: |
| 294 | terminators, multiline commands, and shortcuts. |
| 295 | |
| 296 | :param terminators: iterable containing strings which should terminate commands |
| 297 | :param multiline_commands: iterable containing the names of commands that accept multiline input |
| 298 | :param aliases: dictionary containing aliases |
| 299 | :param shortcuts: dictionary containing shortcuts |
| 300 | """ |
| 301 | self.terminators: tuple[str, ...] |
| 302 | if terminators is None: |
| 303 | self.terminators = (constants.MULTILINE_TERMINATOR,) |
| 304 | else: |
| 305 | self.terminators = tuple(terminators) |
| 306 | self.multiline_commands: tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else () |
| 307 | self.aliases: dict[str, str] = dict(aliases) if aliases is not None else {} |
| 308 | |
| 309 | if shortcuts is None: |
| 310 | shortcuts = constants.DEFAULT_SHORTCUTS |
| 311 | |
| 312 | # Sort the shortcuts in descending order by name length because the longest match |
| 313 | # should take precedence. (e.g., @@file should match '@@' and not '@'. |
| 314 | self.shortcuts = tuple(sorted(shortcuts.items(), key=lambda x: len(x[0]), reverse=True)) |
| 315 | |
| 316 | # commands have to be a word, so make a regular expression |
| 317 | # that matches the first word in the line. This regex has three |
| 318 | # parts: |
| 319 | # - the '\A\s*' matches the beginning of the string (even |
| 320 | # if contains multiple lines) and gobbles up any leading |
| 321 | # whitespace |
| 322 | # - the first parenthesis enclosed group matches one |
| 323 | # or more non-whitespace characters with a non-greedy match |
| 324 | # (that's what the '+?' part does). The non-greedy match |
| 325 | # ensures that this first group doesn't include anything |
| 326 | # matched by the second group |
| 327 | # - the second parenthesis group must be dynamically created |
| 328 | # because it needs to match either whitespace, something in |
| 329 | # REDIRECTION_CHARS, one of the terminators, or the end of |
| 330 | # the string (\Z matches the end of the string even if it |
| 331 | # contains multiple lines) |
| 332 | # |
| 333 | invalid_command_chars = ( |
| 334 | *constants.QUOTES, |
| 335 | *constants.REDIRECTION_CHARS, |
| 336 | *self.terminators, |
| 337 | ) |
| 338 |
no outgoing calls
searching dependent graphs…