cliff/cliff/complete.py

230 lines
6.5 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Bash completion for the CLI.
"""
import logging
import stevedore
from cliff import command
class CompleteDictionary:
"""dictionary for bash completion"""
def __init__(self):
self._dictionary = {}
def add_command(self, command, actions):
optstr = ' '.join(
opt for action in actions for opt in action.option_strings
)
dicto = self._dictionary
last_cmd = command[-1]
for subcmd in command[:-1]:
subdata = dicto.get(subcmd)
# If there is a string in corresponding dictionary, it means the
# verb used for the command exists already.
# For example, {'cmd': 'action'}, and we add the command
# 'cmd_other'. We want the result to be
# {'cmd': 'action other', 'cmd_other': 'sub_action'}
if isinstance(subdata, str):
subdata += ' ' + last_cmd
dicto[subcmd] = subdata
last_cmd = subcmd + '_' + last_cmd
else:
dicto = dicto.setdefault(subcmd, {})
dicto[last_cmd] = optstr
def get_commands(self):
return ' '.join(k for k in sorted(self._dictionary.keys()))
def _get_data_recurse(self, dictionary, path):
ray = []
keys = sorted(dictionary.keys())
for cmd in keys:
name = path + "_" + cmd if path else cmd
value = dictionary[cmd]
if isinstance(value, str):
ray.append((name, value))
else:
cmdlist = ' '.join(sorted(value.keys()))
ray.append((name, cmdlist))
ray += self._get_data_recurse(value, name)
return ray
def get_data(self):
return sorted(self._get_data_recurse(self._dictionary, ""))
class CompleteShellBase(object):
"""base class for bash completion generation"""
def __init__(self, name, output):
self.name = str(name)
self.output = output
def write(self, cmdo, data):
self.output.write(self.get_header())
self.output.write(" cmds='{0}'\n".format(cmdo))
for datum in data:
datum = (datum[0].replace('-', '_'), datum[1])
self.output.write(' cmds_{0}=\'{1}\'\n'.format(*datum))
self.output.write(self.get_trailer())
@property
def escaped_name(self):
return self.name.replace('-', '_')
class CompleteNoCode(CompleteShellBase):
"""completion with no code"""
def __init__(self, name, output):
super(CompleteNoCode, self).__init__(name, output)
def get_header(self):
return ''
def get_trailer(self):
return ''
class CompleteBash(CompleteShellBase):
"""completion for bash"""
def __init__(self, name, output):
super(CompleteBash, self).__init__(name, output)
def get_header(self):
return (
'_'
+ self.escaped_name
+ """()
{
local cur prev words
COMPREPLY=()
_get_comp_words_by_ref -n : cur prev words
# Command data:
"""
)
def get_trailer(self):
return (
"""
dash=-
underscore=_
cmd=""
words[0]=""
completed="${cmds}"
for var in "${words[@]:1}"
do
if [[ ${var} == -* ]] ; then
break
fi
if [ -z "${cmd}" ] ; then
proposed="${var}"
else
proposed="${cmd}_${var}"
fi
local i="cmds_${proposed}"
i=${i//$dash/$underscore}
local comp="${!i}"
if [ -z "${comp}" ] ; then
break
fi
if [[ ${comp} == -* ]] ; then
if [[ ${cur} != -* ]] ; then
completed=""
break
fi
fi
cmd="${proposed}"
completed="${comp}"
done
if [ -z "${completed}" ] ; then
COMPREPLY=( $( compgen -f -- "$cur" ) $( compgen -d -- "$cur" ) )
else
COMPREPLY=( $(compgen -W "${completed}" -- ${cur}) )
fi
return 0
}
complete -F _"""
+ self.escaped_name
+ ' '
+ self.name
+ '\n'
)
class CompleteCommand(command.Command):
"""print bash completion command"""
log = logging.getLogger(__name__ + '.CompleteCommand')
def __init__(self, app, app_args, cmd_name=None):
super(CompleteCommand, self).__init__(app, app_args, cmd_name)
self._formatters = stevedore.ExtensionManager(
namespace='cliff.formatter.completion',
)
def get_parser(self, prog_name):
parser = super(CompleteCommand, self).get_parser(prog_name)
parser.add_argument(
"--name",
default=None,
metavar='<command_name>',
help="Command name to support with command completion",
)
parser.add_argument(
"--shell",
default='bash',
metavar='<shell>',
choices=sorted(self._formatters.names()),
help="Shell being used. Use none for data only (default: bash)",
)
return parser
def get_actions(self, 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 take_action(self, parsed_args):
self.log.debug('take_action(%s)' % parsed_args)
name = parsed_args.name or self.app.NAME
try:
shell_factory = self._formatters[parsed_args.shell].plugin
except KeyError:
raise RuntimeError('Unknown shell syntax %r' % parsed_args.shell)
shell = shell_factory(name, self.app.stdout)
dicto = CompleteDictionary()
for cmd in self.app.command_manager:
command = cmd[0].split()
dicto.add_command(command, self.get_actions(command))
shell.write(dicto.get_commands(), dicto.get_data())
return 0