diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2017-02-06 22:35:44 -0500 |
---|---|---|
committer | Todd Leonhardt <todd.leonhardt@gmail.com> | 2017-02-06 22:35:44 -0500 |
commit | 0aaa9d3d0a2593b9092f3e10c9a06f739fd84fd3 (patch) | |
tree | 1fe30797b044254cf7864d1e0d91851bb87fd33b | |
parent | 4a6bcefbc72018c90534139c706c2b9d3d327810 (diff) | |
download | cmd2-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-x | cmd2.py | 59 | ||||
-rw-r--r-- | tests/test_transcript.py | 2 |
2 files changed, 48 insertions, 13 deletions
@@ -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 |