diff --git a/README.rst b/README.rst index a6eb8a7f..365a6216 100644 --- a/README.rst +++ b/README.rst @@ -282,6 +282,16 @@ or create new completion file, e.g:: register-python-argcomplete --shell fish ~/.config/fish/completions/my-awesome-script.fish +Completion Description For Fish +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +By default help string is added as completion description. + +.. image:: docs/fish_help_string.png + +You can disable this feature by removing ``_ARGCOMPLETE_DFS`` variable, e.g:: + + register-python-argcomplete --shell fish my-awesome-script | grep -v _ARGCOMPLETE_DFS | . + Python Support -------------- Argcomplete requires Python 2.7 or 3.5+. diff --git a/argcomplete/__init__.py b/argcomplete/__init__.py index 6cc524e6..f7d0ae11 100644 --- a/argcomplete/__init__.py +++ b/argcomplete/__init__.py @@ -199,6 +199,11 @@ def __call__(self, argument_parser, always_complete_options=True, exit_method=os debug("Invalid value for IFS, quitting [{v}]".format(v=ifs)) exit_method(1) + dfs = os.environ.get("_ARGCOMPLETE_DFS") + if dfs and len(dfs) != 1: + debug("Invalid value for DFS, quitting [{v}]".format(v=dfs)) + exit_method(1) + comp_line = os.environ["COMP_LINE"] comp_point = int(os.environ["COMP_POINT"]) @@ -226,6 +231,12 @@ def __call__(self, argument_parser, always_complete_options=True, exit_method=os completions = self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) + if dfs: + display_completions = {key_part: value.replace(ifs, " ") if value else "" + for key, value in self._display_completions.items() + for key_part in key} + completions = [dfs.join((key, display_completions.get(key) or "")) for key in completions] + debug("\nReturning completions:", completions) output_stream.write(ifs.join(completions).encode(sys_encoding)) output_stream.flush() @@ -321,18 +332,10 @@ def __call__(self, parser, namespace, values, option_string=None): return self.active_parsers def _get_subparser_completions(self, parser, cword_prefix): - def filter_aliases(metavar, dest, prefix): - if not metavar: - return dest if dest and dest.startswith(prefix) else "" - - # metavar combines dest and aliases with ",". - a = metavar.replace(",", "").split() - return " ".join(x for x in a if x.startswith(prefix)) - for action in parser._get_subactions(): - subcmd_with_aliases = filter_aliases(action.metavar, action.dest, cword_prefix) - if subcmd_with_aliases: - self._display_completions[subcmd_with_aliases] = action.help + dest = action.dest + if dest and dest.startswith(cword_prefix): + self._display_completions[dest] = action.help completions = [subcmd for subcmd in parser.choices.keys() if subcmd.startswith(cword_prefix)] return completions @@ -350,8 +353,8 @@ def _include_options(self, action, cword_prefix): def _get_option_completions(self, parser, cword_prefix): self._display_completions.update( - [[" ".join(ensure_str(x) for x in action.option_strings - if ensure_str(x).startswith(cword_prefix)), action.help] + [[tuple(ensure_str(x) for x in action.option_strings + if ensure_str(x).startswith(cword_prefix)), action.help] for action in parser._actions if action.option_strings]) @@ -437,10 +440,10 @@ def _complete_active_option(self, parser, next_positional, cword_prefix, parsed_ completions += completions_from_callable if isinstance(completer, completers.ChoicesCompleter): self._display_completions.update( - [[x, active_action.help] for x in completions_from_callable]) + [[(x,), active_action.help] for x in completions_from_callable]) else: self._display_completions.update( - [[x, ""] for x in completions_from_callable]) + [[(x,), ""] for x in completions_from_callable]) else: debug("Completer is not callable, trying the readline completer protocol instead") for i in range(9999): @@ -448,7 +451,7 @@ def _complete_active_option(self, parser, next_positional, cword_prefix, parsed_ if next_completion is None: break if self.validator(next_completion, cword_prefix): - self._display_completions.update({next_completion: ""}) + self._display_completions.update({(next_completion,): ""}) completions.append(next_completion) if optional_prefix: completions = [optional_prefix + "=" + completion for completion in completions] @@ -635,7 +638,7 @@ def display_completions(substitution, matches, longest_match_length): readline.set_completion_display_matches_hook(display_completions) """ - return self._display_completions + return {" ".join(k): v for k, v in self._display_completions.items()} class ExclusiveCompletionFinder(CompletionFinder): @staticmethod diff --git a/argcomplete/shell_integration.py b/argcomplete/shell_integration.py index f52cb9ae..5a394041 100644 --- a/argcomplete/shell_integration.py +++ b/argcomplete/shell_integration.py @@ -46,6 +46,7 @@ fishcode = r''' function __fish_%(executable)s_complete set -x _ARGCOMPLETE 1 + set -x _ARGCOMPLETE_DFS \t set -x _ARGCOMPLETE_IFS \n set -x _ARGCOMPLETE_SUPPRESS_SPACE 1 set -x _ARGCOMPLETE_SHELL fish diff --git a/docs/fish_help_string.png b/docs/fish_help_string.png new file mode 100644 index 00000000..98c59d3f Binary files /dev/null and b/docs/fish_help_string.png differ diff --git a/test/prog b/test/prog index f01d2bc3..076b10bc 100755 --- a/test/prog +++ b/test/prog @@ -31,12 +31,12 @@ def get_comp_point(*args, **kwargs): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() -subparsers.add_parser('basic').add_argument('arg', choices=['foo', 'bar', 'baz']) +subparsers.add_parser('basic', help='basic help\nnext line of help').add_argument('arg', choices=['foo', 'bar', 'baz']) subparsers.add_parser('space').add_argument('arg', choices=['foo bar', 'baz']) subparsers.add_parser('cont').add_argument('arg').completer = complete_cont subparsers.add_parser('spec').add_argument('arg', choices=['d$e$f', 'd$e$g', 'x!x', r'y\y']) subparsers.add_parser('quote').add_argument('arg', choices=["1'1", '2"2']) -subparsers.add_parser('break').add_argument('arg', choices=['a:b:c', 'a:b:d']) +subparsers.add_parser('break', help="break help").add_argument('arg', choices=['a:b:c', 'a:b:d']) subparsers.add_parser('env').add_argument('arg').completer = check_environ subparsers.add_parser('debug').add_argument('arg').completer = print_output subparsers.add_parser('point', add_help=False).add_argument('arg', nargs='*').completer = get_comp_point diff --git a/test/test.py b/test/test.py index 5f127e81..b8a8f758 100755 --- a/test/test.py +++ b/test/test.py @@ -74,8 +74,10 @@ def run_completer(self, parser, command, point=None, completer=autocomplete, **k with TemporaryFile() as t: os.environ["COMP_LINE"] = ensure_bytes(command) if USING_PYTHON2 else command os.environ["COMP_POINT"] = point - self.assertRaises(SystemExit, completer, parser, output_stream=t, - exit_method=sys.exit, **kwargs) + with self.assertRaises(SystemExit) as cm: + completer(parser, output_stream=t, exit_method=sys.exit, **kwargs) + if cm.exception.code != 0: + raise Exception("Unexpected exit code %d" % cm.exception.code) t.seek(0) return t.read().decode(sys_encoding).split(IFS) @@ -757,6 +759,31 @@ def test_shellcode_utility(self): sc = shellcode(["prog"], use_defaults=False, shell="woosh", complete_arguments=["-o", "nospace"]) sc = shellcode(["prog"], shell="fish") + def test_option_help(self): + os.environ["_ARGCOMPLETE_DFS"] = "\t" + os.environ["_ARGCOMPLETE_SUPPRESS_SPACE"] = "1" + os.environ["_ARGCOMPLETE_SHELL"] = "fish" + + p = ArgumentParser() + p.add_argument("--foo", help="foo" + IFS + "help") + p.add_argument("--bar", "--bar2", help="bar help") + + subparsers = p.add_subparsers() + subparsers.add_parser("subcommand", help="subcommand help") + subparsers.add_parser("subcommand 2", help="subcommand 2 help") + + completions = self.run_completer(p, "prog --f") + self.assertEqual(set(completions), {"--foo\tfoo help"}) + + completions = self.run_completer(p, "prog --b") + self.assertEqual(set(completions), {"--bar\tbar help", "--bar2\tbar help"}) + + completions = self.run_completer(p, "prog sub") + self.assertEqual(set(completions), {"subcommand\tsubcommand help", "subcommand 2\tsubcommand 2 help"}) + + os.environ["_ARGCOMPLETE_DFS"] = "invalid" + self.assertRaises(Exception, self.run_completer, p, "prog --b") + class TestArgcompleteREPL(unittest.TestCase): def setUp(self): pass