Skip to content

Commit

Permalink
Merge pull request #1004 from python-cmd2/redirected_aliases
Browse files Browse the repository at this point in the history
Fixed alias/macro list commands to include quoted redirectors and terminators
  • Loading branch information
kmvanbrunt authored Oct 1, 2020
2 parents ed7b9e5 + 018c329 commit ad646aa
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 ad646aa

Please sign in to comment.