diff --git a/internal/core/autocomplete.go b/internal/core/autocomplete.go index 937ccc6c3e..c987346132 100644 --- a/internal/core/autocomplete.go +++ b/internal/core/autocomplete.go @@ -196,10 +196,9 @@ func (node *AutoCompleteNode) isLeafCommand() bool { // BuildAutoCompleteTree builds the autocomplete tree from the commands, subcommands and arguments func BuildAutoCompleteTree(commands *Commands) *AutoCompleteNode { root := NewAutoCompleteCommandNode() - scwCommand := root.GetChildOrCreate("scw") - scwCommand.addGlobalFlags() + root.addGlobalFlags() for _, cmd := range commands.commands { - node := scwCommand + node := root // Creates nodes for namespaces, resources, verbs for _, part := range []string{cmd.Namespace, cmd.Resource, cmd.Verb} { @@ -255,6 +254,10 @@ func AutoComplete(ctx context.Context, leftWords []string, wordToComplete string // nodeIndexInWords is the rightmost word index, before the cursor, that contains either a namespace or verb or resource or flag or flag value. // see test 'scw test flower delete f' nodeIndexInWords := 0 + + // We remove command binary name from the left words. + leftWords = leftWords[1:] + for i, word := range leftWords { children, childrenExists := node.Children[word] if !childrenExists { diff --git a/internal/core/autocomplete_test.go b/internal/core/autocomplete_test.go index c3a4cb1024..6aa723068e 100644 --- a/internal/core/autocomplete_test.go +++ b/internal/core/autocomplete_test.go @@ -94,7 +94,6 @@ func TestAutocomplete(t *testing.T) { } } - t.Run("scw", run(&testCase{Suggestions: AutocompleteSuggestions{"scw"}})) t.Run("scw ", run(&testCase{Suggestions: AutocompleteSuggestions{"test"}})) t.Run("scw te", run(&testCase{Suggestions: AutocompleteSuggestions{"test"}})) t.Run("scw test", run(&testCase{Suggestions: AutocompleteSuggestions{"test"}})) diff --git a/internal/core/bootstrap.go b/internal/core/bootstrap.go index 4829faa7e2..4c120d4815 100644 --- a/internal/core/bootstrap.go +++ b/internal/core/bootstrap.go @@ -57,6 +57,7 @@ func Bootstrap(config *BootstrapConfig) (exitCode int, result interface{}, err e // Meta store globally available variables like SDK client. // Meta is injected in a context object that will be passed to all commands. meta := &meta{ + BinaryName: config.Args[0], BuildInfo: config.BuildInfo, stdout: config.Stdout, stderr: config.Stderr, diff --git a/internal/core/cobra_builder.go b/internal/core/cobra_builder.go index 3ee37b0b1c..315add018c 100644 --- a/internal/core/cobra_builder.go +++ b/internal/core/cobra_builder.go @@ -25,7 +25,7 @@ func (b *cobraBuilder) build() *cobra.Command { commandsIndex := map[string]*Command{} rootCmd := &cobra.Command{ - Use: "scw", + Use: b.meta.BinaryName, // Do not display error with cobra, we handle it in bootstrap. SilenceErrors: true, @@ -104,7 +104,7 @@ func (b *cobraBuilder) hydrateCobra(cobraCmd *cobra.Command, cmd *Command) { } if cmd.Examples != nil { - cobraCmd.Annotations["Examples"] = buildExamples(cmd) + cobraCmd.Annotations["Examples"] = buildExamples(b.meta.BinaryName, cmd) } if cmd.SeeAlsos != nil { diff --git a/internal/core/cobra_usage_builder.go b/internal/core/cobra_usage_builder.go index f5d8d4139b..540339f75c 100644 --- a/internal/core/cobra_usage_builder.go +++ b/internal/core/cobra_usage_builder.go @@ -70,7 +70,7 @@ func _buildArgShort(as *ArgSpec) string { // buildExamples builds usage examples string. // This string will be used by cobra usage template. -func buildExamples(cmd *Command) string { +func buildExamples(binaryName string, cmd *Command) string { // Build the examples array. var examples []string @@ -110,7 +110,7 @@ func buildExamples(cmd *Command) string { // Build command line example. commandParts := []string{ - "scw", + binaryName, cmd.Namespace, cmd.Resource, cmd.Verb, diff --git a/internal/core/cobra_utils.go b/internal/core/cobra_utils.go index 8627fe4c48..f39e724372 100644 --- a/internal/core/cobra_utils.go +++ b/internal/core/cobra_utils.go @@ -26,7 +26,7 @@ func cobraRun(ctx context.Context, cmd *Command) func(*cobra.Command, []string) cmdArgs := reflect.New(cmd.ArgsType).Interface() // Handle positional argument by catching first argument `` and rewrite it to `=`. - if err = handlePositionalArg(cmd, rawArgs); err != nil { + if err = handlePositionalArg(meta.BinaryName, cmd, rawArgs); err != nil { return err } @@ -113,7 +113,7 @@ func cobraRun(ctx context.Context, cmd *Command) func(*cobra.Command, []string) // - no positional argument is found. // - an unknown positional argument exists in the comand. // - an argument duplicates a positional argument. -func handlePositionalArg(cmd *Command, rawArgs []string) error { +func handlePositionalArg(binaryName string, cmd *Command, rawArgs []string) error { positionalArg := cmd.ArgSpecs.GetPositionalArg() // Command does not have a positional argument. @@ -131,7 +131,7 @@ func handlePositionalArg(cmd *Command, rawArgs []string) error { otherArgs := append(rawArgs[:i], rawArgs[i+1:]...) return &CliError{ Err: fmt.Errorf("a positional argument is required for this command"), - Hint: positionalArgHint(cmd, argumentValue, otherArgs, positionalArgumentFound), + Hint: positionalArgHint(binaryName, cmd, argumentValue, otherArgs, positionalArgumentFound), } } } @@ -145,12 +145,12 @@ func handlePositionalArg(cmd *Command, rawArgs []string) error { // No positional argument found. return &CliError{ Err: fmt.Errorf("a positional argument is required for this command"), - Hint: positionalArgHint(cmd, "<"+positionalArg.Name+">", rawArgs, false), + Hint: positionalArgHint(binaryName, cmd, "<"+positionalArg.Name+">", rawArgs, false), } } // positionalArgHint formats the positional argument error hint. -func positionalArgHint(cmd *Command, hintValue string, otherArgs []string, positionalArgumentFound bool) string { +func positionalArgHint(binaryName string, cmd *Command, hintValue string, otherArgs []string, positionalArgumentFound bool) string { suggestedArgs := []string{} // If no positional argument exists, suggest one. @@ -161,7 +161,7 @@ func positionalArgHint(cmd *Command, hintValue string, otherArgs []string, posit // Suggest to use the other arguments. suggestedArgs = append(suggestedArgs, otherArgs...) - suggestedCommand := append([]string{"scw", cmd.GetCommandLine()}, suggestedArgs...) + suggestedCommand := append([]string{binaryName, cmd.GetCommandLine()}, suggestedArgs...) return "Try running: " + strings.Join(suggestedCommand, " ") } diff --git a/internal/core/context.go b/internal/core/context.go index c5c61ff8de..9f9ebf9014 100644 --- a/internal/core/context.go +++ b/internal/core/context.go @@ -11,6 +11,8 @@ import ( // meta store globally available variables like sdk client or global Flags. type meta struct { + BinaryName string + ProfileFlag string DebugModeFlag bool PrinterTypeFlag printer.Type @@ -93,3 +95,7 @@ func ExtractEnv(ctx context.Context, envKey string) string { func ExtractUserHomeDir(ctx context.Context) string { return ExtractEnv(ctx, "HOME") } + +func ExtractBinaryName(ctx context.Context) string { + return extractMeta(ctx).BinaryName +} diff --git a/internal/namespaces/autocomplete/autocomplete.go b/internal/namespaces/autocomplete/autocomplete.go index aafb94de95..3341327ac3 100644 --- a/internal/namespaces/autocomplete/autocomplete.go +++ b/internal/namespaces/autocomplete/autocomplete.go @@ -38,83 +38,85 @@ var homePath, _ = os.UserHomeDir() // autocompleteScripts regroups the autocomplete scripts for the different shells // The key is the path of the shell. -var autocompleteScripts = map[string]autocompleteScript{ - "bash": { - // If `scw` is the first word on the command line, - // after hitting [tab] arguments are sent to `scw autocomplete complete bash`: - // - COMP_LINE: the complete command line - // - cword: the index of the word being completed (source COMP_CWORD) - // - words: the words composing the command line (source COMP_WORDS) - // - // Note that `=` signs are excluding from $COMP_WORDBREAKS. As a result, they are NOT be - // considered as breaking words and arguments like `image=` will not be split. - // - // Then `scw autocomplete complete bash` process the line, and tries to returns suggestions. - // These scw suggestions are put into `COMPREPLY` which is used by Bash to provides the shell suggestions. - CompleteFunc: ` - _scw() { +func autocompleteScripts(binaryName string) map[string]autocompleteScript { + return map[string]autocompleteScript{ + "bash": { + // If `scw` is the first word on the command line, + // after hitting [tab] arguments are sent to `scw autocomplete complete bash`: + // - COMP_LINE: the complete command line + // - cword: the index of the word being completed (source COMP_CWORD) + // - words: the words composing the command line (source COMP_WORDS) + // + // Note that `=` signs are excluding from $COMP_WORDBREAKS. As a result, they are NOT be + // considered as breaking words and arguments like `image=` will not be split. + // + // Then `scw autocomplete complete bash` process the line, and tries to returns suggestions. + // These scw suggestions are put into `COMPREPLY` which is used by Bash to provides the shell suggestions. + CompleteFunc: fmt.Sprintf(` + _%[1]s() { _get_comp_words_by_ref -n = cword words - output=$(scw autocomplete complete bash -- "$COMP_LINE" "$cword" "${words[@]}") + output=$(%[1]s autocomplete complete bash -- "$COMP_LINE" "$cword" "${words[@]}") COMPREPLY=($output) # apply compopt option and ignore failure for older bash versions [[ $COMPREPLY == *= ]] && compopt -o nospace 2> /dev/null || true return } - complete -F _scw scw - `, - CompleteScript: `eval "$(scw autocomplete script shell=bash)"`, - ShellConfigurationFile: map[string]string{ - "darwin": path.Join(homePath, ".bash_profile"), - "linux": path.Join(homePath, ".bashrc"), + complete -F _%[1]s %[1]s + `, binaryName), + CompleteScript: fmt.Sprintf(`eval "$(%s autocomplete script shell=bash)"`, binaryName), + ShellConfigurationFile: map[string]string{ + "darwin": path.Join(homePath, ".bash_profile"), + "linux": path.Join(homePath, ".bashrc"), + }, }, - }, - "fish": { - // (commandline) complete command line - // (commandline --cursor) position of the cursor, as number of chars in the command line - // (commandline --current-token) word to complete - // (commandline --tokenize --cut-at-cursor) tokenized selection up until the current cursor position - // formatted as one string-type token per line - // - // If files are shown although --no-files is set, - // it might be because you are using an alias for scw, such as : - // alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go' - // You might want to run 'complete --erase --command go' during development. - // - // TODO: send rightWords - CompleteFunc: ` - complete --erase --command scw; - complete --command scw --no-files; - complete --command scw --arguments '(scw autocomplete complete fish -- (commandline) (commandline --cursor) (commandline --current-token) (commandline --current-process --tokenize --cut-at-cursor))'; - `, - CompleteScript: `eval (scw autocomplete script shell=fish)`, - ShellConfigurationFile: map[string]string{ - "darwin": path.Join(homePath, ".config/fish/config.fish"), - "linux": path.Join(homePath, ".config/fish/config.fish"), + "fish": { + // (commandline) complete command line + // (commandline --cursor) position of the cursor, as number of chars in the command line + // (commandline --current-token) word to complete + // (commandline --tokenize --cut-at-cursor) tokenized selection up until the current cursor position + // formatted as one string-type token per line + // + // If files are shown although --no-files is set, + // it might be because you are using an alias for scw, such as : + // alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go' + // You might want to run 'complete --erase --command go' during development. + // + // TODO: send rightWords + CompleteFunc: fmt.Sprintf(` + complete --erase --command %[1]s; + complete --command %[1]s --no-files; + complete --command %[1]s --arguments '(%[1]s autocomplete complete fish -- (commandline) (commandline --cursor) (commandline --current-token) (commandline --current-process --tokenize --cut-at-cursor))'; + `, binaryName), + CompleteScript: fmt.Sprintf(`eval (%s autocomplete script shell=fish)`, binaryName), + ShellConfigurationFile: map[string]string{ + "darwin": path.Join(homePath, ".config/fish/config.fish"), + "linux": path.Join(homePath, ".config/fish/config.fish"), + }, }, - }, - "zsh": { - // If you are using an alias for scw, such as : - // alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go' - // you might want to run 'compdef _scw go' during development. - CompleteFunc: ` + "zsh": { + // If you are using an alias for scw, such as : + // alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go' + // you might want to run 'compdef _scw go' during development. + CompleteFunc: fmt.Sprintf(` autoload -U compinit && compinit - _scw () { - output=($(scw autocomplete complete zsh -- ${CURRENT} ${words})) + _%[1]s () { + output=($(%[1]s autocomplete complete zsh -- ${CURRENT} ${words})) opts=('-S' ' ') if [[ $output == *= ]]; then opts=('-S' '') fi compadd "${opts[@]}" -- "${output[@]}" } - compdef _scw scw - `, - CompleteScript: `eval "$(scw autocomplete script shell=zsh)"`, - ShellConfigurationFile: map[string]string{ - "darwin": path.Join(homePath, ".zshrc"), - "linux": path.Join(homePath, ".zshrc"), + compdef _%[1]s %[1]s + `, binaryName), + CompleteScript: fmt.Sprintf(`eval "$(%s autocomplete script shell=zsh)"`, binaryName), + ShellConfigurationFile: map[string]string{ + "darwin": path.Join(homePath, ".zshrc"), + "linux": path.Join(homePath, ".zshrc"), + }, }, - }, + } } type InstallArgs struct { @@ -141,6 +143,7 @@ func autocompleteInstallCommand() *core.Command { func InstallCommandRun(ctx context.Context, argsI interface{}) (i interface{}, e error) { // Warning _, _ = interactive.Println("To enable autocomplete, scw needs to update your shell configuration.") + binaryName := core.ExtractBinaryName(ctx) // If `shell=` is empty, ask for a value for `shell=`. shellArg := argsI.(*InstallArgs).Shell @@ -161,7 +164,7 @@ func InstallCommandRun(ctx context.Context, argsI interface{}) (i interface{}, e shellName := filepath.Base(shellArg) - script, exists := autocompleteScripts[shellName] + script, exists := autocompleteScripts(binaryName)[shellName] if !exists { return nil, unsupportedShellError(shellName) } @@ -353,8 +356,9 @@ func autocompleteScriptCommand() *core.Command { }, ArgsType: reflect.TypeOf(autocompleteShowArgs{}), Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) { + binaryName := core.ExtractBinaryName(ctx) shell := filepath.Base(argsI.(*autocompleteShowArgs).Shell) - script, exists := autocompleteScripts[shell] + script, exists := autocompleteScripts(binaryName)[shell] if !exists { return nil, unsupportedShellError(shell) }