Skip to content

Commit

Permalink
Fish completion support (google#122)
Browse files Browse the repository at this point in the history
- Fish completion support (--completion fish)
  • Loading branch information
Ellmen authored and dbieber committed May 21, 2018
1 parent 6912ccd commit 83a8036
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 18 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Please see [The Python Fire Guide](docs/guide.md).
| [Help](docs/using-cli.md#help-flag) | `command -- --help` |
| [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
| [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` |

Expand Down
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
| [Help](using-cli.md#help-flag) | `command -- --help` |
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |

Expand Down
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ The complete set of flags available is shown below, in the reference section.
| [Help](using-cli.md#help-flag) | `command -- --help` | Show help and usage information for the command.
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enter interactive mode.
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output.

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Please see [The Python Fire Guide](guide.md).
| [Help](using-cli.md#help-flag) | `command -- --help` |
| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode.
| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`.
| [Completion](using-cli.md#completion-flag) | `command -- --completion` | Generate a completion script for the CLI.
| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI.
| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command.
| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` |

Expand Down
3 changes: 3 additions & 0 deletions docs/using-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ Call `widget -- --completion` to generate a completion script for the Fire CLI
run `widget -- --completion > ~/.widget-completion`. You should then source this
file; to get permanent completion, source this file from your .bashrc file.

Call `widget -- --completion fish` to generate a completion script for the Fish
shell. Source this file from your fish.config.

If the commands available in the Fire CLI change, you'll have to regenerate the
completion script and source it again.

Expand Down
63 changes: 60 additions & 3 deletions fire/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
import six


def Script(name, component, default_options=None):
return _Script(name, _Commands(component), default_options)
def Script(name, component, default_options=None, shell='bash'):
if shell == 'fish':
return _FishScript(name, _Commands(component), default_options)
return _BashScript(name, _Commands(component), default_options)


def _Script(name, commands, default_options=None):
def _BashScript(name, commands, default_options=None):
"""Returns a Bash script registering a completion function for the commands.
Args:
Expand Down Expand Up @@ -97,6 +99,61 @@ def _Script(name, commands, default_options=None):
)


def _FishScript(name, commands, default_options=None):
"""Returns a Fish script registering a completion function for the commands.
Args:
name: The first token in the commands, also the name of the command.
commands: A list of all possible commands that tab completion can complete
to. Each command is a list or tuple of the string tokens that make up
that command.
default_options: A dict of options that can be used with any command. Use
this if there are flags that can always be appended to a command.
Returns:
A string which is the Fish script. Source the fish script to enable tab
completion in Fish.
"""
default_options = default_options or set()
options_map = defaultdict(lambda: copy(default_options))
for command in commands:
start = (name + ' ' + ' '.join(command[:-1])).strip()
completion = _FormatForCommand(command[-1])
options_map[start].add(completion)
options_map[start.replace('_', '-')].add(completion)
fish_source = """function __fish_using_command
set cmd (commandline -opc)
if [ (count $cmd) -eq (count $argv) ]
for i in (seq (count $argv))
if [ $cmd[$i] != $argv[$i] ]
return 1
end
end
return 0
end
return 1
end
"""
subcommand_template = "complete -c {name} -n " \
"'__fish_using_command {start}' -f -a {subcommand}\n"
flag_template = "complete -c {name} -n " \
"'__fish_using_command {start}' -l {option}\n"
for start in options_map:
for option in sorted(options_map[start]):
if option.startswith('--'):
fish_source += flag_template.format(
name=name,
start=start,
option=option[2:]
)
else:
fish_source += subcommand_template.format(
name=name,
start=start,
subcommand=option
)
return fish_source


def _IncludeMember(name, verbose):
if verbose:
return True
Expand Down
44 changes: 40 additions & 4 deletions fire/completion_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,32 @@

class TabCompletionTest(testutils.BaseTestCase):

def testCompletionScript(self):
# A sanity check test to make sure the completion script satisfies some
# basic assumptions.
def testCompletionBashScript(self):
# A sanity check test to make sure the bash completion script satisfies
# some basic assumptions.
commands = [
['run'],
['halt'],
['halt', '--now'],
]
script = completion._Script(name='command', commands=commands) # pylint: disable=protected-access
script = completion._BashScript(name='command', commands=commands) # pylint: disable=protected-access
self.assertIn('command', script)
self.assertIn('halt', script)
self.assertIn('"$start" == "command"', script)

def testCompletionFishScript(self):
# A sanity check test to make sure the fish completion script satisfies
# some basic assumptions.
commands = [
['run'],
['halt'],
['halt', '--now'],
]
script = completion._FishScript(name='command', commands=commands) # pylint: disable=protected-access
self.assertIn('command', script)
self.assertIn('halt', script)
self.assertIn('-l now', script)

def testFnCompletions(self):
def example(one, two, three):
return one, two, three
Expand Down Expand Up @@ -113,6 +126,29 @@ def testClassScript(self):
self.assertIn('--alpha', script)
self.assertIn('--beta', script)

def testDeepDictFishScript(self):
deepdict = {'level1': {'level2': {'level3': {'level4': {}}}}}
script = completion.Script('deepdict', deepdict, shell='fish')
self.assertIn('level1', script)
self.assertIn('level2', script)
self.assertIn('level3', script)
self.assertNotIn('level4', script) # The default depth is 3.

def testFnFishScript(self):
script = completion.Script('identity', tc.identity, shell='fish')
self.assertIn('arg1', script)
self.assertIn('arg2', script)
self.assertIn('arg3', script)
self.assertIn('arg4', script)

def testClassFishScript(self):
script = completion.Script('', tc.MixedDefaults, shell='fish')
self.assertIn('ten', script)
self.assertIn('sum', script)
self.assertIn('identity', script)
self.assertIn('alpha', script)
self.assertIn('beta', script)

def testNonStringDictCompletions(self):
completions = completion.Completions({
10: 'green',
Expand Down
13 changes: 7 additions & 6 deletions fire/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def main(argv):
-h --help: Provide help and usage information for the command.
-i --interactive: Drop into a Python REPL after running the command.
--completion: Write the Bash completion script for the tool to stdout.
--completion fish: Write the Fish completion script for the tool to stdout.
--separator SEPARATOR: Use SEPARATOR in place of the default separator, '-'.
--trace: Get the Fire Trace for the command.
"""
Expand Down Expand Up @@ -165,9 +166,9 @@ def Fire(component=None, command=None, name=None):
return result


def CompletionScript(name, component):
"""Returns the text of the Bash completion script for a Fire CLI."""
return completion.Script(name, component)
def CompletionScript(name, component, shell):
"""Returns the text of the completion script for a Fire CLI."""
return completion.Script(name, component, shell=shell)


class FireError(Exception):
Expand Down Expand Up @@ -338,7 +339,7 @@ def _Fire(component, args, context, name=None):
initial_args = remaining_args

if not remaining_args and (show_help or interactive or show_trace
or show_completion):
or show_completion is not None):
# Don't initialize the final class or call the final function unless
# there's a separator after it, and instead process the current component.
break
Expand Down Expand Up @@ -469,10 +470,10 @@ def _Fire(component, args, context, name=None):
initial_args)
return component_trace

if show_completion:
if show_completion is not None:
if name is None:
raise ValueError('Cannot make completion script without command name')
script = CompletionScript(name, initial_component)
script = CompletionScript(name, initial_component, shell=show_completion)
component_trace.AddCompletionScript(script)

if interactive:
Expand Down
2 changes: 1 addition & 1 deletion fire/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def CreateParser():
parser.add_argument('--verbose', '-v', action='store_true')
parser.add_argument('--interactive', '-i', action='store_true')
parser.add_argument('--separator', default='-')
parser.add_argument('--completion', action='store_true')
parser.add_argument('--completion', nargs='?', const='bash', type=str)
parser.add_argument('--help', '-h', action='store_true')
parser.add_argument('--trace', '-t', action='store_true')
# TODO: Consider allowing name to be passed as an argument.
Expand Down

0 comments on commit 83a8036

Please sign in to comment.