diff --git a/docs/cli.rst b/docs/cli.rst index b45205107..3fa8fadc4 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -424,6 +424,27 @@ specify the validators. For example:: Annotations are ignored if the positional decorator is present. +Switch Abbreviations +^^^^^^^^^^^^^^^^^^^^ + +The cli supports switches which have been abbreviated by the user, for example, "--h", "--he", or +"--hel" would all match an actual switch name of"--help", as long as no ambiguity arises from +multiple switches that might match the same abbreviation. This behavior is disabled by default but +can be enabled by defining the class-level attribute ``ALLOW_ABBREV`` to True. For example:: + + class MyApp(cli.Application): + ALLOW_ABBREV = True + cheese = cli.Flag(["cheese"], help = "cheese, please") + chives = cli.Flag(["chives"], help = "chives, instead") + +With the above definition, running the following will raise an error due to ambiguity:: + + $ python example.py --ch # error! matches --cheese and --chives + +However, the following two lines are equivalent:: + + $ python example.py --che + $ python example.py --cheese .. _guide-subcommands: diff --git a/plumbum/cli/application.py b/plumbum/cli/application.py index 5a1cdbd88..cb5e1fad3 100644 --- a/plumbum/cli/application.py +++ b/plumbum/cli/application.py @@ -124,6 +124,10 @@ def main(self, src, dst): * ``SUBCOMMAND_HELPMSG`` - Controls the printing of extra "see subcommand -h" help message. Default is a message, set to False to remove. + * ``ALLOW_ABBREV`` - Controls whether partial switch names are supported, for example '--ver' will match + '--verbose'. Default is False for backward consistency with previous plumbum releases. Note that ambiguous + abbreviations will not match, for example if --foothis and --foothat are defined, then --foo will not match. + A note on sub-commands: when an application is the root, its ``parent`` attribute is set to ``None``. When it is used as a nested-command, ``parent`` will point to its direct ancestor. Likewise, when an application is invoked with a sub-command, its ``nested_command`` attribute @@ -141,6 +145,7 @@ def main(self, src, dst): COLOR_GROUPS = None CALL_MAIN_IF_NESTED_COMMAND = True SUBCOMMAND_HELPMSG = T_("see '{parent} {sub} --help' for more info") + ALLOW_ABBREV = False parent = None nested_command = None @@ -259,6 +264,13 @@ def wrapper(subapp): return wrapper(subapp) if subapp else wrapper + def _get_partial_matches(self, partialname): + matches = [] + for switch in self._switches_by_name: + if switch.startswith(partialname): + matches += [switch, ] + return matches + def _parse_args(self, argv): tailargs = [] swfuncs = {} @@ -289,6 +301,15 @@ def _parse_args(self, argv): argv.insert(0, a[eqsign:]) else: name = a[2:] + + if self.ALLOW_ABBREV: + partials = self._get_partial_matches(name) + if len(partials) == 1: + name = partials[0] + elif len(partials) > 1: + raise UnknownSwitch( + T_("Ambiguous partial switch {0}").format("--" + name)) + swname = "--" + name if name not in self._switches_by_name: raise UnknownSwitch( diff --git a/tests/test_cli.py b/tests/test_cli.py index 135105d22..dc55e8165 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -15,6 +15,8 @@ def bacon(self, param): print ("!!b", param) eggs = cli.SwitchAttr(["e"], str, help = "sets the eggs attribute", envname="PLUMBUM_TEST_EGGS") + cheese = cli.Flag(["--cheese"], help = "cheese, please") + chives = cli.Flag(["--chives"], help = "chives, instead") verbose = cli.CountOf(["v"], help = "increases the verbosity level") benedict = cli.CountOf(["--benedict"], help = """a very long help message with lots of useless information that nobody would ever want to read, but heck, we need to test @@ -291,5 +293,15 @@ def test_mandatory_env_var(self, capsys): stdout, stderr = capsys.readouterr() assert "bacon is mandatory" in stdout + def test_partial_switches(self, capsys): + app = SimpleApp + app.ALLOW_ABBREV = True + inst, rc = app.run(["foo", "--bacon=2", "--ch"], exit=False) + stdout, stderr = capsys.readouterr() + assert 'Ambiguous partial switch' in stdout + assert rc == 2 - + inst, rc = app.run(["foo", "--bacon=2", "--chee"], exit=False) + assert rc == 0 + assert inst.cheese is True + assert inst.chives is False