diff --git a/contrib/completion/bash/serverauditor b/contrib/completion/bash/serverauditor index c5618a3..74115a8 100644 --- a/contrib/completion/bash/serverauditor +++ b/contrib/completion/bash/serverauditor @@ -1,3 +1,205 @@ +__serverauditor_entity_labels() { + serverauditor "$1"s -f value -c label -c id +} + +__serverauditor_get_labels_ids() { + case $1 in + host|group|identity|key|pfrule|snippet) + __serverauditor_entity_labels $1 + ;; + *) return 0 + ;; + esac +} + +__serverauditor_subcommand_instance() { + case $1 in + host|group|identity|key|pfrule|snippet) + __serverauditor_get_labels_ids $1 + ;; + connect) + __serverauditor_complete_connect + ;; + info) + __serverauditor_complete_info + ;; + *) return 0 + ;; + esac +} + +__serverauditor_complete_option_value() { + __serverauditor_complete_options_$1 $2 +} + +__serverauditor_complete_connect() { + __serverauditor_get_labels_ids host +} + +__serverauditor_complete_info() { + if [[ " ${words[@]} " =~ " -g " ]] || [[ " ${words[@]} " =~ " --group " ]]; then + __serverauditor_get_labels_ids group + else + __serverauditor_get_labels_ids host + fi +} + +__serverauditor_complete_options_complete() { + case $1 in + --shell) + echo bash + ;; + --name) + echo serverauditor + ;; + *) + return 0 + ;; + esac +} +__serverauditor_complete_options_connect() { + return 0 +} +__serverauditor_complete_options_fullclean() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_group() { + case $1 in + -g|--parent-group) + __serverauditor_get_labels_ids group + ;; + -s|--snippet) + __serverauditor_get_labels_ids snippet + ;; + --ssh-identity) + __serverauditor_get_labels_ids identity + ;; + *) + __serverauditor_complete_common_options $1 + ;; + esac +} +__serverauditor_complete_options_groups() { + __serverauditor_complete_common_list_options $1 +} +__serverauditor_complete_options_help() { + return 0 +} +__serverauditor_complete_options_host() { + case $1 in + -g|--group) + __serverauditor_get_labels_ids group + ;; + -t|--tag) + __serverauditor_get_labels_ids tag + ;; + -s|--snippet) + __serverauditor_get_labels_ids snippet + ;; + --ssh-identity) + __serverauditor_get_labels_ids identity + ;; + *) + __serverauditor_complete_common_options $1 + ;; + esac +} +__serverauditor_complete_options_hosts() { + case $1 in + -g|--group) + __serverauditor_get_labels_ids group + ;; + -t|--tag) + __serverauditor_get_labels_ids tag + ;; + *) + __serverauditor_complete_common_list_options $1 + ;; + esac +} +__serverauditor_complete_options_identities() { + __serverauditor_complete_common_list_options $1 +} +__serverauditor_complete_options_identity() { + case $1 in + -k|--ssh-key) + __serverauditor_get_labels_ids key + ;; + *) + __serverauditor_complete_common_options $1 + ;; + esac +} +__serverauditor_complete_options_info() { + case $1 in + -f|--format) + echo json shell ssh table value yaml + ;; + *) + __serverauditor_complete_common_options $1 + ;; + esac +} +__serverauditor_complete_options_key() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_keys() { + __serverauditor_complete_common_list_options $1 +} +__serverauditor_complete_options_login() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_logout() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_pfrule() { + case $1 in + -H|--host) + __serverauditor_get_labels_ids host + ;; + *) + __serverauditor_complete_common_options $1 + ;; + esac +} +__serverauditor_complete_options_pfrules() { + __serverauditor_complete_common_list_options $1 +} +__serverauditor_complete_options_pull() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_push() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_snippet() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_snippets() { + __serverauditor_complete_common_list_options $1 +} + +__serverauditor_complete_options_sync() { + __serverauditor_complete_common_options $1 +} +__serverauditor_complete_options_tags() { + __serverauditor_complete_common_list_options $1 +} + +__serverauditor_complete_common_list_options() { + case $1 in + -f|--format) + echo csv json table value yaml + ;; + *) + __serverauditor_complete_common_options $1 + ;; + esac +} + +__serverauditor_complete_common_options() { + return 0 +} + _serverauditor() { local cur prev words @@ -5,31 +207,77 @@ _serverauditor() _get_comp_words_by_ref -n : cur prev words # Command data: - cmds='complete connect group groups help host hosts identities identity info login logout pfrule pfrules pull push snippet snippets sync tags' + cmds='complete connect fullclean group groups help host hosts identities identity info key keys login logout pfrule pfrules pull push snippet snippets sync tags' cmds_complete='-h --help --name --shell' - cmds_connect='-h --help --log-file -G --group --ssh' - cmds_group='-h --help --log-file -d --delete -I --interactive -L --label --generate-key --ssh -g --parent-group -p --port -S --strict-key-check -s --snippet --ssh-identity -k --keep-alive-packages -u --username -P --password -i --identity-file' + cmds_complete_arg_options='--name --shell' + cmds_connect='-h --help --log-file' + cmds_connect_arg_options='--log-file' + cmds_connect_file_options='--log-file' + cmds_fullclean='-h --help --log-file -s --strategy -p --password' + cmds_fullclean_arg_options='--log-file -s --strategy -p --password' + cmds_fullclean_file_options='--log-file' + cmds_group='-h --help --log-file -g --parent-group -p --port -S --strict-key-check -s --snippet --ssh-identity -k --keep-alive-packages -u --username -P --password -i --identity-file -d --delete -I --interactive -L --label' + cmds_group_arg_options='--log-file -g --parent-group -p --port -s --snippet --ssh-identity -u --username -P --password -i --identity-file -L --label' + cmds_group_file_options='--log-file -i --identity-file' cmds_groups='-h --help -f --format -c --column --max-width --noindent --quote --log-file -r --recursive' + cmds_groups_arg_options='-f --format -c --column --max-width --log-file' + cmds_groups_file_options='--log-file' cmds_help='-h --help' - cmds_host='-h --help --log-file -d --delete -I --interactive -L --label --generate-key --ssh -t --tags -g --group -a --address -p --port -S --strict-key-check -s --snippet --ssh-identity -k --keep-alive-packages -u --username -P --password -i --identity-file' + cmds_host='-h --help --log-file -t --tags -g --group -a --address -p --port -S --strict-key-check -s --snippet --ssh-identity -k --keep-alive-packages -u --username -P --password -i --identity-file -d --delete -I --interactive -L --label' + cmds_host_arg_options='--log-file -t --tags -g --group -a --address -p --port -s --snippet --ssh-identity -u --username -P --password -i --identity-file -L --label' + cmds_host_file_options='--log-file -i --identity-file' cmds_hosts='-h --help -f --format -c --column --max-width --noindent --quote --log-file -t --tags -g --group' + cmds_hosts_arg_options='-f --format -c --column --max-width --log-file -t --tags -g --group' + cmds_hosts_file_options='--log-file' cmds_identities='-h --help -f --format -c --column --max-width --noindent --quote --log-file' - cmds_identity='-h --help --log-file -d --delete -I --interactive -L --label --generate-key -u --username -p --password -i --identity-file -k --ssh-key' - cmds_info='-h --help --log-file -f --format -c --column --variable --prefix --noindent --address --max-width -G --group -H --host -M --no-merge --ssh' - cmds_login='-h --help --log-file -u --username -p --password --sync-sshconfig' - cmds_logout='-h --help --log-file --clear-sshconfig' - cmds_pfrule='-h --help --log-file -d --delete -I --interactive -L --label -H --host --dynamic --remote --local --binding' + cmds_identities_arg_options='-f --format -c --column --max-width --log-file' + cmds_identities_file_options='--log-file' + cmds_identity='-h --help --log-file -u --username -p --password -i --identity-file -k --ssh-key -d --delete -I --interactive -L --label' + cmds_identity_arg_options='--log-file -u --username -p --password -i --identity-file -k --ssh-key -L --label' + cmds_identity_file_options='--log-file -i --identity-file' + cmds_info='-h --help --log-file -G --group -H --host -M --no-merge -f --format -c --column --prefix --noindent --address --max-width' + cmds_info_arg_options='--log-file -f --format -c --column --prefix --address' + cmds_info_file_options='--log-file' + cmds_key='-h --help --log-file -i --identity-file -d --delete -I --interactive -L --label' + cmds_key_arg_options='--log-file -i --identity-file -L --label' + cmds_key_file_options='--log-file -i --identity-file' + cmds_keys='-h --help -f --format -c --column --max-width --noindent --quote --log-file' + cmds_keys_arg_options='-f --format -c --column --max-width --log-file' + cmds_keys_file_options='--log-file' + cmds_login='-h --help --log-file -u --username -p --password' + cmds_login_arg_options='--log-file -u --username -p --password' + cmds_login_file_options='--log-file' + cmds_logout='-h --help --log-file' + cmds_logout_arg_options='--log-file' + cmds_logout_file_options='--log-file' + cmds_pfrule='-h --help --log-file -H --host --dynamic --remote --local --binding -d --delete -I --interactive -L --label' + cmds_pfrule_arg_options='--log-file -H --host --binding -L --label' + cmds_pfrule_file_options='--log-file' cmds_pfrules='-h --help -f --format -c --column --max-width --noindent --quote --log-file' + cmds_pfrules_arg_options='-f --format -c --column --max-width --log-file' + cmds_pfrules_file_options='--log-file' cmds_pull='-h --help --log-file -s --strategy -p --password' + cmds_pull_arg_options='--log-file -s --strategy -p --password' + cmds_pull_file_options='--log-file' cmds_push='-h --help --log-file -s --strategy -p --password' - cmds_snippet='-h --help --log-file -d --delete -I --interactive -L --label -s --script' + cmds_push_arg_options='--log-file -s --strategy -p --password' + cmds_push_file_options='--log-file' + cmds_snippet='-h --help --log-file -s --script -d --delete -I --interactive -L --label' + cmds_snippet_arg_options='--log-file -s --script -L --label' + cmds_snippet_file_options='--log-file' cmds_snippets='-h --help -f --format -c --column --max-width --noindent --quote --log-file' + cmds_snippets_arg_options='-f --format -c --column --max-width --log-file' + cmds_snippets_file_options='--log-file' cmds_sync='-h --help --log-file -c --credentials' + cmds_sync_arg_options='--log-file -c --credentials' + cmds_sync_file_options='--log-file -c --credentials' cmds_tags='-h --help -f --format -c --column --max-width --noindent --quote --log-file -d --delete' + cmds_tags_arg_options='-f --format -c --column --max-width --log-file' + cmds_tags_file_options='--log-file' cmd="" words[0]="" - completed="${cmds}" + completed_options="${cmds}" for var in "${words[@]:1}" do if [[ ${var} == -* ]] ; then @@ -47,18 +295,38 @@ _serverauditor() fi if [[ ${comp} == -* ]] ; then if [[ ${cur} != -* ]] ; then - completed="" + completed_options="" break fi fi cmd="${proposed}" - completed="${comp}" + completed_options="${comp}" done - if [ -z "${completed}" ] ; then - COMPREPLY=( $( compgen -f -- "$cur" ) $( compgen -d -- "$cur" ) ) + subcommand=${words[1]} + arg_options="cmds_${subcommand}_arg_options" + arg_file_options="cmds_${subcommand}_file_options" + + if [ -z "${completed_options}" ] ; + then + if [ -n "${subcommand}" ] ; + then + if [[ "${prev}" == -* ]] && [[ " ${!arg_options} " =~ " ${prev} " ]]; + then + if [[ " ${!arg_file_options} " =~ " ${prev} " ]]; + then + COMPREPLY=( $(compgen -f -- ${cur}) ) + else + COMPREPLY=( $(compgen -W "$(__serverauditor_complete_option_value $subcommand $prev)" -- ${cur}) ) + fi + else + COMPREPLY=( $(compgen -W "$(__serverauditor_subcommand_instance $subcommand)" -- ${cur}) ) + fi + else + COMPREPLY=( $( compgen -W "" -- "$cur" ) ) + fi else - COMPREPLY=( $(compgen -W "${completed}" -- ${cur}) ) + COMPREPLY=( $(compgen -W "${completed_options}" -- ${cur}) ) fi return 0 } diff --git a/tests/integration/completion/bash/completion_test.py b/tests/integration/completion/bash/completion_test.py new file mode 100644 index 0000000..52f64da --- /dev/null +++ b/tests/integration/completion/bash/completion_test.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +import subprocess +import unittest +from pathlib2 import Path +from mock import Mock +from serverauditor_sshconfig.core.storage import ApplicationStorage +from serverauditor_sshconfig.core.models.terminal import Host, Group + + +# inspired from +# https://github.com/lacostej/unity3d-bash-completion +class Completion(): + + full_cmdline_template = ( + r'source {compfile}; ' + r'COMP_LINE="{COMP_LINE}" COMP_WORDS=({COMP_WORDS}) ' + r'COMP_CWORD={COMP_CWORD} COMP_POINT={COMP_POINT}; ' + r'$(complete -p {program} | ' + r'sed "s/.*-F \\([^ ]*\\) .*/\\1/") && ' + r'echo ${{COMPREPLY[*]}}' + ) + + def prepare(self, program, command): + self.program = program + self.COMP_LINE = '%s %s' % (program, command) + self.COMP_WORDS = self.COMP_LINE.rstrip() + + args = command.split() + self.COMP_CWORD = len(args) + self.COMP_POINT = len(self.COMP_LINE) + + if (self.COMP_LINE[-1] == ' '): + self.COMP_WORDS += ' ' + self.COMP_CWORD += 1 + + def run(self, completion_file, program, command): + self.prepare(program, command) + full_cmdline = self.full_cmdline_template.format( + compfile=completion_file, COMP_LINE=self.COMP_LINE, + COMP_WORDS=self.COMP_WORDS, COMP_POINT=self.COMP_POINT, + program=self.program, COMP_CWORD=self.COMP_CWORD + ) + out = subprocess.Popen( + ['bash', '-i', '-c', full_cmdline], + stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + return out.communicate() + + +class CompletionTestCase(unittest.TestCase): + + def test_completion_internal(self): + self.assertEqualCompletion('fake', '', 'fake ', 'fake ', 1, 5) + self.assertEqualCompletion('fake', ' ', 'fake ', 'fake ', 1, 6) + self.assertEqualCompletion('fake', 'a', 'fake a', 'fake a', 1, 6) + self.assertEqualCompletion('fake', 'aa', 'fake aa', 'fake aa', 1, 7) + self.assertEqualCompletion('fake', 'a ', 'fake a ', 'fake a ', 2, 7) + self.assertEqualCompletion('fake', 'a ', 'fake a ', 'fake a ', 2, 9) + self.assertEqualCompletion('fake', 'a a', 'fake a a', 'fake a a', 2, 8) + self.assertEqualCompletion('fake', 'a a ', 'fake a a ', 'fake a a ', 3, 9) + + def assertEqualCompletion(self, program, cline, line, words, cword, point): + c = Completion() + c.prepare(program, cline) + self.assertEqual(c.program, program) + self.assertEqual(c.COMP_LINE, line) + self.assertEqual(c.COMP_WORDS, words) + self.assertEqual(c.COMP_CWORD, cword) + self.assertEqual(c.COMP_POINT, point) + + +class BashCompletionTest(unittest.TestCase): + + def run_complete(self, completion_file, program, command, expected): + print(command) + stdout, stderr = Completion().run(completion_file, program, command) + print(stderr) + self.assertEqual(stdout, expected + '\n') + + +class ServerauditorTestCase(BashCompletionTest): + + program = 'serverauditor' + completion_file = 'contrib/completion/bash/serverauditor' + + def test_nothing(self): + self.run_complete( + '', + 'complete connect fullclean group groups help host hosts ' + 'identities identity info key keys login logout pfrule ' + 'pfrules pull push snippet snippets sync tags' + ) + + def test_subcommand(self): + self.run_complete('ho', 'host hosts') + self.run_complete('grou', 'group groups') + self.run_complete('i', 'identities identity info') + + def test_options_name(self): + self.run_complete('host --ad', '--address') + self.run_complete( + 'host -', + '-h --help --log-file -t --tags -g --group -a --address ' + '-p --port -S --strict-key-check -s --snippet --ssh-identity ' + '-k --keep-alive-packages -u --username -P --password ' + '-i --identity-file -d --delete -I --interactive -L --label' + ) + self.run_complete( + 'info -', + '-h --help --log-file -G --group -H --host -M --no-merge ' + '-f --format -c --column --prefix --noindent ' + '--address --max-width' + ) + + def test_connect_host_label_and_ids(self): + self.run_complete('connect', '') + first = self.client.create_host('localhost', 'Asparagales') + second = self.client.create_host('localhost', 'xanthorrhoeaceae') + instances = (first, second) + self.run_complete('connect ', ' '.join(['{} {}'.format(i.id, i.label) for i in instances])) + self.run_complete('connect As', 'Asparagales') + self.run_complete('connect xa', 'xanthorrhoeaceae') + + def test_info_host_label_and_ids(self): + self.run_complete('info', '') + first = self.client.create_host('localhost', 'Asparagales') + second = self.client.create_host('localhost', 'xanthorrhoeaceae') + instances = (first, second) + self.run_complete('info ', ' '.join(['{} {}'.format(i.id, i.label) for i in instances])) + self.run_complete('info As', 'Asparagales') + self.run_complete('info xa', 'xanthorrhoeaceae') + + def test_info_group_label_and_ids(self): + self.run_complete('info', '') + first = self.client.create_group('Asparagales') + second = self.client.create_group('xanthorrhoeaceae') + instances = (first, second) + for option in ('-g', '--group'): + subcommand = 'info {} '.format(option) + self.run_complete(subcommand, ' '.join(['{} {}'.format(i.id, i.label) for i in instances])) + self.run_complete(subcommand + ' As'.format(option), 'Asparagales') + self.run_complete(subcommand + ' xa', 'xanthorrhoeaceae') + + def test_update_entity(self): + entity = 'host' + self.run_complete(entity, '') + first = self.client.create_host('localhost', 'Asparagales') + second = self.client.create_host('localhost', 'xanthorrhoeaceae') + instances = (first, second) + self.run_complete(entity + ' ', ' '.join(['{} {}'.format(i.id, i.label) for i in instances])) + self.run_complete(entity + ' As', 'Asparagales') + self.run_complete(entity + ' xa', 'xanthorrhoeaceae') + + def test_list_format_types(self): + for subcommand in ('hosts', 'groups', 'tags', 'identities', 'snippets', 'pfrules', 'keys'): + self.run_complete(subcommand + ' -f ', 'csv json table value yaml') + self.run_complete(subcommand + ' --format ', 'csv json table value yaml') + + def run_complete(self, command, expected): + super(ServerauditorTestCase, self).run_complete( + self.completion_file, self.program, command, expected + ) + + def setUp(self): + self.client = ServerauditorClient() + self.client.clean() + + def tearDown(self): + self.client.clean() + + +class ServerauditorClient(object): + + def __init__(self): + self.app_directory = Path('~/.serverauditor/').expanduser() + if not self.app_directory.is_dir(): + self.app_directory.mkdir() + command_mock = Mock(**{'app.directory_path': self.app_directory}) + self.storage = ApplicationStorage(command_mock) + + def create_host(self, address, label): + return self._create_instance(Host, label=label, address=address) + + def create_group(self, label): + return self._create_instance(Group, label=label) + + def _create_instance(self, model, **kwargs): + instance = model(**kwargs) + with self.storage: + return self.storage.save(instance) + + def clean(self): + if self.app_directory.is_dir(): + self._clean_dir(self.app_directory) + + def _clean_dir(self, dir_path): + [self._clean_dir(x) for x in dir_path.iterdir() if x.is_dir()] + [i.unlink() for i in dir_path.iterdir() if x.is_file()] + dir_path.rmdir()