diff --git a/components/gitpod-cli/cmd/completion.go b/components/gitpod-cli/cmd/completion.go index 5fcc97995a2678..4479a4bb3ba1c4 100644 --- a/components/gitpod-cli/cmd/completion.go +++ b/components/gitpod-cli/cmd/completion.go @@ -5,6 +5,8 @@ package cmd import ( + "fmt" + "log" "os" "github.com/spf13/cobra" @@ -12,23 +14,59 @@ import ( // completionCmd represents the completion command var completionCmd = &cobra.Command{ - Use: "completion", - Hidden: true, - Short: "Generates bash completion scripts", - Long: `To load completion run + Use: "completion", + Short: "Generates shell completion", + Long: fmt.Sprintf(`Bash: -. <(gp completion) + $ source <(%[1]s completion bash) -To configure your bash shell to load completions for each session add to your bashrc + # To load completions for each session, execute once: + $ %[1]s completion bash | sudo tee /etc/bash_completion.d/%[1]s -# ~/.bashrc or ~/.profile -. <(gp completion) -`, +Zsh: + + # If shell completion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load completions for each session, execute once: + $ echo "%[1]s completion zsh" | sudo tee "${fpath[1]}/_%[1]s" + + # You will need to start a new shell for this setup to take effect. + +fish: + + $ %[1]s completion fish | source + + # To load completions for each session, execute once: + $ mkdir -p ~/.config/fish/completions && %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish + + +`, rootCmd.Root().Name()), + ValidArgs: []string{"bash", "fish", "zsh"}, + Args: cobra.ExactValidArgs(1), Run: func(cmd *cobra.Command, args []string) { - rootCmd.GenBashCompletion(os.Stdout) + switch args[0] { + case "bash": + if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { + err_log("bash") + } + case "fish": + if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { + err_log("fish") + } + case "zsh": + if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { + err_log("zsh") + } + } }, } +func err_log(shell string) { + log.Fatalf("Failed to generate completion for %s", shell) +} + func init() { rootCmd.AddCommand(completionCmd) } diff --git a/components/supervisor/pkg/supervisor/supervisor.go b/components/supervisor/pkg/supervisor/supervisor.go index 1d0b050584063a..ed18b15e6f4cc4 100644 --- a/components/supervisor/pkg/supervisor/supervisor.go +++ b/components/supervisor/pkg/supervisor/supervisor.go @@ -178,6 +178,7 @@ func Run(options ...RunOption) { log.WithError(err).Fatal("cannot ensure Gitpod user exists") } symlinkBinaries(cfg) + installGpCliCompletion() configureGit(cfg, childProcEnvvars) tokenService := NewInMemoryTokenService() @@ -592,6 +593,72 @@ func installDotfiles(ctx context.Context, cfg *Config, tokenService *InMemoryTok } } +func installGpCliCompletion() { + supervisorPath, err := os.Executable() + if err != nil { + log.WithError(err).Error("supervisor bin missing") + return + } + + basedir := filepath.Dir(supervisorPath) + gpcli := filepath.Join(basedir, "gitpod-cli") + gpcli_name := filepath.Base(gpcli) + + if _, err := os.Stat(gpcli); err != nil { + log.WithError(err).Error(gpcli_name, "bin is missing") + return + } + + write_comp := func(bin string, basedir string, shell string) { + + target := filepath.Join(basedir, "gitpod-cli") + + // No need to write completion if it already exists + if _, err := os.Stat(target); err == nil { + return + } + + if err := os.MkdirAll(basedir, 0644); err == nil { + if file, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err == nil { + defer file.Close() + + cmd := exec.Command(bin, "completion", shell) + cmd.Stdout = file + + if err := cmd.Run(); err != nil { + log.WithError(err).Error("Failed to write completion for", shell) + } + } + } + } + + // bash + bash_profile := filepath.FromSlash("/etc/profile") + bash_comp_dir := filepath.FromSlash("/usr/share/bash-completion/completions") + + if _, err := os.Stat(bash_comp_dir); err == nil { + write_comp(gpcli, bash_comp_dir, "bash") + // Unless bash-completion package is installed, do not use it's path and fallback to profile + } else if file, err := os.OpenFile(bash_profile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil { + defer file.Close() + + if _, err := file.WriteString(` +if [ "${PS1-}" ] && [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then + source <(gp completion bash) +fi + `); err != nil { + log.Error(err) + } + } + + // fish + write_comp(gpcli, filepath.FromSlash("/usr/local/share/fish/vendor_completions.d"), "fish") + + // zsh + write_comp(gpcli, filepath.FromSlash("/usr/local/share/zsh/site-functions"), "zsh") + +} + func createGitpodService(cfg *Config, tknsrv api.TokenServiceServer) *gitpod.APIoverJSONRPC { endpoint, host, err := cfg.GitpodAPIEndpoint() if err != nil {