Skip to content

Commit

Permalink
Help string support for fish
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey A Volkov committed Jul 4, 2020
1 parent eeb8d4a commit bd65da2
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 21 deletions.
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 | .

External argcomplete script
---------------------------
To register an argcomplete script for an arbitrary name, the ``--external-argcomplete-script`` argument of the ``register-python-argcomplete`` script can be used::
Expand Down
37 changes: 24 additions & 13 deletions argcomplete/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -321,16 +332,16 @@ 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 ""
def filter_aliases(aliases, prefix):
return tuple(x for x in aliases if x.startswith(prefix))

# metavar combines dest and aliases with ",".
a = metavar.replace(",", "").split()
return " ".join(x for x in a if x.startswith(prefix))
aliases_by_parser = {}
for key in parser.choices.keys():
p = parser.choices[key]
aliases_by_parser.setdefault(p, []).append(key)

for action in parser._get_subactions():
subcmd_with_aliases = filter_aliases(action.metavar, action.dest, cword_prefix)
subcmd_with_aliases = filter_aliases(aliases_by_parser[parser.choices[action.dest]], cword_prefix)
if subcmd_with_aliases:
self._display_completions[subcmd_with_aliases] = action.help

Expand All @@ -350,8 +361,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])

Expand Down Expand Up @@ -440,18 +451,18 @@ 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):
next_completion = completer.complete(cword_prefix, i)
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]
Expand Down Expand Up @@ -638,7 +649,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
Expand Down
1 change: 1 addition & 0 deletions argcomplete/shell_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added docs/fish_help_string.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions test/prog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 33 additions & 6 deletions test/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,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)

Expand Down Expand Up @@ -515,7 +517,7 @@ def test_display_completions_with_aliases(self):
completer = CompletionFinder(parser)
completer.rl_complete("", 0)
disp = completer.get_display_completions()
self.assertEqual({"a (b c)": "abc help", "-h --help": "show this help message and exit"}, disp)
self.assertEqual({"a b c": "abc help", "-h --help": "show this help message and exit"}, disp)

# a
completer = CompletionFinder(parser)
Expand All @@ -527,19 +529,19 @@ def test_display_completions_with_aliases(self):
completer = CompletionFinder(parser)
completer.rl_complete("b", 0)
disp = completer.get_display_completions()
self.assertEqual({"": "show this help message and exit"}, disp)
self.assertEqual({"b": "abc help", "": "show this help message and exit"}, disp)

# c
completer = CompletionFinder(parser)
completer.rl_complete("c", 0)
disp = completer.get_display_completions()
self.assertEqual({"c)": "abc help", "": "show this help message and exit"}, disp)
self.assertEqual({"c": "abc help", "": "show this help message and exit"}, disp)

# (
completer = CompletionFinder(parser)
completer.rl_complete("(", 0)
disp = completer.get_display_completions()
self.assertEqual({"(b": "abc help", "": "show this help message and exit"}, disp)
self.assertEqual({"": "show this help message and exit"}, disp)

def test_nargs_one_or_more(self):
def make_parser():
Expand Down Expand Up @@ -801,6 +803,31 @@ def test_shellcode_utility(self):
sc = shellcode(["prog"], shell="fish")
sc = shellcode(["prog"], shell="fish", argcomplete_script="~/.bash_completion.d/prog.py")

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
Expand Down

0 comments on commit bd65da2

Please sign in to comment.