Skip to content

Commit 018c329

Browse files
committed
Fixed issue where quoted redirectors and terminators in aliases and macros were not being
restored when read from a startup script.
1 parent ed7b9e5 commit 018c329

File tree

5 files changed

+97
-35
lines changed

5 files changed

+97
-35
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.3.11 (TBD, 2020)
2+
* Bug Fixes
3+
* Fixed issue where quoted redirectors and terminators in aliases and macros were not being
4+
restored when read from a startup script.
5+
16
## 1.3.10 (September 17, 2020)
27
* Enhancements
38
* Added user-settable option called `always_show_hint`. If True, then tab completion hints will always

cmd2/cmd2.py

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2821,20 +2821,39 @@ def _alias_delete(self, args: argparse.Namespace) -> None:
28212821

28222822
@as_subcommand_to('alias', 'list', alias_list_parser, help=alias_delete_help)
28232823
def _alias_list(self, args: argparse.Namespace) -> None:
2824-
"""List some or all aliases"""
2824+
"""List some or all aliases as 'alias create' commands"""
28252825
create_cmd = "alias create"
28262826
if args.with_silent:
28272827
create_cmd += " --silent"
28282828

2829+
tokens_to_quote = constants.REDIRECTION_TOKENS
2830+
tokens_to_quote.extend(self.statement_parser.terminators)
2831+
28292832
if args.names:
2830-
for cur_name in utils.remove_duplicates(args.names):
2831-
if cur_name in self.aliases:
2832-
self.poutput("{} {} {}".format(create_cmd, cur_name, self.aliases[cur_name]))
2833-
else:
2834-
self.perror("Alias '{}' not found".format(cur_name))
2833+
to_list = utils.remove_duplicates(args.names)
28352834
else:
2836-
for cur_alias in sorted(self.aliases, key=self.default_sort_key):
2837-
self.poutput("{} {} {}".format(create_cmd, cur_alias, self.aliases[cur_alias]))
2835+
to_list = sorted(self.aliases, key=self.default_sort_key)
2836+
2837+
not_found = [] # type: List[str]
2838+
for name in to_list:
2839+
if name not in self.aliases:
2840+
not_found.append(name)
2841+
continue
2842+
2843+
# Quote redirection and terminator tokens for the 'alias create' command
2844+
tokens = shlex_split(self.aliases[name])
2845+
command = tokens[0]
2846+
args = tokens[1:]
2847+
utils.quote_specific_tokens(args, tokens_to_quote)
2848+
2849+
val = command
2850+
if args:
2851+
val += ' ' + ' '.join(args)
2852+
2853+
self.poutput("{} {} {}".format(create_cmd, name, val))
2854+
2855+
for name in not_found:
2856+
self.perror("Alias '{}' not found".format(name))
28382857

28392858
#############################################################
28402859
# Parsers and functions for macro command and subcommands
@@ -3029,20 +3048,39 @@ def _macro_delete(self, args: argparse.Namespace) -> None:
30293048

30303049
@as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help)
30313050
def _macro_list(self, args: argparse.Namespace) -> None:
3032-
"""List some or all macros"""
3051+
"""List some or all macros as 'macro create' commands"""
30333052
create_cmd = "macro create"
30343053
if args.with_silent:
30353054
create_cmd += " --silent"
30363055

3056+
tokens_to_quote = constants.REDIRECTION_TOKENS
3057+
tokens_to_quote.extend(self.statement_parser.terminators)
3058+
30373059
if args.names:
3038-
for cur_name in utils.remove_duplicates(args.names):
3039-
if cur_name in self.macros:
3040-
self.poutput("{} {} {}".format(create_cmd, cur_name, self.macros[cur_name].value))
3041-
else:
3042-
self.perror("Macro '{}' not found".format(cur_name))
3060+
to_list = utils.remove_duplicates(args.names)
30433061
else:
3044-
for cur_macro in sorted(self.macros, key=self.default_sort_key):
3045-
self.poutput("{} {} {}".format(create_cmd, cur_macro, self.macros[cur_macro].value))
3062+
to_list = sorted(self.macros, key=self.default_sort_key)
3063+
3064+
not_found = [] # type: List[str]
3065+
for name in to_list:
3066+
if name not in self.macros:
3067+
not_found.append(name)
3068+
continue
3069+
3070+
# Quote redirection and terminator tokens for the 'macro create' command
3071+
tokens = shlex_split(self.macros[name].value)
3072+
command = tokens[0]
3073+
args = tokens[1:]
3074+
utils.quote_specific_tokens(args, tokens_to_quote)
3075+
3076+
val = command
3077+
if args:
3078+
val += ' ' + ' '.join(args)
3079+
3080+
self.poutput("{} {} {}".format(create_cmd, name, val))
3081+
3082+
for name in not_found:
3083+
self.perror("Macro '{}' not found".format(name))
30463084

30473085
def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
30483086
"""Completes the command argument of help"""

cmd2/utils.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -318,17 +318,29 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
318318
return sorted(list_to_sort, key=natural_keys)
319319

320320

321-
def unquote_specific_tokens(args: List[str], tokens_to_unquote: List[str]) -> None:
321+
def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None:
322322
"""
323-
Unquote a specific tokens in a list of command-line arguments
324-
This is used when certain tokens have to be passed to another command
325-
:param args: the command line args
326-
:param tokens_to_unquote: the tokens, which if present in args, to unquote
323+
Quote specific tokens in a list
324+
325+
:param tokens: token list being edited
326+
:param tokens_to_quote: the tokens, which if present in tokens, to quote
327+
"""
328+
for i, token in enumerate(tokens):
329+
if token in tokens_to_quote:
330+
tokens[i] = quote_string(token)
331+
332+
333+
def unquote_specific_tokens(tokens: List[str], tokens_to_unquote: List[str]) -> None:
334+
"""
335+
Unquote specific tokens in a list
336+
337+
:param tokens: token list being edited
338+
:param tokens_to_unquote: the tokens, which if present in tokens, to unquote
327339
"""
328-
for i, arg in enumerate(args):
329-
unquoted_arg = strip_quotes(arg)
330-
if unquoted_arg in tokens_to_unquote:
331-
args[i] = unquoted_arg
340+
for i, token in enumerate(tokens):
341+
unquoted_token = strip_quotes(token)
342+
if unquoted_token in tokens_to_unquote:
343+
tokens[i] = unquoted_token
332344

333345

334346
def expand_user(token: str) -> str:

docs/api/utils.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Quote Handling
2222

2323
.. autofunction:: cmd2.utils.strip_quotes
2424

25+
.. autofunction:: cmd2.utils.quote_specific_tokens
26+
27+
.. autofunction:: cmd2.utils.unquote_specific_tokens
28+
2529

2630
IO Handling
2731
-----------

tests/test_cmd2.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,16 +1639,17 @@ def test_alias_create(base_app):
16391639
out, err = run_cmd(base_app, 'alias list --with_silent fake')
16401640
assert out == normalize('alias create --silent fake set')
16411641

1642-
def test_alias_create_with_quoted_value(base_app):
1643-
"""Demonstrate that quotes in alias value will be preserved (except for redirectors and terminators)"""
1642+
def test_alias_create_with_quoted_tokens(base_app):
1643+
"""Demonstrate that quotes in alias value will be preserved"""
1644+
create_command = 'alias create fake help ">" "out file.txt" ";"'
16441645

16451646
# Create the alias
1646-
out, err = run_cmd(base_app, 'alias create fake help ">" "out file.txt" ";"')
1647+
out, err = run_cmd(base_app, create_command)
16471648
assert out == normalize("Alias 'fake' created")
16481649

1649-
# Look up the new alias (Only the redirector should be unquoted)
1650+
# Look up the new alias and verify all quotes are preserved
16501651
out, err = run_cmd(base_app, 'alias list fake')
1651-
assert out == normalize('alias create fake help > "out file.txt" ;')
1652+
assert out == normalize(create_command)
16521653

16531654
@pytest.mark.parametrize('alias_name', invalid_command_name)
16541655
def test_alias_create_invalid_name(base_app, alias_name, capsys):
@@ -1748,15 +1749,17 @@ def test_macro_create(base_app):
17481749
out, err = run_cmd(base_app, 'macro list --with_silent fake')
17491750
assert out == normalize('macro create --silent fake set')
17501751

1751-
def test_macro_create_with_quoted_value(base_app):
1752-
"""Demonstrate that quotes in macro value will be preserved (except for redirectors and terminators)"""
1752+
def test_macro_create_with_quoted_tokens(base_app):
1753+
"""Demonstrate that quotes in macro value will be preserved"""
1754+
create_command = 'macro create fake help ">" "out file.txt" ";"'
1755+
17531756
# Create the macro
1754-
out, err = run_cmd(base_app, 'macro create fake help ">" "out file.txt" ";"')
1757+
out, err = run_cmd(base_app, create_command)
17551758
assert out == normalize("Macro 'fake' created")
17561759

1757-
# Look up the new macro (Only the redirector should be unquoted)
1760+
# Look up the new macro and verify all quotes are preserved
17581761
out, err = run_cmd(base_app, 'macro list fake')
1759-
assert out == normalize('macro create fake help > "out file.txt" ;')
1762+
assert out == normalize(create_command)
17601763

17611764
@pytest.mark.parametrize('macro_name', invalid_command_name)
17621765
def test_macro_create_invalid_name(base_app, macro_name):

0 commit comments

Comments
 (0)