Skip to content

Commit 89a5a64

Browse files
authored
Merge pull request #752 from python-cmd2/restrict_alias_macro_names
Removed ability for aliases and macros to share names with commands
2 parents 0f9fcfc + d210846 commit 89a5a64

File tree

6 files changed

+38
-88
lines changed

6 files changed

+38
-88
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* Added `set_choices_function()`, `set_choices_method()`, `set_completer_function()`, and `set_completer_method()`
99
to support cases where this functionality needs to be added to an argparse action outside of the normal
1010
`parser.add_argument()` call.
11+
* Breaking Changes
12+
* Aliases and macros can no longer have the same name as a command
1113

1214
## 0.9.15 (July 24, 2019)
1315
* Bug Fixes

cmd2/cmd2.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,13 +1672,10 @@ def parseline(self, line: str) -> Tuple[str, str, str]:
16721672
statement = self.statement_parser.parse_command_only(line)
16731673
return statement.command, statement.args, statement.command_and_args
16741674

1675-
def onecmd_plus_hooks(self, line: str, *, expand: bool = True, add_to_history: bool = True,
1676-
py_bridge_call: bool = False) -> bool:
1675+
def onecmd_plus_hooks(self, line: str, *, add_to_history: bool = True, py_bridge_call: bool = False) -> bool:
16771676
"""Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
16781677
16791678
:param line: command line to run
1680-
:param expand: If True, then aliases, macros, and shortcuts will be expanded.
1681-
Set this to False if the command token should not be altered. Defaults to True.
16821679
:param add_to_history: If True, then add this command to history. Defaults to True.
16831680
:param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning
16841681
of an app() call from Python. It is used to enable/disable the storage of the
@@ -1689,7 +1686,7 @@ def onecmd_plus_hooks(self, line: str, *, expand: bool = True, add_to_history: b
16891686

16901687
stop = False
16911688
try:
1692-
statement = self._input_line_to_statement(line, expand=expand)
1689+
statement = self._input_line_to_statement(line)
16931690
except EmptyStatement:
16941691
return self._run_cmdfinalization_hooks(stop, None)
16951692
except ValueError as ex:
@@ -1804,15 +1801,12 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement])
18041801
except Exception as ex:
18051802
self.pexcept(ex)
18061803

1807-
def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *,
1808-
expand: bool = True, add_to_history: bool = True) -> bool:
1804+
def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *, add_to_history: bool = True) -> bool:
18091805
"""
18101806
Used when commands are being run in an automated fashion like text scripts or history replays.
18111807
The prompt and command line for each command will be printed if echo is True.
18121808
18131809
:param cmds: commands to run
1814-
:param expand: If True, then aliases, macros, and shortcuts will be expanded.
1815-
Set this to False if the command token should not be altered. Defaults to True.
18161810
:param add_to_history: If True, then add these commands to history. Defaults to True.
18171811
:return: True if running of commands should stop
18181812
"""
@@ -1823,12 +1817,12 @@ def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *,
18231817
if self.echo:
18241818
self.poutput('{}{}'.format(self.prompt, line))
18251819

1826-
if self.onecmd_plus_hooks(line, expand=expand, add_to_history=add_to_history):
1820+
if self.onecmd_plus_hooks(line, add_to_history=add_to_history):
18271821
return True
18281822

18291823
return False
18301824

1831-
def _complete_statement(self, line: str, *, expand: bool = True) -> Statement:
1825+
def _complete_statement(self, line: str) -> Statement:
18321826
"""Keep accepting lines of input until the command is complete.
18331827
18341828
There is some pretty hacky code here to handle some quirks of
@@ -1837,13 +1831,11 @@ def _complete_statement(self, line: str, *, expand: bool = True) -> Statement:
18371831
backwards compatibility with the standard library version of cmd.
18381832
18391833
:param line: the line being parsed
1840-
:param expand: If True, then aliases and shortcuts will be expanded.
1841-
Set this to False if the command token should not be altered. Defaults to True.
18421834
:return: the completed Statement
18431835
"""
18441836
while True:
18451837
try:
1846-
statement = self.statement_parser.parse(line, expand=expand)
1838+
statement = self.statement_parser.parse(line)
18471839
if statement.multiline_command and statement.terminator:
18481840
# we have a completed multiline command, we are done
18491841
break
@@ -1854,7 +1846,7 @@ def _complete_statement(self, line: str, *, expand: bool = True) -> Statement:
18541846
except ValueError:
18551847
# we have unclosed quotation marks, lets parse only the command
18561848
# and see if it's a multiline
1857-
statement = self.statement_parser.parse_command_only(line, expand=expand)
1849+
statement = self.statement_parser.parse_command_only(line)
18581850
if not statement.multiline_command:
18591851
# not a multiline command, so raise the exception
18601852
raise
@@ -1891,13 +1883,11 @@ def _complete_statement(self, line: str, *, expand: bool = True) -> Statement:
18911883
raise EmptyStatement()
18921884
return statement
18931885

1894-
def _input_line_to_statement(self, line: str, *, expand: bool = True) -> Statement:
1886+
def _input_line_to_statement(self, line: str) -> Statement:
18951887
"""
18961888
Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
18971889
18981890
:param line: the line being parsed
1899-
:param expand: If True, then aliases, macros, and shortcuts will be expanded.
1900-
Set this to False if the command token should not be altered. Defaults to True.
19011891
:return: parsed command line as a Statement
19021892
"""
19031893
used_macros = []
@@ -1906,14 +1896,14 @@ def _input_line_to_statement(self, line: str, *, expand: bool = True) -> Stateme
19061896
# Continue until all macros are resolved
19071897
while True:
19081898
# Make sure all input has been read and convert it to a Statement
1909-
statement = self._complete_statement(line, expand=expand)
1899+
statement = self._complete_statement(line)
19101900

19111901
# Save the fully entered line if this is the first loop iteration
19121902
if orig_line is None:
19131903
orig_line = statement.raw
19141904

19151905
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
1916-
if expand and statement.command in self.macros.keys() and statement.command not in used_macros:
1906+
if statement.command in self.macros.keys() and statement.command not in used_macros:
19171907
used_macros.append(statement.command)
19181908
line = self._resolve_macro(statement)
19191909
if line is None:
@@ -2127,22 +2117,19 @@ def _cmd_func_name(self, command: str) -> str:
21272117
return target if callable(getattr(self, target, None)) else ''
21282118

21292119
# noinspection PyMethodOverriding
2130-
def onecmd(self, statement: Union[Statement, str], *,
2131-
expand: bool = True, add_to_history: bool = True) -> bool:
2120+
def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool:
21322121
""" This executes the actual do_* method for a command.
21332122
21342123
If the command provided doesn't exist, then it executes default() instead.
21352124
21362125
:param statement: intended to be a Statement instance parsed command from the input stream, alternative
21372126
acceptance of a str is present only for backward compatibility with cmd
2138-
:param expand: If True, then aliases, macros, and shortcuts will be expanded.
2139-
Set this to False if the command token should not be altered. Defaults to True.
21402127
:param add_to_history: If True, then add this command to history. Defaults to True.
21412128
:return: a flag indicating whether the interpretation of commands should stop
21422129
"""
21432130
# For backwards compatibility with cmd, allow a str to be passed in
21442131
if not isinstance(statement, Statement):
2145-
statement = self._input_line_to_statement(statement, expand=expand)
2132+
statement = self._input_line_to_statement(statement)
21462133

21472134
func = self.cmd_func(statement.command)
21482135
if func:
@@ -2331,6 +2318,10 @@ def _alias_create(self, args: argparse.Namespace) -> None:
23312318
self.perror("Invalid alias name: {}".format(errmsg))
23322319
return
23332320

2321+
if args.name in self.get_all_commands():
2322+
self.perror("Alias cannot have the same name as a command")
2323+
return
2324+
23342325
if args.name in self.macros:
23352326
self.perror("Alias cannot have the same name as a macro")
23362327
return
@@ -2460,8 +2451,8 @@ def _macro_create(self, args: argparse.Namespace) -> None:
24602451
self.perror("Invalid macro name: {}".format(errmsg))
24612452
return
24622453

2463-
if args.name in self.statement_parser.multiline_commands:
2464-
self.perror("Macro cannot have the same name as a multiline command")
2454+
if args.name in self.get_all_commands():
2455+
self.perror("Macro cannot have the same name as a command")
24652456
return
24662457

24672458
if args.name in self.aliases:

cmd2/parsing.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -356,20 +356,17 @@ def is_valid_command(self, word: str) -> Tuple[bool, str]:
356356
errmsg = ''
357357
return valid, errmsg
358358

359-
def tokenize(self, line: str, *, expand: bool = True) -> List[str]:
359+
def tokenize(self, line: str) -> List[str]:
360360
"""
361361
Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed
362362
363363
:param line: the command line being lexed
364-
:param expand: If True, then aliases and shortcuts will be expanded.
365-
Set this to False if the command token should not be altered. Defaults to True.
366364
:return: A list of tokens
367365
:raises ValueError if there are unclosed quotation marks.
368366
"""
369367

370368
# expand shortcuts and aliases
371-
if expand:
372-
line = self._expand(line)
369+
line = self._expand(line)
373370

374371
# check if this line is a comment
375372
if line.lstrip().startswith(constants.COMMENT_CHAR):
@@ -382,15 +379,13 @@ def tokenize(self, line: str, *, expand: bool = True) -> List[str]:
382379
tokens = self.split_on_punctuation(tokens)
383380
return tokens
384381

385-
def parse(self, line: str, *, expand: bool = True) -> Statement:
382+
def parse(self, line: str) -> Statement:
386383
"""
387384
Tokenize the input and parse it into a Statement object, stripping
388385
comments, expanding aliases and shortcuts, and extracting output
389386
redirection directives.
390387
391388
:param line: the command line being parsed
392-
:param expand: If True, then aliases and shortcuts will be expanded.
393-
Set this to False if the command token should not be altered. Defaults to True.
394389
:return: the created Statement
395390
:raises ValueError if there are unclosed quotation marks
396391
"""
@@ -407,7 +402,7 @@ def parse(self, line: str, *, expand: bool = True) -> Statement:
407402
arg_list = []
408403

409404
# lex the input into a list of tokens
410-
tokens = self.tokenize(line, expand=expand)
405+
tokens = self.tokenize(line)
411406

412407
# of the valid terminators, find the first one to occur in the input
413408
terminator_pos = len(tokens) + 1
@@ -529,7 +524,7 @@ def parse(self, line: str, *, expand: bool = True) -> Statement:
529524
output_to=output_to)
530525
return statement
531526

532-
def parse_command_only(self, rawinput: str, *, expand: bool = True) -> Statement:
527+
def parse_command_only(self, rawinput: str) -> Statement:
533528
"""Partially parse input into a Statement object.
534529
535530
The command is identified, and shortcuts and aliases are expanded.
@@ -554,15 +549,12 @@ def parse_command_only(self, rawinput: str, *, expand: bool = True) -> Statement
554549
whitespace.
555550
556551
:param rawinput: the command line as entered by the user
557-
:param expand: If True, then aliases and shortcuts will be expanded.
558-
Set this to False if the command token should not be altered. Defaults to True.
559552
:return: the created Statement
560553
"""
561554
line = rawinput
562555

563556
# expand shortcuts and aliases
564-
if expand:
565-
line = self._expand(rawinput)
557+
line = self._expand(rawinput)
566558

567559
command = ''
568560
args = ''
@@ -616,7 +608,7 @@ def get_command_arg_list(self, command_name: str, to_parse: Union[Statement, str
616608
"""
617609
# Check if to_parse needs to be converted to a Statement
618610
if not isinstance(to_parse, Statement):
619-
to_parse = self.parse(command_name + ' ' + to_parse, expand=False)
611+
to_parse = self.parse(command_name + ' ' + to_parse)
620612

621613
if preserve_quotes:
622614
return to_parse, to_parse.arg_list

docs/features/shortcuts_aliases_macros.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Use ``alias delete`` to remove aliases
6565

6666
For more details run: ``help alias delete``
6767

68+
Note: Aliases cannot have the same name as a command or macro
6869

6970
Macros
7071
------
@@ -93,3 +94,5 @@ sessions.
9394
For more details on listing macros run: ``help macro list``
9495

9596
For more details on deleting macros run: ``help macro delete``
97+
98+
Note: Macros cannot have the same name as a command or alias

tests/test_cmd2.py

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,6 +1625,10 @@ def test_alias_create_invalid_name(base_app, alias_name, capsys):
16251625
out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name))
16261626
assert "Invalid alias name" in err[0]
16271627

1628+
def test_alias_create_with_command_name(base_app):
1629+
out, err = run_cmd(base_app, 'alias create help stuff')
1630+
assert "Alias cannot have the same name as a command" in err[0]
1631+
16281632
def test_alias_create_with_macro_name(base_app):
16291633
macro = "my_macro"
16301634
run_cmd(base_app, 'macro create {} help'.format(macro))
@@ -1713,19 +1717,16 @@ def test_macro_create_invalid_name(base_app, macro_name):
17131717
out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name))
17141718
assert "Invalid macro name" in err[0]
17151719

1720+
def test_macro_create_with_command_name(base_app):
1721+
out, err = run_cmd(base_app, 'macro create help stuff')
1722+
assert "Macro cannot have the same name as a command" in err[0]
1723+
17161724
def test_macro_create_with_alias_name(base_app):
17171725
macro = "my_macro"
17181726
run_cmd(base_app, 'alias create {} help'.format(macro))
17191727
out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
17201728
assert "Macro cannot have the same name as an alias" in err[0]
17211729

1722-
def test_macro_create_with_command_name(multiline_app):
1723-
out, err = run_cmd(multiline_app, 'macro create help stuff')
1724-
assert out == normalize("Macro 'help' created")
1725-
1726-
out, err = run_cmd(multiline_app, 'macro create orate stuff')
1727-
assert "Macro cannot have the same name as a multiline command" in err[0]
1728-
17291730
def test_macro_create_with_args(base_app):
17301731
# Create the macro
17311732
out, err = run_cmd(base_app, 'macro create fake {1} {2}')
@@ -1843,37 +1844,6 @@ def test_nonexistent_macro(base_app):
18431844

18441845
assert exception is not None
18451846

1846-
def test_input_line_to_statement_expand(base_app):
1847-
# Enable/Disable expansion of shortcuts
1848-
line = '!ls'
1849-
statement = base_app._input_line_to_statement(line, expand=True)
1850-
assert statement.command == 'shell'
1851-
1852-
statement = base_app._input_line_to_statement(line, expand=False)
1853-
assert statement.command == '!ls'
1854-
1855-
# Enable/Disable expansion of aliases
1856-
run_cmd(base_app, 'alias create help macro')
1857-
1858-
line = 'help'
1859-
statement = base_app._input_line_to_statement(line, expand=True)
1860-
assert statement.command == 'macro'
1861-
1862-
statement = base_app._input_line_to_statement(line, expand=False)
1863-
assert statement.command == 'help'
1864-
1865-
run_cmd(base_app, 'alias delete help')
1866-
1867-
# Enable/Disable expansion of macros
1868-
run_cmd(base_app, 'macro create help alias')
1869-
1870-
line = 'help'
1871-
statement = base_app._input_line_to_statement(line, expand=True)
1872-
assert statement.command == 'alias'
1873-
1874-
statement = base_app._input_line_to_statement(line, expand=False)
1875-
assert statement.command == 'help'
1876-
18771847
def test_ppaged(outsim_app):
18781848
msg = 'testing...'
18791849
end = '\n'

tests/test_parsing.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -604,19 +604,11 @@ def test_empty_statement_raises_exception():
604604
('l', 'shell', 'ls -al')
605605
])
606606
def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
607-
# Test first with expansion
608607
statement = parser.parse(line)
609608
assert statement.command == command
610609
assert statement == args
611610
assert statement.args == statement
612611

613-
# Now allow no expansion
614-
tokens = shlex_split(line)
615-
statement = parser.parse(line, expand=False)
616-
assert statement.command == tokens[0]
617-
assert shlex_split(statement) == tokens[1:]
618-
assert statement.args == statement
619-
620612
def test_parse_alias_on_multiline_command(parser):
621613
line = 'anothermultiline has > inside an unfinished command'
622614
statement = parser.parse(line)

0 commit comments

Comments
 (0)