summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-03-16 18:58:19 -0400
committerGitHub <noreply@github.com>2018-03-16 18:58:19 -0400
commit70b340da1ea080e42ec91d0d37a8c266d095df94 (patch)
treeba5666ae95ea8bb2376a61cf42f13fa71cf83b27
parent3abfbf6c994f5a7e4aa918fc9fff10da99cf6f25 (diff)
parent9465ff2abd51bafa4c8928b19ed58140721fe567 (diff)
downloadcmd2-git-70b340da1ea080e42ec91d0d37a8c266d095df94.tar.gz
Merge pull request #314 from python-cmd2/alias
Alias
-rw-r--r--.pytest_cache/v/cache/lastfailed1
-rw-r--r--CHANGELOG.md1
-rwxr-xr-xREADME.md1
-rwxr-xr-xcmd2.py152
-rw-r--r--docs/freefeatures.rst6
-rw-r--r--docs/settingchanges.rst26
-rw-r--r--tests/conftest.py3
-rw-r--r--tests/test_cmd2.py46
-rw-r--r--tests/test_completion.py51
-rw-r--r--tests/test_parsing.py6
-rw-r--r--tests/test_transcript.py4
-rw-r--r--tests/transcripts/from_cmdloop.txt4
12 files changed, 262 insertions, 39 deletions
diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed
deleted file mode 100644
index 9e26dfee..00000000
--- a/.pytest_cache/v/cache/lastfailed
+++ /dev/null
@@ -1 +0,0 @@
-{} \ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d82d191d..3c590bea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
* Added [quit_on_sigint](http://cmd2.readthedocs.io/en/latest/settingchanges.html#quit-on-sigint) attribute to enable canceling current line instead of quitting when Ctrl+C is typed
* Added possibility of having readline history preservation in a SubMenu
* Added [table_display.py](https://github.com/python-cmd2/cmd2/blob/master/examples/table_display.py) example to demonstrate how to display tabular data
+ * Added command aliasing with ``alias`` command
## 0.8.1 (March 9, 2018)
diff --git a/README.md b/README.md
index 1c9c5957..ab57f823 100755
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ Main Features
- Option to display long output using a pager with ``cmd2.Cmd.ppaged()``
- Multi-line commands
- Special-character command shortcuts (beyond cmd's `@` and `!`)
+- Command aliasing
- Settable environment parameters
- Parsing commands with arguments using `argparse`, including support for sub-commands
- Sub-menu support via the ``AddSubmenu`` decorator
diff --git a/cmd2.py b/cmd2.py
index 59bc5381..2a5993b6 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -164,6 +164,27 @@ def set_use_arg_list(val):
USE_ARG_LIST = val
+# noinspection PyUnusedLocal
+def basic_complete(text, line, begidx, endidx, match_against):
+ """
+ Performs tab completion against a list
+ :param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
+ :param line: str - the current input line with leading whitespace removed
+ :param begidx: int - the beginning index of the prefix text
+ :param endidx: int - the ending index of the prefix text
+ :param match_against: iterable - the list being matched against
+ :return: List[str] - a list of possible tab completions
+ """
+ completions = [cur_str for cur_str in match_against if cur_str.startswith(text)]
+
+ # If there is only 1 match and it's at the end of the line, then add a space
+ if len(completions) == 1 and endidx == len(line):
+ completions[0] += ' '
+
+ completions.sort()
+ return completions
+
+
def flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=None):
"""
Tab completes based on a particular flag preceding the token being completed
@@ -500,7 +521,7 @@ def with_argparser_and_unknown_args(argparser):
argparser.prog = func.__name__[3:]
# If the description has not been set, then use the method docstring if one exists
- if not argparser.description and func.__doc__:
+ if argparser.description is None and func.__doc__:
argparser.description = func.__doc__
cmd_wrapper.__doc__ = argparser.format_help()
@@ -539,7 +560,7 @@ def with_argparser(argparser):
argparser.prog = func.__name__[3:]
# If the description has not been set, then use the method docstring if one exists
- if not argparser.description and func.__doc__:
+ if argparser.description is None and func.__doc__:
argparser.description = func.__doc__
cmd_wrapper.__doc__ = argparser.format_help()
@@ -1024,6 +1045,7 @@ class Cmd(cmd.Cmd):
prefixParser = pyparsing.Empty()
redirector = '>' # for sending output to file
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
+ aliases = dict()
terminators = [';'] # make sure your terminators are not in legalChars!
# Attributes which are NOT dynamically settable at runtime
@@ -1113,7 +1135,8 @@ class Cmd(cmd.Cmd):
legalChars=self.legalChars, commentGrammars=self.commentGrammars,
commentInProgress=self.commentInProgress,
blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser,
- preparse=self.preparse, postparse=self.postparse, shortcuts=self.shortcuts)
+ preparse=self.preparse, postparse=self.postparse, aliases=self.aliases,
+ shortcuts=self.shortcuts)
self._transcript_files = transcript_files
# Used to enable the ability for a Python script to quit the application
@@ -1391,20 +1414,11 @@ class Cmd(cmd.Cmd):
self.completion_matches = compfunc(text, line, begidx, endidx)
else:
- # Complete the command against command names and shortcuts. By design, shortcuts that start with
- # symbols not in self.identchars won't be tab completed since they are handled in the above if
- # statement. This includes shortcuts like: ?, !, @, @@
- strs_to_match = []
-
- # If a command has been started, then match against shortcuts. This keeps shortcuts out of the
- # full list of commands that show up when tab completion is done on an empty line.
- if len(line) > 0:
- for (shortcut, expansion) in self.shortcuts:
- strs_to_match.append(shortcut)
+ # Complete the command against aliases and command names
+ strs_to_match = list(self.aliases.keys())
- # Get command names
- do_text = 'do_' + text
- strs_to_match.extend([cur_name[3:] for cur_name in self.get_names() if cur_name.startswith(do_text)])
+ # Add command names
+ strs_to_match.extend(self.get_command_names())
# Perform matching
completions = [cur_str for cur_str in strs_to_match if cur_str.startswith(text)]
@@ -1420,6 +1434,10 @@ class Cmd(cmd.Cmd):
except IndexError:
return None
+ def get_command_names(self):
+ """ Returns a list of commands """
+ return [cur_name[3:] for cur_name in self.get_names() if cur_name.startswith('do_')]
+
def complete_help(self, text, line, begidx, endidx):
"""
Override of parent class method to handle tab completing subcommands
@@ -1538,6 +1556,12 @@ class Cmd(cmd.Cmd):
# Deal with empty line or all whitespace line
return None, None, line
+ # Handle aliases
+ for cur_alias in self.aliases:
+ if line == cur_alias or line.startswith(cur_alias + ' '):
+ line = line.replace(cur_alias, self.aliases[cur_alias], 1)
+ break
+
# Expand command shortcuts to the full command name
for (shortcut, expansion) in self.shortcuts:
if line.startswith(shortcut):
@@ -1923,6 +1947,92 @@ class Cmd(cmd.Cmd):
return stop
@with_argument_list
+ def do_alias(self, arglist):
+ """Define or display aliases
+
+Usage: Usage: alias [<name> <value>]
+ Where:
+ name - name of the alias being added or edited
+ value - what the alias will be resolved to
+ this can contain spaces and does not need to be quoted
+
+ Without arguments, `alias' prints a list of all aliases in a resuable form
+
+ Example: alias ls !ls -lF
+"""
+ # If no args were given, then print a list of current aliases
+ if len(arglist) == 0:
+ for cur_alias in self.aliases:
+ self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias]))
+
+ # The user is creating an alias
+ elif len(arglist) >= 2:
+ name = arglist[0]
+ value = ' '.join(arglist[1:])
+
+ # Make sure the alias does not match an existing command
+ cmd_func = self._func_named(name)
+ if cmd_func is not None:
+ self.perror("Alias names cannot match an existing command: {!r}".format(name), traceback_war=False)
+ return
+
+ # Check for a valid name
+ for cur_char in name:
+ if cur_char not in self.identchars:
+ self.perror("Alias names can only contain the following characters: {}".format(self.identchars),
+ traceback_war=False)
+ return
+
+ # Set the alias
+ self.aliases[name] = value
+ self.poutput("Alias created")
+
+ else:
+ self.do_help('alias')
+
+ def complete_alias(self, text, line, begidx, endidx):
+ """ Tab completion for alias """
+ index_dict = \
+ {
+ 1: self.aliases,
+ 2: self.get_command_names()
+ }
+ return index_based_complete(text, line, begidx, endidx, index_dict, path_complete)
+
+ @with_argument_list
+ def do_unalias(self, arglist):
+ """Unsets aliases
+
+Usage: Usage: unalias [-a] name [name ...]
+ Where:
+ name - name of the alias being unset
+
+ Options:
+ -a remove all alias definitions
+"""
+ if len(arglist) == 0:
+ self.do_help('unalias')
+
+ if '-a' in arglist:
+ self.aliases.clear()
+ self.poutput("All aliases cleared")
+
+ else:
+ # Get rid of duplicates
+ arglist = list(set(arglist))
+
+ for cur_arg in arglist:
+ if cur_arg in self.aliases:
+ del self.aliases[cur_arg]
+ self.poutput("Alias {!r} cleared".format(cur_arg))
+ else:
+ self.perror("Alias {!r} does not exist".format(cur_arg), traceback_war=False)
+
+ def complete_unalias(self, text, line, begidx, endidx):
+ """ Tab completion for unalias """
+ return basic_complete(text, line, begidx, endidx, self.aliases)
+
+ @with_argument_list
def do_help(self, arglist):
"""List available commands with "help" or detailed help with "help cmd"."""
if arglist:
@@ -2747,12 +2857,13 @@ class ParserManager:
Class which encapsulates all of the pyparsing parser functionality for cmd2 in a single location.
"""
def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars, commentInProgress,
- blankLinesAllowed, prefixParser, preparse, postparse, shortcuts):
+ blankLinesAllowed, prefixParser, preparse, postparse, aliases, shortcuts):
"""Creates and uses parsers for user input according to app's parameters."""
self.commentGrammars = commentGrammars
self.preparse = preparse
self.postparse = postparse
+ self.aliases = aliases
self.shortcuts = shortcuts
self.main_parser = self._build_main_parser(redirector=redirector, terminators=terminators,
@@ -2854,6 +2965,13 @@ class ParserManager:
s = self.preparse(raw)
s = self.input_source_parser.transformString(s.lstrip())
s = self.commentGrammars.transformString(s)
+
+ # Handle aliases
+ for cur_alias in self.aliases:
+ if s == cur_alias or s.startswith(cur_alias + ' '):
+ s = s.replace(cur_alias, self.aliases[cur_alias], 1)
+ break
+
for (shortcut, expansion) in self.shortcuts:
if s.startswith(shortcut):
s = s.replace(shortcut, expansion + ' ', 1)
diff --git a/docs/freefeatures.rst b/docs/freefeatures.rst
index 6cebbaf3..dff82de4 100644
--- a/docs/freefeatures.rst
+++ b/docs/freefeatures.rst
@@ -320,8 +320,8 @@ Additionally, it is trivial to add identical file system path completion to your
have defined a custom command ``foo`` by implementing the ``do_foo`` method. To enable path completion for the ``foo``
command, then add a line of code similar to the following to your class which inherits from ``cmd2.Cmd``::
- # Assuming you have an "import cmd2" somewhere at the top
- complete_foo = cmd2.Cmd.path_complete
+ # Make sure you have an "import functools" somewhere at the top
+ complete_foo = functools.partial(path_complete)
This will effectively define the ``complete_foo`` readline completer method in your class and make it utilize the same
path completion logic as the built-in commands.
@@ -332,4 +332,4 @@ path completion of directories only for this command by adding a line of code si
which inherits from ``cmd2.Cmd``::
# Make sure you have an "import functools" somewhere at the top
- complete_bar = functools.partialmethod(cmd2.Cmd.path_complete, dir_only=True)
+ complete_bar = functools.partial(path_complete, dir_only=True)
diff --git a/docs/settingchanges.rst b/docs/settingchanges.rst
index 67292a48..f5ba16d4 100644
--- a/docs/settingchanges.rst
+++ b/docs/settingchanges.rst
@@ -9,12 +9,12 @@ its name is included in the dictionary ``app.settable``.
(To define your own user-settable parameters, see :ref:`parameters`)
-Shortcuts (command aliases)
+Shortcuts
===========================
-Command aliases for long command names such as special-character shortcuts for common commands can make life more
-convenient for your users. Shortcuts are used without a space separating them from their arguments,
-like ``!ls``. By default, the following shortcuts are defined:
+Command shortcuts for long command names and common commands can make life more convenient for your users.
+Shortcuts are used without a space separating them from their arguments, like ``!ls``. By default, the
+following shortcuts are defined:
``?``
help
@@ -41,12 +41,28 @@ To define more shortcuts, update the dict ``App.shortcuts`` with the
.. warning::
- Command aliases needed to be created by updating the ``shortcuts`` dictionary attribute prior to calling the
+ Shortcuts need to be created by updating the ``shortcuts`` dictionary attribute prior to calling the
``cmd2.Cmd`` super class ``__init__()`` method. Moreover, that super class init method needs to be called after
updating the ``shortcuts`` attribute This warning applies in general to many other attributes which are not
settable at runtime such as ``commentGrammars``, ``multilineCommands``, etc.
+Aliases
+================
+
+In addition to shortcuts, ``cmd2`` provides a full alias feature via the ``alias`` command which is similar to the
+``alias`` command in Bash.
+
+The syntax to create an alias is ``alias <name> <value>``. ``value`` can contain spaces and does not need
+to be quoted. Ex: ``alias ls !ls -lF``
+
+If ``alias`` is run without arguments, then a list of all aliases will be printed to stdout and are in the proper
+``alias`` command syntax, meaning they can easily be reused.
+
+The ``unalias`` is used to clear aliases. Using the ``-a`` flag will clear all aliases. Otherwise provide a list of
+aliases to clear. Ex: ``unalias ls cd pwd`` will clear the aliases called ls, cd, and pwd.
+
+
Default to shell
================
diff --git a/tests/conftest.py b/tests/conftest.py
index 5ee5dd58..58ec8ee0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -15,7 +15,8 @@ import cmd2
# Help text for base cmd2.Cmd application
BASE_HELP = """Documented commands (type help <topic>):
========================================
-edit help history load py pyscript quit set shell shortcuts
+alias help load pyscript set shortcuts
+edit history py quit shell unalias
"""
# Help text for the history command
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index fd37e25e..d69bf343 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1044,7 +1044,8 @@ def test_custom_help_menu(help_app):
expected = normalize("""
Documented commands (type help <topic>):
========================================
-edit help history load py pyscript quit set shell shortcuts squat
+alias help load pyscript set shortcuts unalias
+edit history py quit shell squat
Undocumented commands:
======================
@@ -1541,3 +1542,46 @@ def test_poutput_none(base_app):
out = base_app.stdout.buffer
expected = ''
assert out == expected
+
+
+def test_alias(base_app, capsys):
+ # Create the alias
+ out = run_cmd(base_app, 'alias fake pyscript')
+ assert out == normalize("Alias created")
+
+ # Use the alias
+ run_cmd(base_app, 'fake')
+ out, err = capsys.readouterr()
+ assert "pyscript command requires at least 1 argument" in err
+
+ # See a list of aliases
+ out = run_cmd(base_app, 'alias')
+ assert out == normalize('alias fake pyscript')
+
+def test_alias_with_cmd_name(base_app, capsys):
+ run_cmd(base_app, 'alias help eos')
+ out, err = capsys.readouterr()
+ assert "cannot match an existing command" in err
+
+def test_alias_with_invalid_name(base_app, capsys):
+ run_cmd(base_app, 'alias @ help')
+ out, err = capsys.readouterr()
+ assert "can only contain the following characters" in err
+
+
+def test_unalias(base_app):
+ # Create an alias
+ run_cmd(base_app, 'alias fake pyscript')
+
+ # Remove the alias
+ out = run_cmd(base_app, 'unalias fake')
+ assert out == normalize("Alias 'fake' cleared")
+
+def test_unalias_all(base_app):
+ out = run_cmd(base_app, 'unalias -a')
+ assert out == normalize("All aliases cleared")
+
+def test_unalias_non_existing(base_app, capsys):
+ run_cmd(base_app, 'unalias fake')
+ out, err = capsys.readouterr()
+ assert "does not exist" in err
diff --git a/tests/test_completion.py b/tests/test_completion.py
index c1111ab9..a9f75dce 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -17,7 +17,7 @@ import cmd2
import mock
import pytest
-from cmd2 import path_complete, flag_based_complete, index_based_complete
+from cmd2 import path_complete, basic_complete, flag_based_complete, index_based_complete
@pytest.fixture
def cmd2_app():
@@ -399,7 +399,7 @@ def test_path_completion_no_tokens():
assert path_complete(text, line, begidx, endidx) == []
-# List of strings used with flag and index based completion functions
+# List of strings used with basic, flag, and index based completion functions
food_item_strs = ['Pizza', 'Hamburger', 'Ham', 'Potato']
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football']
@@ -414,6 +414,39 @@ flag_dict = \
'--other': path_complete, # Tab-complete using path_complete function after --other flag in command line
}
+def test_basic_completion_single_end():
+ text = 'Pi'
+ line = 'list_food -f Pi'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza ']
+
+def test_basic_completion_single_mid():
+ text = 'Pi'
+ line = 'list_food -f Pi'
+ begidx = len(line) - len(text)
+ endidx = begidx + 1
+
+ assert basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza']
+
+def test_basic_completion_multiple():
+ text = ''
+ line = 'list_food -f '
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert basic_complete(text, line, begidx, endidx, food_item_strs) == sorted(food_item_strs)
+
+def test_basic_completion_nomatch():
+ text = 'q'
+ line = 'list_food -f q'
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ assert basic_complete(text, line, begidx, endidx, food_item_strs) == []
+
+
def test_flag_based_completion_single_end():
text = 'Pi'
line = 'list_food -f Pi'
@@ -435,6 +468,7 @@ def test_flag_based_completion_multiple():
line = 'list_food -f '
endidx = len(line)
begidx = endidx - len(text)
+
assert flag_based_complete(text, line, begidx, endidx, flag_dict) == sorted(food_item_strs)
def test_flag_based_completion_nomatch():
@@ -442,6 +476,7 @@ def test_flag_based_completion_nomatch():
line = 'list_food -f q'
endidx = len(line)
begidx = endidx - len(text)
+
assert flag_based_complete(text, line, begidx, endidx, flag_dict) == []
def test_flag_based_default_completer(request):
@@ -900,6 +935,7 @@ def test_cmd2_submenu_completion_multiple(sb_app):
assert first_match is not None and sb_app.completion_matches == [
'_relative_load',
+ 'alias',
'edit',
'eof',
'eos',
@@ -912,7 +948,8 @@ def test_cmd2_submenu_completion_multiple(sb_app):
'quit',
'set',
'shell',
- 'shortcuts'
+ 'shortcuts',
+ 'unalias'
]
@@ -1006,6 +1043,7 @@ def test_cmd2_help_submenu_completion_multiple(sb_app):
begidx = endidx - len(text)
assert sb_app.complete_help(text, line, begidx, endidx) == [
'_relative_load',
+ 'alias',
'edit',
'eof',
'eos',
@@ -1018,7 +1056,8 @@ def test_cmd2_help_submenu_completion_multiple(sb_app):
'quit',
'set',
'shell',
- 'shortcuts'
+ 'shortcuts',
+ 'unalias'
]
@@ -1037,6 +1076,7 @@ def test_cmd2_help_submenu_completion_subcommands(sb_app):
begidx = endidx - len(text)
assert sb_app.complete_help(text, line, begidx, endidx) == [
'_relative_load',
+ 'alias',
'edit',
'eof',
'eos',
@@ -1049,5 +1089,6 @@ def test_cmd2_help_submenu_completion_subcommands(sb_app):
'quit',
'set',
'shell',
- 'shortcuts'
+ 'shortcuts',
+ 'unalias'
]
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 8ef9c5c0..12b50eda 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -29,7 +29,8 @@ def parser():
multilineCommands=c.multilineCommands, legalChars=c.legalChars,
commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress,
blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser,
- preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts)
+ preparse=c.preparse, postparse=c.postparse, aliases=c.aliases,
+ shortcuts=c.shortcuts)
return c.parser_manager.main_parser
# Case-sensitive ParserManager
@@ -41,7 +42,8 @@ def cs_pm():
multilineCommands=c.multilineCommands, legalChars=c.legalChars,
commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress,
blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser,
- preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts)
+ preparse=c.preparse, postparse=c.postparse, aliases=c.aliases,
+ shortcuts=c.shortcuts)
return c.parser_manager
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 43b7a72c..8c2af29d 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -129,8 +129,8 @@ def test_base_with_transcript(_cmdline_app):
Documented commands (type help <topic>):
========================================
-edit history mumble py quit set shortcuts
-help load orate pyscript say shell speak
+alias help load orate pyscript say shell speak
+edit history mumble py quit set shortcuts unalias
(Cmd) help say
Repeats what you tell me to.
diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt
index 07bdc30d..ebbf3c91 100644
--- a/tests/transcripts/from_cmdloop.txt
+++ b/tests/transcripts/from_cmdloop.txt
@@ -5,8 +5,8 @@
Documented commands (type help <topic>):
========================================
-edit history mumble py quit set shortcuts/ */
-help load orate pyscript say shell speak/ */
+alias help load orate pyscript say shell speak/ */
+edit history mumble py quit set shortcuts unalias/ */
(Cmd) help say
Repeats what you tell me to.