summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2017-02-06 22:35:44 -0500
committerTodd Leonhardt <todd.leonhardt@gmail.com>2017-02-06 22:35:44 -0500
commit0aaa9d3d0a2593b9092f3e10c9a06f739fd84fd3 (patch)
tree1fe30797b044254cf7864d1e0d91851bb87fd33b
parent4a6bcefbc72018c90534139c706c2b9d3d327810 (diff)
downloadcmd2-git-0aaa9d3d0a2593b9092f3e10c9a06f739fd84fd3.tar.gz
Experiment with different modes of parsing command-line arguments for option commands.
This is an experiment with usin non-POSIX parsing of the command line with shlex before passing it to the optparse OptionParser. The posix vs non-posix setitng primarily effects how quotes and escape characters are dealt with. I'm experimenting with various options to see what may feel the most natural for the majority of end users.
-rwxr-xr-xcmd2.py59
-rw-r--r--tests/test_transcript.py2
2 files changed, 48 insertions, 13 deletions
diff --git a/cmd2.py b/cmd2.py
index f90bdb84..dfbe8074 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -77,11 +77,13 @@ __version__ = '0.7.0'
pyparsing.ParserElement.enablePackrat()
# If true, it attempts to be as backward compatible as possible by falling back to str.split() argument splititng if
-# shlex.split() throws an exception.
-# Advantages of setting it True: More "permissiive" and backward compatible argument parsing
-# Advantages of setting it False: Stricter parsing which requires escaping certain characters (more similar to shell)
-# TODO: Figure out how to make this a settable parameter in Cmd class, but accessible in options decorator
-BACKWARD_COMPATIBLE_PARSING = True
+BACKWARD_COMPATIBLE_PARSING = False
+
+# Use POSIX or Non-POSIX (Windows) rules for shlex.split
+POSIX_SHLEX = False
+
+# For option commands pass a list of argument strings instead of a single argument string
+USE_ARG_LIST = False
class OptionParser(optparse.OptionParser):
@@ -144,8 +146,22 @@ def _which(editor):
return None
-optparse.Values.get = _attr_get_
+def strip_quotes(arg):
+ """ Strip outer quotes from a string.
+
+ Applies to both single and doulbe quotes.
+
+ :param arg: str - string to strip outer quotes from
+ :return str - same string with potentially outer quotes stripped
+ """
+ quote_chars = '"' + "'"
+
+ if len(arg) > 1 and arg[0] == arg[-1] and arg[0] in quote_chars:
+ arg = arg[1:-1]
+ return arg
+
+optparse.Values.get = _attr_get_
options_defined = [] # used to distinguish --options from SQL-style --comments
@@ -174,7 +190,11 @@ def options(option_list, arg_desc="arg"):
optionParser = OptionParser()
for opt in option_list:
optionParser.add_option(opt)
- optionParser.set_usage("%s [options] %s" % (func.__name__[3:], arg_desc))
+ # Allow reasonable help for commands defined with @options and an empty list of options
+ if len(option_list) > 0:
+ optionParser.set_usage("%s [options] %s" % (func.__name__[3:], arg_desc))
+ else:
+ optionParser.set_usage("%s %s" % (func.__name__[3:], arg_desc))
optionParser._func = func
def new_func(instance, arg):
@@ -182,18 +202,33 @@ def options(option_list, arg_desc="arg"):
if BACKWARD_COMPATIBLE_PARSING:
# For backwads compatibility, fall back to str.split() if shlex.split throws an Exception
try:
- opts, newArgList = optionParser.parse_args(shlex.split(arg))
+ opts, newArgList = optionParser.parse_args(shlex.split(arg, posix=POSIX_SHLEX))
except Exception:
opts, newArgList = optionParser.parse_args(arg.split())
else:
- # Enforce a stricter syntax, requiring users to quote or escape certain characters
- opts, newArgList = optionParser.parse_args(shlex.split(arg))
+ # Enforce a stricter syntax
+ opts, newArgList = optionParser.parse_args(shlex.split(arg, posix=POSIX_SHLEX))
+
+ # If not POSIX, make sure to strip off outer quotes
+ if not POSIX_SHLEX:
+ new_arg_list = []
+ for arg in newArgList:
+ new_arg_list.append(strip_quotes(arg))
+ newArgList = new_arg_list
+
+ # Also strip off outer quotes on string option values
+ for key, val in opts.__dict__.items():
+ if isinstance(val, str):
+ opts.__dict__[key] = strip_quotes(val)
# Must find the remaining args in the original argument list, but
# mustn't include the command itself
# if hasattr(arg, 'parsed') and newArgList[0] == arg.parsed.command:
# newArgList = newArgList[1:]
- newArgs = remaining_args(arg, newArgList)
+ if USE_ARG_LIST:
+ newArgs = newArgList
+ else:
+ newArgs = remaining_args(arg, newArgList)
if isinstance(arg, ParsedString):
arg = arg.with_args_replaced(newArgs)
else:
@@ -957,7 +992,7 @@ class Cmd(cmd.Cmd):
if statement.parsed.pipeTo:
# Pipe output from the command to the shell command via echo
- command_line = 'echo "{}" | {}'.format(command_output.rstrip(), statement.parsed.pipeTo)
+ command_line = r'echo "{}" | {}'.format(command_output.rstrip(), statement.parsed.pipeTo)
result = subprocess.check_output(command_line, shell=True)
if six.PY3:
self.stdout.write(result.decode())
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index d699f545..7e89180a 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -204,7 +204,7 @@ Options:
def test_comment_stripping(_cmdline_app):
out = run_cmd(_cmdline_app, 'speak it was /* not */ delicious! # Yuck!')
- expected = normalize("""it was delicious!""")
+ expected = normalize("""it was delicious!""")
assert out == expected