summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-09-21 09:54:39 -0400
committerGitHub <noreply@github.com>2018-09-21 09:54:39 -0400
commit69b16f1631c0f808964d797d84a943c862284ab5 (patch)
treef6a9027a31537181a4b98ccb9e2967d282f14193
parente17d0e96990e0d8215ff1f1d234c46ca7c8f01ac (diff)
parent89097505f50d295a3879b3be3eec45bcdb9d08eb (diff)
downloadcmd2-git-69b16f1631c0f808964d797d84a943c862284ab5.tar.gz
Merge branch 'master' into colorize
-rw-r--r--.travis.yml4
-rw-r--r--CHANGELOG.md2
-rw-r--r--cmd2/cmd2.py81
-rw-r--r--cmd2/utils.py36
-rw-r--r--tests/test_cmd2.py20
-rw-r--r--tests/test_utils.py35
6 files changed, 133 insertions, 45 deletions
diff --git a/.travis.yml b/.travis.yml
index 30fdc8cf..63b73ecf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,7 +14,9 @@ matrix:
python: 3.6
env: TOXENV=py36
- os: linux
- python: 3.7-dev
+ python: 3.7
+ dist: xenial
+ sudo: true # Travis CI doesn't yet support official (non-development) Python 3.7 on container-based builds
env: TOXENV=py37
- os: linux
python: 3.5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d49d34e..6bf3c961 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
* Enables applications to return a non-zero exit code when exiting from ``cmdloop``
* ``ACHelpFormatter`` now inherits from ``argparse.RawTextHelpFormatter`` to make it easier
for formatting help/description text
+ * Aliases are now sorted alphabetically
+ * The **set** command now tab-completes settable parameter names
## 0.9.4 (August 21, 2018)
* Bug Fixes
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 9f68a3d9..b6322c22 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -45,7 +45,7 @@ from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Un
from . import constants
from . import utils
from . import plugin
-from .argparse_completer import AutoCompleter, ACArgumentParser
+from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .parsing import StatementParser, Statement
@@ -2301,6 +2301,9 @@ Usage: Usage: alias [name] | [<name> <value>]
# Set the alias
self.aliases[name] = value
self.poutput("Alias {!r} created".format(name))
+
+ # Keep aliases in alphabetically sorted order
+ self.aliases = collections.OrderedDict(sorted(self.aliases.items()))
else:
errmsg = "Aliases can not contain: {}".format(invalidchars)
self.perror(errmsg, traceback_war=False)
@@ -2558,22 +2561,21 @@ Usage: Usage: unalias [-a] name [name ...]
Output redirection and pipes allowed: {}"""
return read_only_settings.format(str(self.terminators), self.allow_cli_args, self.allow_redirection)
- def show(self, args: argparse.Namespace, parameter: str) -> None:
+ def show(self, args: argparse.Namespace, parameter: str='') -> None:
"""Shows current settings of parameters.
:param args: argparse parsed arguments from the set command
- :param parameter:
- :return:
+ :param parameter: optional search parameter
"""
- param = ''
- if parameter:
- param = parameter.strip().lower()
+ param = parameter.strip().lower()
result = {}
maxlen = 0
+
for p in self.settable:
if (not param) or p.startswith(param):
- result[p] = '%s: %s' % (p, str(getattr(self, p)))
+ result[p] = '{}: {}'.format(p, str(getattr(self, p)))
maxlen = max(maxlen, len(result[p]))
+
if result:
for p in sorted(result):
if args.long:
@@ -2585,7 +2587,7 @@ Usage: Usage: unalias [-a] name [name ...]
if args.all:
self.poutput('\nRead only settings:{}'.format(self.cmdenvironment()))
else:
- raise LookupError("Parameter '%s' not supported (type 'set' for list of parameters)." % param)
+ raise LookupError("Parameter '{}' not supported (type 'set' for list of parameters).".format(param))
set_description = "Sets a settable parameter or shows current settings of parameters.\n"
set_description += "\n"
@@ -2595,39 +2597,44 @@ Usage: Usage: unalias [-a] name [name ...]
set_parser = ACArgumentParser(description=set_description)
set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
- set_parser.add_argument('settable', nargs=(0, 2), help='[param_name] [value]')
+ setattr(set_parser.add_argument('param', nargs='?', help='parameter to set or view'),
+ ACTION_ARG_CHOICES, settable)
+ set_parser.add_argument('value', nargs='?', help='the new value for settable')
@with_argparser(set_parser)
def do_set(self, args: argparse.Namespace) -> None:
"""Sets a settable parameter or shows current settings of parameters"""
- try:
- param_name, val = args.settable
- val = val.strip()
- param_name = param_name.strip().lower()
- if param_name not in self.settable:
- hits = [p for p in self.settable if p.startswith(param_name)]
- if len(hits) == 1:
- param_name = hits[0]
- else:
- return self.show(args, param_name)
- current_val = getattr(self, param_name)
- if (val[0] == val[-1]) and val[0] in ("'", '"'):
- val = val[1:-1]
+
+ # Check if param was passed in
+ if not args.param:
+ return self.show(args)
+ param = args.param.strip().lower()
+
+ # Check if value was passed in
+ if not args.value:
+ return self.show(args, param)
+ value = args.value
+
+ # Check if param points to just one settable
+ if param not in self.settable:
+ hits = [p for p in self.settable if p.startswith(param)]
+ if len(hits) == 1:
+ param = hits[0]
else:
- val = utils.cast(current_val, val)
- setattr(self, param_name, val)
- self.poutput('%s - was: %s\nnow: %s\n' % (param_name, current_val, val))
- if current_val != val:
- try:
- onchange_hook = getattr(self, '_onchange_%s' % param_name)
- onchange_hook(old=current_val, new=val)
- except AttributeError:
- pass
- except (ValueError, AttributeError):
- param = ''
- if args.settable:
- param = args.settable[0]
- self.show(args, param)
+ return self.show(args, param)
+
+ # Update the settable's value
+ current_value = getattr(self, param)
+ value = utils.cast(current_value, value)
+ setattr(self, param, value)
+
+ self.poutput('{} - was: {}\nnow: {}\n'.format(param, current_value, value))
+
+ # See if we need to call a change hook for this settable
+ if current_value != value:
+ onchange_hook = getattr(self, '_onchange_{}'.format(param), None)
+ if onchange_hook is not None:
+ onchange_hook(old=current_value, new=value)
def do_shell(self, statement: Statement) -> None:
"""Execute a command as if at the OS prompt
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 64401895..9d71d061 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -20,6 +20,28 @@ def strip_ansi(text: str) -> str:
return constants.ANSI_ESCAPE_RE.sub('', text)
+def is_quoted(arg: str) -> bool:
+ """
+ Checks if a string is quoted
+ :param arg: the string being checked for quotes
+ :return: True if a string is quoted
+ """
+ return len(arg) > 1 and arg[0] == arg[-1] and arg[0] in constants.QUOTES
+
+
+def quote_string_if_needed(arg: str) -> str:
+ """ Quotes a string if it contains spaces and isn't already quoted """
+ if is_quoted(arg) or ' ' not in arg:
+ return arg
+
+ if '"' in arg:
+ quote = "'"
+ else:
+ quote = '"'
+
+ return quote + arg + quote
+
+
def strip_quotes(arg: str) -> str:
""" Strip outer quotes from a string.
@@ -28,7 +50,7 @@ def strip_quotes(arg: str) -> str:
:param arg: string to strip outer quotes from
:return: same string with potentially outer quotes stripped
"""
- if len(arg) > 1 and arg[0] == arg[-1] and arg[0] in constants.QUOTES:
+ if is_quoted(arg):
arg = arg[1:-1]
return arg
@@ -71,6 +93,8 @@ def cast(current: Any, new: str) -> Any:
:return: new value with same type as current, or the current value if there was an error casting
"""
typ = type(current)
+ orig_new = new
+
if typ == bool:
try:
return bool(int(new))
@@ -78,18 +102,18 @@ def cast(current: Any, new: str) -> Any:
pass
try:
new = new.lower()
+ if (new == 'on') or (new[0] in ('y', 't')):
+ return True
+ if (new == 'off') or (new[0] in ('n', 'f')):
+ return False
except AttributeError:
pass
- if (new == 'on') or (new[0] in ('y', 't')):
- return True
- if (new == 'off') or (new[0] in ('n', 'f')):
- return False
else:
try:
return typ(new)
except (ValueError, TypeError):
pass
- print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new))
+ print("Problem setting parameter (now {}) to {}; incorrect type?".format(current, orig_new))
return current
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index e2a3d854..0f1c5ab9 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -73,7 +73,7 @@ def test_base_invalid_option(base_app, capsys):
out = normalize(out)
err = normalize(err)
assert 'Error: unrecognized arguments: -z' in err[0]
- assert out[0] == 'Usage: set settable{0..2} [-h] [-a] [-l]'
+ assert out[0] == 'Usage: set [param] [value] [-h] [-a] [-l]'
def test_base_shortcuts(base_app):
out = run_cmd(base_app, 'shortcuts')
@@ -1795,6 +1795,24 @@ def test_create_invalid_alias(base_app, alias_name, capsys):
out, err = capsys.readouterr()
assert "can not contain" in err
+def test_complete_unalias(base_app):
+ text = 'f'
+ line = text
+ endidx = len(line)
+ begidx = endidx - len(text)
+
+ # Validate there are no completions when there are no aliases
+ assert base_app.complete_unalias(text, line, begidx, endidx) == []
+
+ # Create a few aliases - two the start with 'f' and one that doesn't
+ run_cmd(base_app, 'alias fall quit')
+ run_cmd(base_app, 'alias fake pyscript')
+ run_cmd(base_app, 'alias carapace shell')
+
+ # Validate that there are now completions
+ expected = ['fake', 'fall']
+ assert base_app.complete_unalias(text, line, begidx, endidx) == expected
+
def test_ppaged(base_app):
msg = 'testing...'
end = '\n'
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 691abdf8..8c8daa39 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -76,3 +76,38 @@ def test_natural_sort():
assert cu.natural_sort(my_list) == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ']
my_list = ['a3', 'a22', 'A2', 'A11', 'a1']
assert cu.natural_sort(my_list) == ['a1', 'A2', 'a3', 'A11', 'a22']
+
+def test_is_quoted_short():
+ my_str = ''
+ assert not cu.is_quoted(my_str)
+ your_str = '"'
+ assert not cu.is_quoted(your_str)
+
+def test_is_quoted_yes():
+ my_str = '"This is a test"'
+ assert cu.is_quoted(my_str)
+ your_str = "'of the emergengy broadcast system'"
+ assert cu.is_quoted(your_str)
+
+def test_is_quoted_no():
+ my_str = '"This is a test'
+ assert not cu.is_quoted(my_str)
+ your_str = "of the emergengy broadcast system'"
+ assert not cu.is_quoted(your_str)
+ simple_str = "hello world"
+ assert not cu.is_quoted(simple_str)
+
+def test_quote_string_if_needed_yes():
+ my_str = "Hello World"
+ assert cu.quote_string_if_needed(my_str) == '"' + my_str + '"'
+ your_str = '"foo" bar'
+ assert cu.quote_string_if_needed(your_str) == "'" + your_str + "'"
+
+def test_quot_string_if_needed_no():
+ my_str = "HelloWorld"
+ assert cu.quote_string_if_needed(my_str) == my_str
+ your_str = "'Hello World'"
+ assert cu.quote_string_if_needed(your_str) == your_str
+
+
+