Commit d141a32a authored by Jérome Perrin's avatar Jérome Perrin

cli: fish shell completions

cliff complete command only supports bash, but fish offers much more user friendly
completions.

cliff uses a pluggable system, but this where new shell completions can be registered,
but the plugin (CompleteFish) can not access the command manager to get more
details about options, since fish can also show help of suggestion, we want to
access the help of each option to show this during suggestions.
Also we want to make some more clever suggestions (for example slapos node start complete
with services identifiers).
For these reasons we need to register a command to replace the default complete command
and can not achieve this with simply a cliff.formatter.completion entry point.

This can be installed with:

    slapos complete > ~/.bash_completion.d/slapos
    slapos complete --shell fish > ~/.config/fish/completions/slapos.fish
parent 51971a3f
...@@ -86,10 +86,14 @@ setup(name=name, ...@@ -86,10 +86,14 @@ setup(name=name,
'slapos-watchdog = slapos.grid.watchdog:main', 'slapos-watchdog = slapos.grid.watchdog:main',
'slapos = slapos.cli.entry:main', 'slapos = slapos.cli.entry:main',
], ],
'cliff.formatter.completion': [
'fish = slapos.cli.complete:CompleteFish',
],
'slapos.cli': [ 'slapos.cli': [
# Utilities # Utilities
'cache lookup = slapos.cli.cache:CacheLookupCommand', 'cache lookup = slapos.cli.cache:CacheLookupCommand',
'cache source = slapos.cli.cache_source:CacheLookupCommand', 'cache source = slapos.cli.cache_source:CacheLookupCommand',
'complete = slapos.cli.complete:CompleteCommand',
# SlapOS Node commands # SlapOS Node commands
'node bang = slapos.cli.bang:BangCommand', 'node bang = slapos.cli.bang:BangCommand',
'node format = slapos.cli.format:FormatCommand', 'node format = slapos.cli.format:FormatCommand',
......
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010-2014 Vifib SARL and Contributors.
# All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation; either version 2.1
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import inspect
import argparse
import cliff
class CompleteFish(cliff.complete.CompleteShellBase):
# not used (we implement the logic in CompleteCommand instead), but registered
# so that CompleteCommand knows it supports fish output.
pass
class CompleteCommand(cliff.complete.CompleteCommand):
"""Generate shell completions.
"""
def take_action(self, parsed_args):
if parsed_args.shell == 'fish':
def get_actions(command):
the_cmd = self.app.command_manager.find_command(command)
cmd_factory, cmd_name, search_args = the_cmd
cmd = cmd_factory(self.app, search_args)
if self.app.interactive_mode:
full_name = (cmd_name)
else:
full_name = (' '.join([self.app.NAME, cmd_name]))
cmd_parser = cmd.get_parser(full_name)
return cmd_parser._get_optional_actions()
def output_action(action, subcommands=()):
cmd_options = ''
for option in action.option_strings:
if option.startswith('--'):
cmd_options += ' -l {}'.format(option[2:])
elif option.startswith('-'):
cmd_options += ' -s {}'.format(option[1:])
extra_flags = ''
if isinstance(action, argparse._StoreAction):
# XXX _StoreAction needs to store *something*, assume it's a file.
extra_flags = ' -F '
cmd_description = action.help.replace("'", "\\'")
subcommand_condition_flag = ''
if subcommands:
subcommand_condition = '; and '.join(
'__fish_seen_subcommand_from {subcommand}'.format(subcommand=subcommand)
for subcommand in subcommands)
subcommand_condition_flag = " -n '{subcommand_condition}' ".format(subcommand_condition=subcommand_condition)
self.app.stdout.write(
"complete -c {slapos} {subcommand_condition_flag} {extra_flags} {cmd_options} -d '{cmd_description}'\n".format(
slapos=self.app.NAME,
subcommand_condition_flag=subcommand_condition_flag,
extra_flags=extra_flags,
cmd_options=cmd_options,
cmd_description=cmd_description,
)
)
self.app.stdout.write("""
# completions for slapos command generated with `slapos complete --shell=fish`
function __fish_print_slapos_services
echo all\\tAll services
eval ( commandline -o | head -1 ) node supervisorctl status | sed -e 's/ /\t/'
end
""")
self.app.stdout.write("# all subcommands\n")
subcommands = set([])
for subcommand, _ in self.app.command_manager:
subcommands.add(subcommand.split(' ')[0])
self.app.stdout.write(
"set -l __fish_slapos_subcommands {subcommands}\n".format(
subcommands=' '.join(subcommands)))
self.app.stdout.write("# general actions\n")
for action in self.app.parser._get_positional_actions():
output_action(action)
subcommands_descriptions = {}
subcommands_actions = {}
for cmd_name, _ in self.app.command_manager:
cmd_class, _, _ = self.app.command_manager.find_command(cmd_name.split())
cmd_description = (inspect.getdoc(cmd_class) or '').splitlines()[0].replace("'", "\\'")
subcommands_descriptions[cmd_name] = cmd_description
subcommands_actions[cmd_name] = get_actions(cmd_name.split())
subcommands_descriptions.setdefault('cache', 'Manage cache')
subcommands_descriptions.setdefault('configure', 'Manage configuration')
subcommands_descriptions.setdefault('computer', 'Manage computer')
subcommands_descriptions.setdefault('node', 'Manage node')
subcommands_descriptions.setdefault('proxy', 'Manage proxy')
subcommands_descriptions.setdefault('service', 'Manage services')
for cmd_name, cmd_description in sorted(subcommands_descriptions.items()):
self.app.stdout.write("\n## command {cmd_name}\n".format(cmd_name=cmd_name))
if ' ' in cmd_name:
base_cmd, sub_cmd = cmd_name.split(' ')
other_sub_commands = []
for other_command in subcommands_actions.keys():
if ' ' in other_command:
other_command_base, other_command_cmd = other_command.split()
if other_command_base == base_cmd:
other_sub_commands.append(other_command_cmd)
condition_no_other_subcommand = ' ; and not __fish_seen_subcommand_from '.join(other_sub_commands)
self.app.stdout.write(
"complete -c {slapos} -f -n '__fish_seen_subcommand_from {base_cmd} "
"{condition_no_other_subcommand}' -a '{sub_cmd}' -d '{cmd_description}'\n".format(
slapos=self.app.NAME,
base_cmd=base_cmd,
condition_no_other_subcommand=condition_no_other_subcommand,
sub_cmd=sub_cmd,
cmd_description=cmd_description,
)
)
else:
self.app.stdout.write(
"complete -c {slapos} -f -n \"not __fish_seen_subcommand_from $__fish_slapos_subcommands\""
" -a '{cmd_name}' -d '{cmd_description}'\n".format(
slapos=self.app.NAME,
cmd_name=cmd_name,
cmd_description=cmd_description,
)
)
for action in subcommands_actions.get(cmd_name, ()):
output_action(action, cmd_name.split(' '))
if cmd_name in (
'node restart',
'node start',
'node status',
'node stop',
'node tail',
):
base_cmd, sub_cmd = cmd_name.split(' ')
self.app.stdout.write(
"complete -c {slapos} -f "
" -n '__fish_seen_subcommand_from {base_cmd}; and __fish_seen_subcommand_from {sub_cmd}' "
" -a '(__fish_print_slapos_services)'\n".format(
slapos=self.app.NAME,
base_cmd=base_cmd,
sub_cmd=sub_cmd,
cmd_description=cmd_description,
)
)
return
return super(CompleteCommand, self).take_action(parsed_args)
...@@ -44,6 +44,7 @@ from cliff.commandmanager import CommandManager, LOG ...@@ -44,6 +44,7 @@ from cliff.commandmanager import CommandManager, LOG
from requests.packages import urllib3 from requests.packages import urllib3
import slapos.version import slapos.version
import slapos.cli.complete
# silence messages like 'Starting connection' that are logged with INFO # silence messages like 'Starting connection' that are logged with INFO
urllib3_logger = logging.getLogger('requests.packages.urllib3') urllib3_logger = logging.getLogger('requests.packages.urllib3')
...@@ -139,6 +140,9 @@ class SlapOSApp(App): ...@@ -139,6 +140,9 @@ class SlapOSApp(App):
version=slapos.version.version, version=slapos.version.version,
command_manager=SlapOSCommandManager('slapos.cli'), command_manager=SlapOSCommandManager('slapos.cli'),
) )
# replace default complete command
self.command_manager.add_command('complete', slapos.cli.complete.CompleteCommand)
def _set_streams(self, stdin, stdout, stderr): def _set_streams(self, stdin, stdout, stderr):
try: try:
......
...@@ -567,3 +567,15 @@ print(request('software_release', 'instance').getInstanceParameterDict()['parame ...@@ -567,3 +567,15 @@ print(request('software_release', 'instance').getInstanceParameterDict()['parame
script.write(self.script) script.write(self.script)
script.flush() script.flush()
app.run(('console', '--cfg', config_file, script.name)) app.run(('console', '--cfg', config_file, script.name))
class TestCliComplete(CliMixin):
def test_complete_bash(self):
with patch.object(sys, 'stdout', StringIO()) as app_stdout:
self.assertEqual(slapos.cli.entry.SlapOSApp().run(['complete']), 0)
self.assertIn('COMPREPLY', app_stdout.getvalue())
def test_complete_fish(self):
with patch.object(sys, 'stdout', StringIO()) as app_stdout:
self.assertEqual(slapos.cli.entry.SlapOSApp().run(['complete', '--shell=fish']), 0)
self.assertIn('__fish_seen_subcommand_from', app_stdout.getvalue())
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment