Skip to content

Commit

Permalink
Fixed issue where quoted redirectors and terminators in aliases and m…
Browse files Browse the repository at this point in the history
…acros were not being

restored when read from a startup script.
  • Loading branch information
kmvanbrunt committed Oct 1, 2020
1 parent ed7b9e5 commit 018c329
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 35 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.3.11 (TBD, 2020)
* Bug Fixes
* Fixed issue where quoted redirectors and terminators in aliases and macros were not being
restored when read from a startup script.

## 1.3.10 (September 17, 2020)
* Enhancements
* Added user-settable option called `always_show_hint`. If True, then tab completion hints will always
Expand Down
70 changes: 54 additions & 16 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2821,20 +2821,39 @@ def _alias_delete(self, args: argparse.Namespace) -> None:

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

tokens_to_quote = constants.REDIRECTION_TOKENS
tokens_to_quote.extend(self.statement_parser.terminators)

if args.names:
for cur_name in utils.remove_duplicates(args.names):
if cur_name in self.aliases:
self.poutput("{} {} {}".format(create_cmd, cur_name, self.aliases[cur_name]))
else:
self.perror("Alias '{}' not found".format(cur_name))
to_list = utils.remove_duplicates(args.names)
else:
for cur_alias in sorted(self.aliases, key=self.default_sort_key):
self.poutput("{} {} {}".format(create_cmd, cur_alias, self.aliases[cur_alias]))
to_list = sorted(self.aliases, key=self.default_sort_key)

not_found = [] # type: List[str]
for name in to_list:
if name not in self.aliases:
not_found.append(name)
continue

# Quote redirection and terminator tokens for the 'alias create' command
tokens = shlex_split(self.aliases[name])
command = tokens[0]
args = tokens[1:]
utils.quote_specific_tokens(args, tokens_to_quote)

val = command
if args:
val += ' ' + ' '.join(args)

self.poutput("{} {} {}".format(create_cmd, name, val))

for name in not_found:
self.perror("Alias '{}' not found".format(name))

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

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

tokens_to_quote = constants.REDIRECTION_TOKENS
tokens_to_quote.extend(self.statement_parser.terminators)

if args.names:
for cur_name in utils.remove_duplicates(args.names):
if cur_name in self.macros:
self.poutput("{} {} {}".format(create_cmd, cur_name, self.macros[cur_name].value))
else:
self.perror("Macro '{}' not found".format(cur_name))
to_list = utils.remove_duplicates(args.names)
else:
for cur_macro in sorted(self.macros, key=self.default_sort_key):
self.poutput("{} {} {}".format(create_cmd, cur_macro, self.macros[cur_macro].value))
to_list = sorted(self.macros, key=self.default_sort_key)

not_found = [] # type: List[str]
for name in to_list:
if name not in self.macros:
not_found.append(name)
continue

# Quote redirection and terminator tokens for the 'macro create' command
tokens = shlex_split(self.macros[name].value)
command = tokens[0]
args = tokens[1:]
utils.quote_specific_tokens(args, tokens_to_quote)

val = command
if args:
val += ' ' + ' '.join(args)

self.poutput("{} {} {}".format(create_cmd, name, val))

for name in not_found:
self.perror("Macro '{}' not found".format(name))

def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""Completes the command argument of help"""
Expand Down
30 changes: 21 additions & 9 deletions cmd2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,17 +318,29 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
return sorted(list_to_sort, key=natural_keys)


def unquote_specific_tokens(args: List[str], tokens_to_unquote: List[str]) -> None:
def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None:
"""
Unquote a specific tokens in a list of command-line arguments
This is used when certain tokens have to be passed to another command
:param args: the command line args
:param tokens_to_unquote: the tokens, which if present in args, to unquote
Quote specific tokens in a list
:param tokens: token list being edited
:param tokens_to_quote: the tokens, which if present in tokens, to quote
"""
for i, token in enumerate(tokens):
if token in tokens_to_quote:
tokens[i] = quote_string(token)


def unquote_specific_tokens(tokens: List[str], tokens_to_unquote: List[str]) -> None:
"""
Unquote specific tokens in a list
:param tokens: token list being edited
:param tokens_to_unquote: the tokens, which if present in tokens, to unquote
"""
for i, arg in enumerate(args):
unquoted_arg = strip_quotes(arg)
if unquoted_arg in tokens_to_unquote:
args[i] = unquoted_arg
for i, token in enumerate(tokens):
unquoted_token = strip_quotes(token)
if unquoted_token in tokens_to_unquote:
tokens[i] = unquoted_token


def expand_user(token: str) -> str:
Expand Down
4 changes: 4 additions & 0 deletions docs/api/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Quote Handling

.. autofunction:: cmd2.utils.strip_quotes

.. autofunction:: cmd2.utils.quote_specific_tokens

.. autofunction:: cmd2.utils.unquote_specific_tokens


IO Handling
-----------
Expand Down
23 changes: 13 additions & 10 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1639,16 +1639,17 @@ def test_alias_create(base_app):
out, err = run_cmd(base_app, 'alias list --with_silent fake')
assert out == normalize('alias create --silent fake set')

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

# Create the alias
out, err = run_cmd(base_app, 'alias create fake help ">" "out file.txt" ";"')
out, err = run_cmd(base_app, create_command)
assert out == normalize("Alias 'fake' created")

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

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

def test_macro_create_with_quoted_value(base_app):
"""Demonstrate that quotes in macro value will be preserved (except for redirectors and terminators)"""
def test_macro_create_with_quoted_tokens(base_app):
"""Demonstrate that quotes in macro value will be preserved"""
create_command = 'macro create fake help ">" "out file.txt" ";"'

# Create the macro
out, err = run_cmd(base_app, 'macro create fake help ">" "out file.txt" ";"')
out, err = run_cmd(base_app, create_command)
assert out == normalize("Macro 'fake' created")

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

@pytest.mark.parametrize('macro_name', invalid_command_name)
def test_macro_create_invalid_name(base_app, macro_name):
Expand Down

0 comments on commit 018c329

Please sign in to comment.