diff options
author | Doug Hellmann <doug.hellmann@gmail.com> | 2013-11-12 18:02:55 -0500 |
---|---|---|
committer | Doug Hellmann <doug.hellmann@gmail.com> | 2013-11-12 18:02:55 -0500 |
commit | 82e28c2e99c80890e409335137009876c45225a2 (patch) | |
tree | 9bd8e713fd0c5404f6bc9d01fd5eb5e542b5db77 | |
parent | 8077b6f1c8bb7e6ca7fa619d609947704603b62e (diff) | |
parent | 60361edf68cf5822d96e215d71fe14260df01ec4 (diff) | |
download | cliff-82e28c2e99c80890e409335137009876c45225a2.tar.gz |
Merge commit '60361edf68cf5822d96e215d71fe14260df01ec4' into completion
# By Terry Howe
# Via Doug Hellmann (1) and Terry Howe (1)
* commit '60361edf68cf5822d96e215d71fe14260df01ec4':
code style fixes
code style fixes
various python code optimizations; shuffle I/O to shell classes
add bash complete
-rw-r--r-- | cliff/complete.py | 186 | ||||
-rw-r--r-- | cliff/tests/test_complete.py | 129 | ||||
-rw-r--r-- | docs/source/complete.rst | 45 | ||||
-rw-r--r-- | docs/source/index.rst | 1 |
4 files changed, 361 insertions, 0 deletions
diff --git a/cliff/complete.py b/cliff/complete.py new file mode 100644 index 0000000..b469e84 --- /dev/null +++ b/cliff/complete.py @@ -0,0 +1,186 @@ + +"""Bash completion for the CLI. +""" + +import logging + +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 + for subcmd in command[:-1]: + dicto = dicto.setdefault(subcmd, {}) + dicto[command[-1]] = 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: + if path == "": + name = cmd + else: + name = path + "_" + 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: + self.output.write(' cmds_{0}=\'{1}\'\n'.format(*datum)) + self.output.write(self.get_trailer()) + + +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.name + """() +{ + local cur prev words + COMPREPLY=() + _get_comp_words_by_ref -n : cur prev words + + # Command data: +""") + + def get_trailer(self): + return (""" + 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}" + 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.name + ' ' + self.name + '\n') + + +class CompleteCommand(command.Command): + """print bash completion command + """ + + log = logging.getLogger(__name__ + '.CompleteCommand') + + 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=['bash', 'none'], + 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) + + if parsed_args.name: + name = parsed_args.name + else: + name = self.app.NAME + if parsed_args.shell == "none": + shell = CompleteNoCode(name, self.app.stdout) + else: + shell = CompleteBash(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 diff --git a/cliff/tests/test_complete.py b/cliff/tests/test_complete.py new file mode 100644 index 0000000..7a650d6 --- /dev/null +++ b/cliff/tests/test_complete.py @@ -0,0 +1,129 @@ +"""Bash completion tests +""" + +import mock + +from cliff.app import App +from cliff.commandmanager import CommandManager +from cliff import complete + + +def test_complete_dictionary(): + sot = complete.CompleteDictionary() + sot.add_command("image delete".split(), + [mock.Mock(option_strings=["1"])]) + sot.add_command("image list".split(), + [mock.Mock(option_strings=["2"])]) + sot.add_command("image create".split(), + [mock.Mock(option_strings=["3"])]) + sot.add_command("volume type create".split(), + [mock.Mock(option_strings=["4"])]) + sot.add_command("volume type delete".split(), + [mock.Mock(option_strings=["5"])]) + assert "image volume" == sot.get_commands() + result = sot.get_data() + assert "image" == result[0][0] + assert "create delete list" == result[0][1] + assert "image_create" == result[1][0] + assert "3" == result[1][1] + assert "image_delete" == result[2][0] + assert "1" == result[2][1] + assert "image_list" == result[3][0] + assert "2" == result[3][1] + + +class FakeStdout: + def __init__(self): + self.content = [] + + def write(self, text): + self.content.append(text) + + def make_string(self): + result = '' + for line in self.content: + result = result + line + return result + + +def given_cmdo_data(): + cmdo = "image server" + data = [("image", "create"), + ("image_create", "--eolus"), + ("server", "meta ssh"), + ("server_meta_delete", "--wilson"), + ("server_ssh", "--sunlight")] + return cmdo, data + + +def then_data(content): + assert " cmds='image server'\n" in content + assert " cmds_image='create'\n" in content + assert " cmds_image_create='--eolus'\n" in content + assert " cmds_server='meta ssh'\n" in content + assert " cmds_server_meta_delete='--wilson'\n" in content + assert " cmds_server_ssh='--sunlight'\n" in content + + +def test_complete_no_code(): + output = FakeStdout() + sot = complete.CompleteNoCode("doesNotMatter", output) + sot.write(*given_cmdo_data()) + then_data(output.content) + + +def test_complete_bash(): + output = FakeStdout() + sot = complete.CompleteBash("openstack", output) + sot.write(*given_cmdo_data()) + then_data(output.content) + assert "_openstack()\n" in output.content[0] + assert "complete -F _openstack openstack\n" in output.content[-1] + + +def test_complete_command_parser(): + sot = complete.CompleteCommand(mock.Mock(), mock.Mock()) + parser = sot.get_parser('nothing') + assert "nothing" == parser.prog + assert "print bash completion command\n " == parser.description + + +def given_complete_command(): + cmd_mgr = CommandManager('cliff.tests') + app = App('testing', '1', cmd_mgr, stdout=FakeStdout()) + sot = complete.CompleteCommand(app, mock.Mock()) + cmd_mgr.add_command('complete', complete.CompleteCommand) + return sot, app, cmd_mgr + + +def then_actions_equal(actions): + optstr = ' '.join(opt for action in actions + for opt in action.option_strings) + assert '-h --help --name --shell' == optstr + + +def test_complete_command_get_actions(): + sot, app, cmd_mgr = given_complete_command() + app.interactive_mode = False + actions = sot.get_actions(["complete"]) + then_actions_equal(actions) + + +def test_complete_command_get_actions_interactive(): + sot, app, cmd_mgr = given_complete_command() + app.interactive_mode = True + actions = sot.get_actions(["complete"]) + then_actions_equal(actions) + + +def test_complete_command_take_action(): + sot, app, cmd_mgr = given_complete_command() + parsed_args = mock.Mock() + parsed_args.name = "test_take" + content = app.stdout.content + assert 0 == sot.take_action(parsed_args) + assert "_test_take()\n" in content[0] + assert "complete -F _test_take test_take\n" in content[-1] + assert " cmds='complete help'\n" in content + assert " cmds_complete='-h --help --name --shell'\n" in content + assert " cmds_help='-h --help'\n" in content diff --git a/docs/source/complete.rst b/docs/source/complete.rst new file mode 100644 index 0000000..2a08098 --- /dev/null +++ b/docs/source/complete.rst @@ -0,0 +1,45 @@ +==================== + Command Completion +==================== + +A generic command completion command is available to generate a +bash-completion script. Currently, the command will generate a script +for bash versions 3 or 4. There is also a mode that generates only +data that can be used in your own script. The command completion script +is generated based on the commands and options that you have specified +in cliff. + +Usage +===== + +In order for your command to support command completions, you need to +add the `cliff.complete.CompleteCommand` class to your command manager. + +:: + + self.command_manager.add_command('complete', cliff.complete.CompleteCommand) + +When you run the command, it will generate a bash-completion script: + +:: + + (.venv)$ mycmd complete + _mycmd() + { + local cur prev words + COMPREPLY=() + _get_comp_words_by_ref -n : cur prev words + + # Command data: + cmds='agent aggregate backup' + cmds_agent='--name' + ... + if [ -z "${completed}" ] ; then + COMPREPLY=( $( compgen -f -- "$cur" ) $( compgen -d -- "$cur" ) ) + else + COMPREPLY=( $(compgen -W "${completed}" -- ${cur}) ) + fi + return 0 + } + complete -F _mycmd mycmd + diff --git a/docs/source/index.rst b/docs/source/index.rst index 0a13664..b474208 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,6 +15,7 @@ Contents: demoapp list_commands show_commands + complete interactive_mode classes install |