Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help string support for fish #264

Merged
merged 2 commits into from
Jul 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | .

Git Bash Support
----------------
Due to limitations of file descriptor inheritance on Windows,
Expand Down
37 changes: 24 additions & 13 deletions argcomplete/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,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 @@ -233,6 +238,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 @@ -328,16 +339,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))
evanunderscore marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -357,8 +368,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 @@ -447,18 +458,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 @@ -645,7 +656,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 @@ -60,6 +60,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