Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ shell, and the option for a persistent bottom bar that can display realtime stat
- Removed `Statement.pipe_to` since it can be handled by `Statement.redirector` and
`Statement.redirect_to`.
- Changed `StatementParser.parse_command_only()` to return a `PartialStatement` object.
- Renamed `Macro.arg_list` to `Macro.args`.
- Enhancements
- New `cmd2.Cmd` parameters
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These
Expand Down
19 changes: 8 additions & 11 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@
import tempfile
import threading
from code import InteractiveConsole
from collections import (
OrderedDict,
namedtuple,
)
from collections import namedtuple
from collections.abc import (
Callable,
Iterable,
Expand Down Expand Up @@ -2951,7 +2948,7 @@ def _resolve_macro(self, statement: Statement) -> str | None:
# Resolve the arguments in reverse and read their values from statement.argv since those
# are unquoted. Macro args should have been quoted when the macro was created.
resolved = macro.value
reverse_arg_list = sorted(macro.arg_list, key=lambda ma: ma.start_index, reverse=True)
reverse_arg_list = sorted(macro.args, key=lambda ma: ma.start_index, reverse=True)

for macro_arg in reverse_arg_list:
if macro_arg.is_escaped:
Expand Down Expand Up @@ -3743,7 +3740,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:
value += ' ' + ' '.join(args.command_args)

# Find all normal arguments
arg_list = []
macro_args = []
normal_matches = re.finditer(MacroArg.macro_normal_arg_pattern, value)
max_arg_num = 0
arg_nums = set()
Expand All @@ -3762,7 +3759,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:
arg_nums.add(cur_num)
max_arg_num = max(max_arg_num, cur_num)

arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False))
macro_args.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False))
except StopIteration:
pass

Expand All @@ -3781,15 +3778,15 @@ def _macro_create(self, args: argparse.Namespace) -> None:
# Get the number string between the braces
cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0]

arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True))
macro_args.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True))
except StopIteration:
pass

# Set the macro
result = "overwritten" if args.name in self.macros else "created"
self.poutput(f"Macro '{args.name}' {result}")

self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list)
self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, args=macro_args)
self.last_result = True

# macro -> delete
Expand Down Expand Up @@ -4961,15 +4958,15 @@ def do_history(self, args: argparse.Namespace) -> bool | None:
self.last_result = history
return None

def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryItem]':
def _get_history(self, args: argparse.Namespace) -> dict[int, HistoryItem]:
"""If an argument was supplied, then retrieve partial contents of the history; otherwise retrieve entire history.

This function returns a dictionary with history items keyed by their 1-based index in ascending order.
"""
if args.arg:
try:
int_arg = int(args.arg)
return OrderedDict({int_arg: self.history.get(int_arg)})
return {int_arg: self.history.get(int_arg)}
except ValueError:
pass

Expand Down
11 changes: 5 additions & 6 deletions cmd2/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import json
import re
from collections import OrderedDict
from collections.abc import (
Callable,
Iterable,
Expand Down Expand Up @@ -224,7 +223,7 @@ def get(self, index: int) -> HistoryItem:
#
spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))(?P<end>-?[1-9]\d*)?\s*$')

def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
def span(self, span: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']:
"""Return a slice of the History list.

:param span: string containing an index or a slice
Expand Down Expand Up @@ -273,7 +272,7 @@ def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int,

return self._build_result_dictionary(start, end)

def str_search(self, search: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
def str_search(self, search: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']:
"""Find history items which contain a given string.

:param search: the string to search for
Expand All @@ -292,7 +291,7 @@ def isin(history_item: HistoryItem) -> bool:
start = 0 if include_persisted else self.session_start_index
return self._build_result_dictionary(start, len(self), isin)

def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
def regex_search(self, regex: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']:
"""Find history items which match a given regular expression.

:param regex: the regular expression to search for.
Expand Down Expand Up @@ -328,13 +327,13 @@ def truncate(self, max_length: int) -> None:

def _build_result_dictionary(
self, start: int, end: int, filter_func: Callable[[HistoryItem], bool] | None = None
) -> 'OrderedDict[int, HistoryItem]':
) -> dict[int, 'HistoryItem']:
"""Build history search results.

:param start: start index to search from
:param end: end index to stop searching (exclusive).
"""
results: OrderedDict[int, HistoryItem] = OrderedDict()
results: dict[int, HistoryItem] = {}
for index in range(start, end):
if filter_func is None or filter_func(self[index]):
results[index + 1] = self[index]
Expand Down
50 changes: 30 additions & 20 deletions cmd2/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
import re
import shlex
import sys
from collections.abc import Iterable
from collections.abc import (
Iterable,
Sequence,
)
from dataclasses import (
asdict,
dataclass,
field,
)
from typing import Any
from typing import (
Any,
ClassVar,
)

if sys.version_info >= (3, 11):
from typing import Self
Expand Down Expand Up @@ -37,13 +43,9 @@ def shlex_split(str_to_split: str) -> list[str]:
return shlex.split(str_to_split, comments=False, posix=False)


@dataclass(frozen=True)
@dataclass(frozen=True, slots=True)
class MacroArg:
"""Information used to replace or unescape arguments in a macro value when the macro is resolved.

Normal argument syntax: {5}
Escaped argument syntax: {{5}}.
"""
"""Information used to resolve or unescape macro arguments."""

# The starting index of this argument in the macro value
start_index: int
Expand All @@ -56,21 +58,22 @@ class MacroArg:
# Tells if this argument is escaped and therefore needs to be unescaped
is_escaped: bool

# Pattern used to find normal argument
# Digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
# Match strings like: {5}, {{{{{4}, {2}}}}}
macro_normal_arg_pattern = re.compile(r'(?<!{){\d+}|{\d+}(?!})')
# Matches normal args like {5}
# Uses lookarounds to ensure exactly one brace.
# (?<!{){ -> Match '{' not preceded by '{'
# \d+ -> Match digits
# }(?!}) -> Match '}' not followed by '}'
macro_normal_arg_pattern: ClassVar[re.Pattern[str]] = re.compile(r'(?<!{){\d+}|{\d+}(?!})')

# Pattern used to find escaped arguments
# Digits surrounded by 2 or more braces on both sides
# Match strings like: {{5}}, {{{{{4}}, {{2}}}}}
macro_escaped_arg_pattern = re.compile(r'{{2}\d+}{2}')
# Matches escaped args like {{5}}
# Specifically looking for exactly two braces on each side.
macro_escaped_arg_pattern: ClassVar[re.Pattern[str]] = re.compile(r'{{2}\d+}{2}')

# Finds a string of digits
digit_pattern = re.compile(r'\d+')
digit_pattern: ClassVar[re.Pattern[str]] = re.compile(r'\d+')


@dataclass(frozen=True)
@dataclass(frozen=True, slots=True)
class Macro:
"""Defines a cmd2 macro."""

Expand All @@ -83,8 +86,15 @@ class Macro:
# The minimum number of args the user has to pass to this macro
minimum_arg_count: int

# Used to fill in argument placeholders in the macro
arg_list: list[MacroArg] = field(default_factory=list)
# Metadata for argument placeholders and escaped sequences found in 'value'.
# This is stored internally as a tuple.
args: Sequence[MacroArg] = field(default_factory=tuple)

def __post_init__(self) -> None:
"""Finalize the object after initialization."""
# Convert args to an immutable tuple.
if not isinstance(self.args, tuple):
object.__setattr__(self, 'args', tuple(self.args))


@dataclass(frozen=True)
Expand Down
Loading