From 361760e97a76f895d7e9131f0d9063f5bdc6c6ca Mon Sep 17 00:00:00 2001 From: meshde Date: Wed, 10 Oct 2018 09:23:52 +0530 Subject: [PATCH 01/10] Make option flags available in completion even if prev word is another option Fix #136 for Bash --- fire/completion.py | 93 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 8b74776f..3ed65097 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -47,12 +47,30 @@ def _BashScript(name, commands, default_options=None): completion in Bash. """ default_options = default_options or set() - options_map = collections.defaultdict(lambda: copy.copy(default_options)) + global_options = copy(default_options) + options_map = defaultdict(lambda: copy(default_options)) + subcommands_map = defaultdict(lambda: set()) + for command in commands: - start = (name + ' ' + ' '.join(command[:-1])).strip() - completion = _FormatForCommand(command[-1]) - options_map[start].add(completion) - options_map[start.replace('_', '-')].add(completion) + if len(command) == 1: + + if _isOption(command[0]): + global_options.add(command[0]) + else: + subcommands_map[name].add(command[0]) + + elif len(command) != 0: + + subcommand = command[-2] + arg = _FormatForCommand(command[-1]) + + if _isOption(arg): + args_map = options_map + else: + args_map = subcommands_map + + args_map[subcommand].add(arg) + args_map[subcommand.replace('_', '-')].add(arg) bash_completion_template = """# bash completion support for {name} # DO NOT EDIT. @@ -60,41 +78,70 @@ def _BashScript(name, commands, default_options=None): _complete-{identifier}() {{ - local start cur opts + local cur opts lastcommand COMPREPLY=() - start="${{COMP_WORDS[@]:0:COMP_CWORD}}" cur="${{COMP_WORDS[COMP_CWORD]}}" + lastcommand=$(get_lastcommand) opts="{default_options}" + GLOBAL_OPTIONS="{global_options}" -{start_checks} +{checks} COMPREPLY=( $(compgen -W "${{opts}}" -- ${{cur}}) ) return 0 }} +get_lastcommand() +{{ + local lastcommand i + + lastcommand= + for ((i=0; i < ${{#COMP_WORDS[@]}}; ++i)); do + if [[ ${{COMP_WORDS[i]}} != -* ]] && [[ -n ${{COMP_WORDS[i]}} ]] && [[ + ${{COMP_WORDS[i]}} != $cur ]]; then + lastcommand=${{COMP_WORDS[i]}} + fi + done + + echo $lastcommand +}} + complete -F _complete-{identifier} {command} """ - start_check_template = """ - if [[ "$start" == "{start}" ]] ; then - opts="{completions}" - fi""" - - start_checks = '\n'.join( - start_check_template.format( - start=start, - completions=' '.join(sorted(options_map[start])) - ) - for start in options_map + + check_wrapper = """ + case "${{lastcommand}}" in + {lastcommand_checks} + esac""" + + lastcommand_check_template = """ + {command}) + opts="{options} ${{GLOBAL_OPTIONS}}" + ;;""" + + lastcommand_checks = '\n'.join( + lastcommand_check_template.format( + command=command, + options=' '.join(sorted( + options_map[command].union(subcommands_map[command]) + )) + ) + for command in subcommands_map.keys() + options_map.keys() + ) + + checks = check_wrapper.format( + lastcommand_checks=lastcommand_checks, ) return ( bash_completion_template.format( name=name, command=name, - start_checks=start_checks, + checks=checks, default_options=' '.join(default_options), - identifier=name.replace('/', '').replace('.', '').replace(',', '') + identifier=name.replace('/', '').replace('.', '').replace(',', ''), + global_options=' '.join(global_options), ) ) @@ -302,3 +349,7 @@ def _Commands(component, depth=3): for command in _Commands(member, depth - 1): yield (member_name,) + command + + +def _isOption(arg): + return arg.startswith('-') From 2465cf7a32003833ba9ae4b87308f2a60d6d8f54 Mon Sep 17 00:00:00 2001 From: meshde Date: Fri, 12 Oct 2018 02:54:18 +0530 Subject: [PATCH 02/10] Refactor: Move preprocessing steps in _BashScript into separate func --- fire/completion.py | 56 ++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 3ed65097..3b7d13ed 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -46,31 +46,11 @@ def _BashScript(name, commands, default_options=None): A string which is the Bash script. Source the bash script to enable tab completion in Bash. """ - default_options = default_options or set() - global_options = copy(default_options) - options_map = defaultdict(lambda: copy(default_options)) - subcommands_map = defaultdict(lambda: set()) - - for command in commands: - if len(command) == 1: - - if _isOption(command[0]): - global_options.add(command[0]) - else: - subcommands_map[name].add(command[0]) - - elif len(command) != 0: - - subcommand = command[-2] - arg = _FormatForCommand(command[-1]) - if _isOption(arg): - args_map = options_map - else: - args_map = subcommands_map - - args_map[subcommand].add(arg) - args_map[subcommand.replace('_', '-')].add(arg) + default_options = default_options or set() + global_options, options_map, subcommands_map = _GetMaps( + name, commands, default_options + ) bash_completion_template = """# bash completion support for {name} # DO NOT EDIT. @@ -353,3 +333,31 @@ def _Commands(component, depth=3): def _isOption(arg): return arg.startswith('-') + +def _GetMaps(name, commands, default_options): + global_options = copy(default_options) + options_map = defaultdict(lambda: copy(default_options)) + subcommands_map = defaultdict(lambda: set()) + + for command in commands: + if len(command) == 1: + + if _isOption(command[0]): + global_options.add(command[0]) + else: + subcommands_map[name].add(command[0]) + + elif len(command) != 0: + + subcommand = command[-2] + arg = _FormatForCommand(command[-1]) + + if _isOption(arg): + args_map = options_map + else: + args_map = subcommands_map + + args_map[subcommand].add(arg) + args_map[subcommand.replace('_', '-')].add(arg) + + return global_options, options_map, subcommands_map From 0738229b09929e12d608aa61f17fb7913729a1cd Mon Sep 17 00:00:00 2001 From: meshde Date: Fri, 12 Oct 2018 03:18:06 +0530 Subject: [PATCH 03/10] Fix #136 for Fish --- fire/completion.py | 58 +++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 3b7d13ed..7abaa711 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -141,43 +141,47 @@ def _FishScript(name, commands, default_options=None): completion in Fish. """ default_options = default_options or set() - options_map = collections.defaultdict(lambda: copy.copy(default_options)) - for command in commands: - start = (name + ' ' + ' '.join(command[:-1])).strip() - completion = _FormatForCommand(command[-1]) - options_map[start].add(completion) - options_map[start.replace('_', '-')].add(completion) + global_options, options_map, subcommands_map = _GetMaps( + name, commands, default_options + ) + fish_source = """function __fish_using_command set cmd (commandline -opc) - if [ (count $cmd) -eq (count $argv) ] - for i in (seq (count $argv)) - if [ $cmd[$i] != $argv[$i] ] + for i in (seq (count $cmd) 1) + switch $cmd[$i] + case "-*" + case "*" + if [ $cmd[$i] = $argv[1] ] + return 0 + else return 1 end end - return 0 end return 1 end """ - subcommand_template = ("complete -c {name} -n '__fish_using_command {start}' " - "-f -a {subcommand}\n") + + subcommand_template = ("complete -c {name} -n '__fish_using_command " + "{command}' -f -a {subcommand}\n") flag_template = ("complete -c {name} -n " - "'__fish_using_command {start}' -l {option}\n") - for start in options_map: - for option in sorted(options_map[start]): - if option.startswith('--'): - fish_source += flag_template.format( - name=name, - start=start, - option=option[2:] - ) - else: - fish_source += subcommand_template.format( - name=name, - start=start, - subcommand=option - ) + "'__fish_using_command {command}' -l {option}\n") + + for command in subcommands_map.keys() + options_map.keys(): + for subcommand in subcommands_map[command]: + fish_source += subcommand_template.format( + name=name, + command=command, + subcommand=subcommand, + ) + + for option in options_map[command].union(global_options): + fish_source += flag_template.format( + name=name, + command=command, + option=option.lstrip("--"), + ) + return fish_source From 43942da3b6e3a24be8e1ce74dccda29df29400e6 Mon Sep 17 00:00:00 2001 From: meshde Date: Sat, 13 Oct 2018 03:41:47 +0530 Subject: [PATCH 04/10] Remove entered options from completion suggestions --- fire/completion.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/fire/completion.py b/fire/completion.py index 7abaa711..0fbb0259 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -87,6 +87,32 @@ def _BashScript(name, commands, default_options=None): echo $lastcommand }} +filter_options() +{{ + local opts + opts="" + for opt in "$@" + do + if ! option_already_entered $opt; then + opts="$opts $opt" + fi + done + + echo $opts +}} + +option_already_entered() +{{ + local opt + for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}} + do + if [ $1 == $opt ]; then + return 0 + fi + done + return 1 +}} + complete -F _complete-{identifier} {command} """ @@ -98,6 +124,7 @@ def _BashScript(name, commands, default_options=None): lastcommand_check_template = """ {command}) opts="{options} ${{GLOBAL_OPTIONS}}" + opts=$(filter_options $opts) ;;""" lastcommand_checks = '\n'.join( @@ -160,12 +187,26 @@ def _FishScript(name, commands, default_options=None): end return 1 end + +function __option_entered_check + set cmd (commandline -opc) + for i in (seq (count $cmd)) + switch $cmd[$i] + case "-*" + if [ $cmd[$i] = $argv[1] ] + return 1 + end + end + end + return 0 +end """ subcommand_template = ("complete -c {name} -n '__fish_using_command " "{command}' -f -a {subcommand}\n") flag_template = ("complete -c {name} -n " - "'__fish_using_command {command}' -l {option}\n") + "'__fish_using_command {command}; and __option_entered_check --{option}'" + " -l {option}\n") for command in subcommands_map.keys() + options_map.keys(): for subcommand in subcommands_map[command]: From 2381503f631b715cfdc72e0836ea05c741d34d51 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 14 Oct 2018 01:31:56 +0530 Subject: [PATCH 05/10] Ensure module flags appear before a func or after its args After a function is entered as a subcommand, the completion shows both function args and module flags. But if a module flag is entered following a function or its args, the function args are no longer shown in completion suggestion. --- fire/completion.py | 68 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 0fbb0259..93a28221 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -58,8 +58,9 @@ def _BashScript(name, commands, default_options=None): _complete-{identifier}() {{ - local cur opts lastcommand + local cur prev opts lastcommand COMPREPLY=() + prev="${{COMP_WORDS[COMP_CWORD-1]}}" cur="${{COMP_WORDS[COMP_CWORD]}}" lastcommand=$(get_lastcommand) @@ -113,6 +114,18 @@ def _BashScript(name, commands, default_options=None): return 1 }} +is_prev_global() +{{ + local opt + for opt in $GLOBAL_OPTIONS + do + if [ $opt == $prev ]; then + return 0 + fi + done + return 1 +}} + complete -F _complete-{identifier} {command} """ @@ -123,16 +136,34 @@ def _BashScript(name, commands, default_options=None): lastcommand_check_template = """ {command}) - opts="{options} ${{GLOBAL_OPTIONS}}" + {opts_assignment} opts=$(filter_options $opts) ;;""" + opts_assignment_subcommand_template = """ + if is_prev_global; then + opts="${{GLOBAL_OPTIONS}}" + else + opts="{options} ${{GLOBAL_OPTIONS}}" + fi""" + + opts_assignment_main_command_template = """ + opts="{options} ${{GLOBAL_OPTIONS}}" """ + + def get_opts_assignment_template(command): + if command == name: + return opts_assignment_main_command_template + else: + return opts_assignment_subcommand_template + lastcommand_checks = '\n'.join( lastcommand_check_template.format( command=command, - options=' '.join(sorted( - options_map[command].union(subcommands_map[command]) - )) + opts_assignment=get_opts_assignment_template(command).format( + options=' '.join(sorted( + options_map[command].union(subcommands_map[command]) + )), + ), ) for command in subcommands_map.keys() + options_map.keys() ) @@ -200,14 +231,30 @@ def _FishScript(name, commands, default_options=None): end return 0 end + +function __is_prev_global + set cmd (commandline -opc) + set global_options {global_options} + set prev (count $cmd) + + for opt in $global_options + if [ "--$opt" = $cmd[$prev] ] + echo $prev + return 0 + end + end + return 1 +end + """ subcommand_template = ("complete -c {name} -n '__fish_using_command " "{command}' -f -a {subcommand}\n") flag_template = ("complete -c {name} -n " - "'__fish_using_command {command}; and __option_entered_check --{option}'" + "'__fish_using_command {command};{prev_global_check} and __option_entered_check --{option}'" " -l {option}\n") + prev_global_check = " and __is_prev_global;" for command in subcommands_map.keys() + options_map.keys(): for subcommand in subcommands_map[command]: fish_source += subcommand_template.format( @@ -217,13 +264,20 @@ def _FishScript(name, commands, default_options=None): ) for option in options_map[command].union(global_options): + check_needed = command != name fish_source += flag_template.format( name=name, command=command, + prev_global_check=prev_global_check if check_needed else "", option=option.lstrip("--"), ) - return fish_source + return fish_source.format( + global_options=' '.join( + '"{option}"'.format(option=option) + for option in global_options + ) + ) def _IncludeMember(name, verbose): From f558ebc95f1256784dda679f2529298cc51e2a16 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 14 Oct 2018 15:28:52 +0530 Subject: [PATCH 06/10] Update TabCompletionTest --- fire/completion_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fire/completion_test.py b/fire/completion_test.py index 47d80b4a..f767ab74 100644 --- a/fire/completion_test.py +++ b/fire/completion_test.py @@ -36,7 +36,10 @@ def testCompletionBashScript(self): script = completion._BashScript(name='command', commands=commands) # pylint: disable=protected-access self.assertIn('command', script) self.assertIn('halt', script) - self.assertIn('"$start" == "command"', script) + + assert_template = "{command})" + for last_command in ['command', 'run', 'halt']: + self.assertIn(assert_template.format(command=last_command), script) def testCompletionFishScript(self): # A sanity check test to make sure the fish completion script satisfies From 708b2cc3adb3231c87fc606f50a03dea007387b4 Mon Sep 17 00:00:00 2001 From: meshde Date: Tue, 16 Oct 2018 01:49:09 +0530 Subject: [PATCH 07/10] Minor Correction in completion_test --- fire/completion_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/completion_test.py b/fire/completion_test.py index f767ab74..b9498d09 100644 --- a/fire/completion_test.py +++ b/fire/completion_test.py @@ -38,7 +38,7 @@ def testCompletionBashScript(self): self.assertIn('halt', script) assert_template = "{command})" - for last_command in ['command', 'run', 'halt']: + for last_command in ['command', 'halt']: self.assertIn(assert_template.format(command=last_command), script) def testCompletionFishScript(self): From c01b01182b9f41e6bd8980f9915b1ad298da2383 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 4 Nov 2018 08:48:10 +0530 Subject: [PATCH 08/10] Fix iterating through a command more than once while generating its completion script code --- fire/completion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 93a28221..effbc9ae 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -165,7 +165,7 @@ def get_opts_assignment_template(command): )), ), ) - for command in subcommands_map.keys() + options_map.keys() + for command in set(subcommands_map.keys()).union(set(options_map.keys())) ) checks = check_wrapper.format( @@ -255,7 +255,7 @@ def _FishScript(name, commands, default_options=None): " -l {option}\n") prev_global_check = " and __is_prev_global;" - for command in subcommands_map.keys() + options_map.keys(): + for command in set(subcommands_map.keys()).union(set(options_map.keys())): for subcommand in subcommands_map[command]: fish_source += subcommand_template.format( name=name, From 1fd24246f2dcfe09548c54453adc3acd447bf226 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 4 Nov 2018 09:55:32 +0530 Subject: [PATCH 09/10] pylint fixes --- fire/completion.py | 78 +++++++++++++++++++++++++---------------- fire/completion_test.py | 2 +- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index effbc9ae..77bb286d 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -49,7 +49,7 @@ def _BashScript(name, commands, default_options=None): default_options = default_options or set() global_options, options_map, subcommands_map = _GetMaps( - name, commands, default_options + name, commands, default_options ) bash_completion_template = """# bash completion support for {name} @@ -150,26 +150,26 @@ def _BashScript(name, commands, default_options=None): opts_assignment_main_command_template = """ opts="{options} ${{GLOBAL_OPTIONS}}" """ - def get_opts_assignment_template(command): + def _GetOptsAssignmentTemplate(command): if command == name: return opts_assignment_main_command_template else: return opts_assignment_subcommand_template lastcommand_checks = '\n'.join( - lastcommand_check_template.format( - command=command, - opts_assignment=get_opts_assignment_template(command).format( - options=' '.join(sorted( - options_map[command].union(subcommands_map[command]) - )), - ), - ) - for command in set(subcommands_map.keys()).union(set(options_map.keys())) + lastcommand_check_template.format( + command=command, + opts_assignment=_GetOptsAssignmentTemplate(command).format( + options=' '.join(sorted( + options_map[command].union(subcommands_map[command]) + )), + ), + ) + for command in set(subcommands_map.keys()).union(set(options_map.keys())) ) checks = check_wrapper.format( - lastcommand_checks=lastcommand_checks, + lastcommand_checks=lastcommand_checks, ) return ( @@ -200,7 +200,7 @@ def _FishScript(name, commands, default_options=None): """ default_options = default_options or set() global_options, options_map, subcommands_map = _GetMaps( - name, commands, default_options + name, commands, default_options ) fish_source = """function __fish_using_command @@ -251,32 +251,32 @@ def _FishScript(name, commands, default_options=None): subcommand_template = ("complete -c {name} -n '__fish_using_command " "{command}' -f -a {subcommand}\n") flag_template = ("complete -c {name} -n " - "'__fish_using_command {command};{prev_global_check} and __option_entered_check --{option}'" - " -l {option}\n") + "'__fish_using_command {command};{prev_global_check} and " + "__option_entered_check --{option}' -l {option}\n") prev_global_check = " and __is_prev_global;" for command in set(subcommands_map.keys()).union(set(options_map.keys())): for subcommand in subcommands_map[command]: fish_source += subcommand_template.format( - name=name, - command=command, - subcommand=subcommand, + name=name, + command=command, + subcommand=subcommand, ) for option in options_map[command].union(global_options): check_needed = command != name fish_source += flag_template.format( - name=name, - command=command, - prev_global_check=prev_global_check if check_needed else "", - option=option.lstrip("--"), + name=name, + command=command, + prev_global_check=prev_global_check if check_needed else "", + option=option.lstrip("--"), ) return fish_source.format( - global_options=' '.join( - '"{option}"'.format(option=option) - for option in global_options - ) + global_options=' '.join( + '"{option}"'.format(option=option) + for option in global_options + ) ) @@ -430,28 +430,44 @@ def _Commands(component, depth=3): yield (member_name,) + command -def _isOption(arg): +def _IsOption(arg): return arg.startswith('-') def _GetMaps(name, commands, default_options): + """Returns sets of subcommands and options for each command. + + Args: + name: The first token in the commands, also the name of the command. + commands: A list of all possible commands that tab completion can complete + to. Each command is a list or tuple of the string tokens that make up + that command. + default_options: A dict of options that can be used with any command. Use + this if there are flags that can always be appended to a command. + Returns: + global_options: A set of all options of the first token of the command. + subcommands_map: A dict storing set of subcommands for each + command/subcommand. + options_map: A dict storing set of options for each subcommand. + """ + global_options = copy(default_options) options_map = defaultdict(lambda: copy(default_options)) - subcommands_map = defaultdict(lambda: set()) + subcommands_map = defaultdict(set()) for command in commands: if len(command) == 1: - if _isOption(command[0]): + if _IsOption(command[0]): global_options.add(command[0]) else: subcommands_map[name].add(command[0]) - elif len(command) != 0: + elif command: subcommand = command[-2] arg = _FormatForCommand(command[-1]) - if _isOption(arg): + if _IsOption(arg): args_map = options_map else: args_map = subcommands_map diff --git a/fire/completion_test.py b/fire/completion_test.py index b9498d09..eaf80a70 100644 --- a/fire/completion_test.py +++ b/fire/completion_test.py @@ -39,7 +39,7 @@ def testCompletionBashScript(self): assert_template = "{command})" for last_command in ['command', 'halt']: - self.assertIn(assert_template.format(command=last_command), script) + self.assertIn(assert_template.format(command=last_command), script) def testCompletionFishScript(self): # A sanity check test to make sure the fish completion script satisfies From da780bc676017f259989a0a4e0754b4521b6d6e1 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 4 Nov 2018 10:04:06 +0530 Subject: [PATCH 10/10] Minor fix --- fire/completion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 77bb286d..e766a89f 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -450,9 +450,9 @@ def _GetMaps(name, commands, default_options): options_map: A dict storing set of options for each subcommand. """ - global_options = copy(default_options) - options_map = defaultdict(lambda: copy(default_options)) - subcommands_map = defaultdict(set()) + global_options = copy.copy(default_options) + options_map = collections.defaultdict(lambda: copy.copy(default_options)) + subcommands_map = collections.defaultdict(set) for command in commands: if len(command) == 1: