diff --git a/internal/core/autocomplete.go b/internal/core/autocomplete.go index b7c4d3032e..764cad8b46 100644 --- a/internal/core/autocomplete.go +++ b/internal/core/autocomplete.go @@ -2,7 +2,6 @@ package core import ( "context" - "os" "regexp" "sort" "strconv" @@ -195,9 +194,9 @@ func (node *AutoCompleteNode) isLeafCommand() bool { } // BuildAutoCompleteTree builds the autocomplete tree from the commands, subcommands and arguments -func BuildAutoCompleteTree(commands *Commands) *AutoCompleteNode { +func BuildAutoCompleteTree(commands *Commands, meta *meta) *AutoCompleteNode { root := NewAutoCompleteCommandNode() - scwCommand := root.GetChildOrCreate(os.Args[0]) + scwCommand := root.GetChildOrCreate(meta.BinaryName) scwCommand.addGlobalFlags() for _, cmd := range commands.commands { node := scwCommand @@ -247,9 +246,10 @@ func BuildAutoCompleteTree(commands *Commands) *AutoCompleteNode { // eg: scw test flower create name=p -o=jso func AutoComplete(ctx context.Context, leftWords []string, wordToComplete string, rightWords []string) *AutocompleteResponse { commands := ExtractCommands(ctx) + meta := extractMeta(ctx) // Create AutoComplete Tree - commandTreeRoot := BuildAutoCompleteTree(commands) + commandTreeRoot := BuildAutoCompleteTree(commands, meta) // For each left word that is not a flag nor an argument, we try to go deeper in the autocomplete tree and store the current node in `node`. node := commandTreeRoot @@ -260,7 +260,7 @@ func AutoComplete(ctx context.Context, leftWords []string, wordToComplete string if i == 0 { // override the command name with the real one // useful when command is renamed or behind a shell function - word = os.Args[0] + word = meta.BinaryName } children, childrenExists := node.Children[word] 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 1bdd649b10..fb86203b8c 100644 --- a/internal/core/cobra_builder.go +++ b/internal/core/cobra_builder.go @@ -1,7 +1,6 @@ package core import ( - "os" "strings" "github.com/spf13/cobra" @@ -26,7 +25,7 @@ func (b *cobraBuilder) build() *cobra.Command { commandsIndex := map[string]*Command{} rootCmd := &cobra.Command{ - Use: os.Args[0], + Use: b.meta.BinaryName, // Do not display error with cobra, we handle it in bootstrap. SilenceErrors: true, @@ -105,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(cmd, b.meta) } if cmd.SeeAlsos != nil { diff --git a/internal/core/cobra_usage_builder.go b/internal/core/cobra_usage_builder.go index 143029c5b2..68515b0016 100644 --- a/internal/core/cobra_usage_builder.go +++ b/internal/core/cobra_usage_builder.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "os" "strings" "text/tabwriter" @@ -71,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(cmd *Command, meta *meta) string { // Build the examples array. var examples []string @@ -98,7 +97,7 @@ func buildExamples(cmd *Command) string { // Build command line example. commandParts := []string{ - os.Args[0], + meta.BinaryName, cmd.Namespace, cmd.Resource, cmd.Verb, diff --git a/internal/core/cobra_utils.go b/internal/core/cobra_utils.go index 6f1e82ea5d..e7c1fc865b 100644 --- a/internal/core/cobra_utils.go +++ b/internal/core/cobra_utils.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "os" "reflect" "strings" @@ -27,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(cmd, rawArgs, meta); err != nil { return err } @@ -107,7 +106,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(cmd *Command, rawArgs []string, meta *meta) error { positionalArg := cmd.ArgSpecs.GetPositionalArg() // Command does not have a positional argument. @@ -125,7 +124,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(cmd, argumentValue, otherArgs, positionalArgumentFound, meta), } } } @@ -139,12 +138,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(cmd, "<"+positionalArg.Name+">", rawArgs, false, meta), } } // positionalArgHint formats the positional argument error hint. -func positionalArgHint(cmd *Command, hintValue string, otherArgs []string, positionalArgumentFound bool) string { +func positionalArgHint(cmd *Command, hintValue string, otherArgs []string, positionalArgumentFound bool, meta *meta) string { suggestedArgs := []string{} // If no positional argument exists, suggest one. @@ -155,7 +154,7 @@ func positionalArgHint(cmd *Command, hintValue string, otherArgs []string, posit // Suggest to use the other arguments. suggestedArgs = append(suggestedArgs, otherArgs...) - suggestedCommand := append([]string{os.Args[0], cmd.GetCommandLine()}, suggestedArgs...) + suggestedCommand := append([]string{meta.BinaryName, cmd.GetCommandLine()}, suggestedArgs...) return "Try running: " + strings.Join(suggestedCommand, " ") } diff --git a/internal/core/context.go b/internal/core/context.go index bec8fc3c8a..2200a7628e 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 @@ -71,3 +73,7 @@ func ExtractEnv(ctx context.Context, envKey string) string { } return os.Getenv(envKey) } + +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 5189d6efe0..78fc3c1370 100644 --- a/internal/namespaces/autocomplete/autocomplete.go +++ b/internal/namespaces/autocomplete/autocomplete.go @@ -36,20 +36,21 @@ type autocompleteScript struct { // 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: fmt.Sprintf(` +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 @@ -60,42 +61,42 @@ var autocompleteScripts = map[string]autocompleteScript{ return } complete -F _%[1]s %[1]s - `, os.Args[0]), - CompleteScript: fmt.Sprintf(`eval "$(%s autocomplete script shell=bash)"`, os.Args[0]), - ShellConfigurationFile: map[string]string{ - "darwin": path.Join(os.Getenv("HOME"), ".bash_profile"), - "linux": path.Join(os.Getenv("HOME"), ".bashrc"), + `, binaryName), + CompleteScript: fmt.Sprintf(`eval "$(%s autocomplete script shell=bash)"`, binaryName), + ShellConfigurationFile: map[string]string{ + "darwin": path.Join(os.Getenv("HOME"), ".bash_profile"), + "linux": path.Join(os.Getenv("HOME"), ".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: fmt.Sprintf(` + "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))'; - `, os.Args[0]), - CompleteScript: fmt.Sprintf(`eval (%s autocomplete script shell=fish)`, os.Args[0]), - ShellConfigurationFile: map[string]string{ - "darwin": path.Join(os.Getenv("HOME"), ".config/fish/config.fish"), - "linux": path.Join(os.Getenv("HOME"), ".config/fish/config.fish"), + `, binaryName), + CompleteScript: fmt.Sprintf(`eval (%s autocomplete script shell=fish)`, binaryName), + ShellConfigurationFile: map[string]string{ + "darwin": path.Join(os.Getenv("HOME"), ".config/fish/config.fish"), + "linux": path.Join(os.Getenv("HOME"), ".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: fmt.Sprintf(` + "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 _%[1]s () { output=($(%[1]s autocomplete complete zsh -- ${CURRENT} ${words})) @@ -106,13 +107,14 @@ var autocompleteScripts = map[string]autocompleteScript{ compadd "${opts[@]}" -- "${output[@]}" } compdef _%[1]s %[1]s - `, os.Args[0]), - CompleteScript: fmt.Sprintf(`eval "$(%s autocomplete script shell=zsh)"`, os.Args[0]), - ShellConfigurationFile: map[string]string{ - "darwin": path.Join(os.Getenv("HOME"), ".zshrc"), - "linux": path.Join(os.Getenv("HOME"), ".zshrc"), + `, binaryName), + CompleteScript: fmt.Sprintf(`eval "$(%s autocomplete script shell=zsh)"`, binaryName), + ShellConfigurationFile: map[string]string{ + "darwin": path.Join(os.Getenv("HOME"), ".zshrc"), + "linux": path.Join(os.Getenv("HOME"), ".zshrc"), + }, }, - }, + } } type InstallArgs struct { @@ -139,6 +141,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 @@ -159,7 +162,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) } @@ -344,8 +347,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) }