summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTerry Howe <terrylhowe@gmail.com>2013-10-25 21:37:51 -0600
committerTerry Howe <terrylhowe@gmail.com>2013-10-25 21:37:51 -0600
commit5be07a22eb8197d805af7954316f8c29e3a66c3f (patch)
treefb8d6289b0248287f58d733f9e2d8ebe0b8aa607
parentff3ac6435ba27a29cdc015d0d7df45c245a7a5e2 (diff)
downloadcliff-5be07a22eb8197d805af7954316f8c29e3a66c3f.tar.gz
add bash complete
-rw-r--r--cliff/complete.py196
-rw-r--r--cliff/tests/test_complete.py139
-rw-r--r--docs/source/complete.rst45
-rw-r--r--docs/source/index.rst1
4 files changed, 381 insertions, 0 deletions
diff --git a/cliff/complete.py b/cliff/complete.py
new file mode 100644
index 0000000..24cb7e4
--- /dev/null
+++ b/cliff/complete.py
@@ -0,0 +1,196 @@
+
+"""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 = ""
+ dicto = self._dictionary
+ for action in actions:
+ for opt in action.option_strings:
+ if optstr:
+ optstr += " " + opt
+ else:
+ optstr += opt
+ last = None
+ lastsubcmd = None
+ for subcmd in command:
+ if subcmd not in dicto:
+ dicto[subcmd] = {}
+ last = dicto
+ lastsubcmd = subcmd
+ dicto = dicto[subcmd]
+ last[lastsubcmd] = optstr
+
+ def get_commands(self):
+ cmdo = ""
+ keys = sorted(self._dictionary.keys())
+ for cmd in keys:
+ if cmdo == "":
+ cmdo += cmd
+ else:
+ cmdo += " " + cmd
+ return cmdo
+
+ def _get_data_recurse(self, dictionary, path):
+ ray = []
+ keys = sorted(dictionary.keys())
+ for cmd in keys:
+ if path == "":
+ name = str(cmd)
+ else:
+ name = path + "_" + str(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 CompleteNoCode:
+ """completion with no code
+ """
+ def __init__(self, name):
+ self.name = name
+
+ def get_header(self):
+ return ''
+
+ def get_trailer(self):
+ return ''
+
+
+class CompleteBash:
+ """completion for bash
+ """
+ def __init__(self, name):
+ self.name = str(name)
+
+ def get_header(self):
+ return ('_' + self.name + '()\n\
+{\n\
+ local cur prev words\n\
+ COMPREPLY=()\n\
+ _get_comp_words_by_ref -n : cur prev words\n\
+\n\
+ # Command data:\n')
+
+ def get_trailer(self):
+ return ('\
+\n\
+ cmd=""\n\
+ words[0]=""\n\
+ completed="${cmds}" \n\
+ for var in "${words[@]:1}"\n\
+ do\n\
+ if [[ ${var} == -* ]] ; then\n\
+ break\n\
+ fi\n\
+ if [ -z "${cmd}" ] ; then\n\
+ proposed="${var}"\n\
+ else\n\
+ proposed="${cmd}_${var}"\n\
+ fi\n\
+ local i="cmds_${proposed}"\n\
+ local comp="${!i}"\n\
+ if [ -z "${comp}" ] ; then\n\
+ break\n\
+ fi\n\
+ if [[ ${comp} == -* ]] ; then\n\
+ if [[ ${cur} != -* ]] ; then\n\
+ completed=""\n\
+ break\n\
+ fi\n\
+ fi\n\
+ cmd="${proposed}"\n\
+ completed="${comp}"\n\
+ done\n\
+\n\
+ if [ -z "${completed}" ] ; then\n\
+ COMPREPLY=( $( compgen -f -- "$cur" ) $( compgen -d -- "$cur" ) )\n\
+ else\n\
+ COMPREPLY=( $(compgen -W "${completed}" -- ${cur}) )\n\
+ fi\n\
+ return 0\n\
+}\n\
+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)
+ else:
+ shell = CompleteBash(name)
+
+ self.app.stdout.write(shell.get_header())
+
+ dicto = CompleteDictionary()
+ for cmd in self.app.command_manager:
+ command = cmd[0].split()
+ dicto.add_command(command, self.get_actions(command))
+
+ cmdo = dicto.get_commands()
+ self.app.stdout.write(" cmds='{0}'\n".format(cmdo))
+ for datum in dicto.get_data():
+ self.app.stdout.write(' cmds_{0}=\'{1}\'\n'.format(*datum))
+
+ self.app.stdout.write(shell.get_trailer())
+
+ return 0
diff --git a/cliff/tests/test_complete.py b/cliff/tests/test_complete.py
new file mode 100644
index 0000000..23775c0
--- /dev/null
+++ b/cliff/tests/test_complete.py
@@ -0,0 +1,139 @@
+"""Bash completion tests
+"""
+
+import mock
+import os
+
+from cliff import complete
+
+
+def test_add_command():
+ 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 create_complete_command_mocks():
+ app = mock.Mock(name="app")
+ app_args = mock.Mock(name="app_args")
+ # actions
+ action_one = mock.Mock(name="action_one")
+ action_one.option_strings = ["Eolus"]
+ action_two = mock.Mock(name="action_two")
+ action_two.option_strings = ["Wilson", "Sunlight"]
+ actions = [action_one, action_two]
+ # get_optional_actions
+ get_optional_actions = mock.Mock(name="get_optional_actions")
+ get_optional_actions.return_value = actions
+ # cmd_parser
+ cmd_parser = mock.Mock(name="cmd_parser")
+ cmd_parser._get_optional_actions = get_optional_actions
+ # get_parser
+ get_parser = mock.Mock(name="get_parser")
+ get_parser.return_value = cmd_parser
+ # cmd_factory_init
+ cmd_factory_init = mock.Mock("cmd_factory_init")
+ cmd_factory_init.get_parser = get_parser
+ # cmd_factory
+ cmd_factory = mock.Mock(name="cmd_factory")
+ cmd_factory.return_value = cmd_factory_init
+ # find_command
+ cmd_name = "yale"
+ search_args = "search_args"
+ find_command = mock.Mock(name="find_command")
+ find_command.return_value = (cmd_factory, cmd_name, search_args)
+ # command_manager
+ commands = [["image create"], ["server meta delete"], ["server ssh"]]
+ command_manager = mock.MagicMock()
+ command_manager.__iter__.return_value = iter(commands)
+ command_manager.find_command = find_command
+ app.command_manager = command_manager
+ app.NAME = "openstack"
+ app.interactive_mode = False
+ app.stdout = FakeStdout()
+ return (complete.CompleteCommand(app, app_args), app, actions, cmd_factory_init)
+
+def check_parser(cmd, args, verify_args):
+ cmd_parser = cmd.get_parser('check_parser')
+ parsed_args = cmd_parser.parse_args(args)
+ for av in verify_args:
+ attr, value = av
+ if attr:
+ assert attr in parsed_args
+ assert getattr(parsed_args, attr) == value
+
+def test_parser_nothing():
+ sot, app, actions, cmd_factory_init = create_complete_command_mocks()
+ check_parser(sot, [], [('name', None), ('shell', 'bash')])
+
+def test_parser_no_code():
+ sot, app, actions, cmd_factory_init = create_complete_command_mocks()
+ check_parser(sot, ["--shell", "none", "--name", "foo"],
+ [('name', 'foo'), ('shell', 'none')])
+
+def test_get_actions():
+ sot, app, actions, cmd_factory_init = create_complete_command_mocks()
+ result = sot.get_actions("yale")
+ cmd_factory_init.get_parser.assert_called_with('openstack yale')
+ assert actions == result
+
+def test_get_actions_interactive():
+ sot, app, actions, cmd_factory_init = create_complete_command_mocks()
+ app.interactive_mode = True
+ result = sot.get_actions("yale")
+ cmd_factory_init.get_parser.assert_called_with('yale')
+ assert actions == result
+
+def verify_data(content):
+ assert " cmds='image server'\n" in content
+ assert " cmds_image='create'\n" in content
+ assert " cmds_image_create='Eolus Wilson Sunlight'\n" in content
+ assert " cmds_server='meta ssh'\n" in content
+ assert " cmds_server_meta_delete='Eolus Wilson Sunlight'\n" in content
+ assert " cmds_server_ssh='Eolus Wilson Sunlight'\n" in content
+
+def test_take_action_nocode():
+ sot, app, actions, cmd_factory_init = create_complete_command_mocks()
+ parsed_args = mock.Mock()
+ parsed_args.shell = "none"
+ sot.take_action(parsed_args)
+ verify_data(app.stdout.content)
+
+def test_take_action_code():
+ sot, app, actions, cmd_factory_init = create_complete_command_mocks()
+ parsed_args = mock.Mock()
+ parsed_args.name = "openstack"
+ sot.take_action(parsed_args)
+ verify_data(app.stdout.content)
+ assert "_openstack()\n" in app.stdout.content[0]
+ assert "complete -F _openstack openstack\n" in app.stdout.content[-1]
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