diff --git a/dev/check/shellcheck.sh b/dev/check/shellcheck.sh index d50dcec56edc..61abeda80cc4 100755 --- a/dev/check/shellcheck.sh +++ b/dev/check/shellcheck.sh @@ -10,7 +10,10 @@ cd "$(dirname "${BASH_SOURCE[0]}")"/../.. SHELL_SCRIPTS=() -while IFS='' read -r line; do SHELL_SCRIPTS+=("$line"); done < <(comm -12 <(git ls-files | sort) <(shfmt -f . | sort)) +# ignore dev/sg/internal/usershell/autocomplete which just houses scripts copied from elsewhere +GREP_IGNORE_FILES="dev/sg/internal/usershell/autocomplete" + +while IFS='' read -r line; do SHELL_SCRIPTS+=("$line"); done < <(comm -12 <(git ls-files | sort) <(shfmt -f . | grep -v $GREP_IGNORE_FILES | sort)) set +e OUT=$(shellcheck --external-sources --source-path="SCRIPTDIR" --color=always "${SHELL_SCRIPTS[@]}") diff --git a/dev/sg/completions.go b/dev/sg/completions.go new file mode 100644 index 000000000000..3a1b5a1024e0 --- /dev/null +++ b/dev/sg/completions.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +// completeOptions provides autocompletions based on the options returned by generateOptions +func completeOptions(generateOptions func() (options []string)) cli.BashCompleteFunc { + return func(cmd *cli.Context) { + for _, opt := range generateOptions() { + fmt.Fprintf(cmd.App.Writer, "%s\n", opt) + } + // Also render default completions to support flags + cli.DefaultCompleteWithFlags(cmd.Command)(cmd) + } +} diff --git a/dev/sg/internal/usershell/autocomplete.go b/dev/sg/internal/usershell/autocomplete.go new file mode 100644 index 000000000000..ce9685634f7b --- /dev/null +++ b/dev/sg/internal/usershell/autocomplete.go @@ -0,0 +1,23 @@ +package usershell + +import ( + _ "embed" + "fmt" + "path/filepath" +) + +var ( + //go:embed autocomplete/bash_autocomplete + bashAutocompleteScript string + //go:embed autocomplete/zsh_autocomplete + zshAutocompleteScript string +) + +var AutocompleteScripts = map[Shell]string{ + BashShell: bashAutocompleteScript, + ZshShell: zshAutocompleteScript, +} + +func AutocompleteScriptPath(sgHome string, shell Shell) string { + return filepath.Join(sgHome, fmt.Sprintf("sg.%s_autocomplete", shell)) +} diff --git a/dev/sg/internal/usershell/autocomplete/README.md b/dev/sg/internal/usershell/autocomplete/README.md new file mode 100644 index 000000000000..6621d58d39e4 --- /dev/null +++ b/dev/sg/internal/usershell/autocomplete/README.md @@ -0,0 +1,3 @@ +# autocomplete scripts + +Autocomplete scripts are sourced from [`urfave/cli/autocomplete`](https://github.com/urfave/cli/tree/master/autocomplete). diff --git a/dev/sg/internal/usershell/autocomplete/bash_autocomplete b/dev/sg/internal/usershell/autocomplete/bash_autocomplete new file mode 100644 index 000000000000..ff6c387e487f --- /dev/null +++ b/dev/sg/internal/usershell/autocomplete/bash_autocomplete @@ -0,0 +1,21 @@ +#! /bin/bash + +: ${PROG:=$(basename ${BASH_SOURCE})} + +_cli_bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + local cur opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ "$cur" == "-"* ]]; then + opts=$(${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion) + else + opts=$(${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion) + fi + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + fi +} + +complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG +unset PROG diff --git a/dev/sg/internal/usershell/autocomplete/zsh_autocomplete b/dev/sg/internal/usershell/autocomplete/zsh_autocomplete new file mode 100644 index 000000000000..52db40798575 --- /dev/null +++ b/dev/sg/internal/usershell/autocomplete/zsh_autocomplete @@ -0,0 +1,25 @@ +#compdef $PROG + +_CLI_ZSH_AUTOCOMPLETE_HACK=1 + +_cli_zsh_autocomplete() { + + local -a opts + local cur + cur=${words[-1]} + if [[ "$cur" == "-"* ]]; then + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") + else + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") + fi + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi + + return +} + +compdef _cli_zsh_autocomplete $PROG diff --git a/dev/sg/sg_ci.go b/dev/sg/sg_ci.go index fbc00e398731..9798a0001d2d 100644 --- a/dev/sg/sg_ci.go +++ b/dev/sg/sg_ci.go @@ -231,6 +231,7 @@ can provide it directly (for example, 'sg ci build [runtype] [argument]'). Learn more about pipeline run types in https://docs.sourcegraph.com/dev/background-information/ci/reference.`, strings.Join(getAllowedBuildTypeArgs(), "\n ")), + BashComplete: completeOptions(getAllowedBuildTypeArgs), Flags: []cli.Flag{ &cli.StringFlag{ Name: "commit", diff --git a/dev/sg/sg_live.go b/dev/sg/sg_live.go index ceb9f0678490..a9f1dbe4d51a 100644 --- a/dev/sg/sg_live.go +++ b/dev/sg/sg_live.go @@ -20,6 +20,9 @@ var liveCommand = &cli.Command{ Category: CategoryCompany, Description: constructLiveCmdLongHelp(), Action: execAdapter(liveExec), + BashComplete: completeOptions(func() (options []string) { + return append(environmentNames(), `https\://...`) + }), } func constructLiveCmdLongHelp() string { diff --git a/dev/sg/sg_run.go b/dev/sg/sg_run.go index 7d945afa4013..aab6db7e8764 100644 --- a/dev/sg/sg_run.go +++ b/dev/sg/sg_run.go @@ -33,6 +33,12 @@ var runCommand = &cli.Command{ addToMacOSFirewallFlag, }, Action: execAdapter(runExec), + BashComplete: completeOptions(func() (options []string) { + for name := range globalConf.Commands { + options = append(options, name) + } + return + }), } func runExec(ctx context.Context, args []string) error { diff --git a/dev/sg/sg_setup.go b/dev/sg/sg_setup.go index 6b0b5bf11680..533d1fd4e612 100644 --- a/dev/sg/sg_setup.go +++ b/dev/sg/sg_setup.go @@ -593,3 +593,73 @@ type stringCommandBuilder func(context.Context) string func (l stringCommandBuilder) Build(ctx context.Context) string { return l(ctx) } + +var dependencyCategoryAdditionalSgConfiguration = dependencyCategory{ + name: "Additional sg configuration", + autoFixing: true, + dependencies: []*dependency{ + { + name: "autocompletions", + check: func(ctx context.Context) error { + if !usershell.IsSupportedShell(ctx) { + return nil // dont do setup + } + sgHome, err := root.GetSGHomePath() + if err != nil { + return err + } + shell := usershell.ShellType(ctx) + autocompletePath := usershell.AutocompleteScriptPath(sgHome, shell) + if _, err := os.Stat(autocompletePath); err != nil { + return errors.Wrapf(err, "autocomplete script for shell %s not found", shell) + } + + shellConfig := usershell.ShellConfigPath(ctx) + conf, err := os.ReadFile(shellConfig) + if err != nil { + return err + } + if !strings.Contains(string(conf), autocompletePath) { + return errors.Newf("autocomplete script %s not found in shell config %s", + autocompletePath, shellConfig) + } + return nil + }, + instructionsCommandsBuilder: stringCommandBuilder(func(ctx context.Context) string { + sgHome, err := root.GetSGHomePath() + if err != nil { + return fmt.Sprintf("echo %s && exit 1", err.Error()) + } + + var commands []string + + shell := usershell.ShellType(ctx) + if shell == "" { + return "echo 'Failed to detect shell type' && exit 1" + } + autocompleteScript := usershell.AutocompleteScripts[shell] + autocompletePath := usershell.AutocompleteScriptPath(sgHome, shell) + commands = append(commands, + fmt.Sprintf(`echo "Writing autocomplete script to %s"`, autocompletePath), + fmt.Sprintf(`echo '%s' > %s`, autocompleteScript, autocompletePath)) + + shellConfig := usershell.ShellConfigPath(ctx) + if shellConfig == "" { + return "echo 'Failed to detect shell config path' && exit 1" + } + conf, err := os.ReadFile(shellConfig) + if err != nil { + return fmt.Sprintf("echo %s && exit 1", err.Error()) + } + if !strings.Contains(string(conf), autocompletePath) { + commands = append(commands, + fmt.Sprintf(`echo "Adding configuration to %s"`, shellConfig), + fmt.Sprintf(`echo "PROG=sg source %s" >> %s`, + autocompletePath, shellConfig)) + } + + return strings.Join(commands, "\n") + }), + }, + }, +} diff --git a/dev/sg/sg_setup_mac.go b/dev/sg/sg_setup_mac.go index 973b99200550..26bfc39ee267 100644 --- a/dev/sg/sg_setup_mac.go +++ b/dev/sg/sg_setup_mac.go @@ -279,4 +279,5 @@ YOU NEED TO RESTART 'sg setup' AFTER RUNNING THIS COMMAND!`, }, }, }, + dependencyCategoryAdditionalSgConfiguration, } diff --git a/dev/sg/sg_setup_ubuntu.go b/dev/sg/sg_setup_ubuntu.go index 2dd7b4b5a30a..5bae22ad1b99 100644 --- a/dev/sg/sg_setup_ubuntu.go +++ b/dev/sg/sg_setup_ubuntu.go @@ -291,4 +291,5 @@ YOU NEED TO RESTART 'sg setup' AFTER RUNNING THIS COMMAND!`, }, }, }, + dependencyCategoryAdditionalSgConfiguration, } diff --git a/dev/sg/sg_start.go b/dev/sg/sg_start.go index 031b6eb8b5d6..d0fc8d900d14 100644 --- a/dev/sg/sg_start.go +++ b/dev/sg/sg_start.go @@ -72,6 +72,12 @@ var ( addToMacOSFirewallFlag, }, + BashComplete: completeOptions(func() (options []string) { + for name := range globalConf.Commandsets { + options = append(options, name) + } + return + }), Action: execAdapter(startExec), } ) diff --git a/dev/sg/sg_tests.go b/dev/sg/sg_tests.go index 416757bc20d9..6c79990f4da4 100644 --- a/dev/sg/sg_tests.go +++ b/dev/sg/sg_tests.go @@ -28,7 +28,13 @@ var testCommand = &cli.Command{ ArgsUsage: "", Usage: "Run the given test suite", Category: CategoryDev, - Action: execAdapter(testExec), + BashComplete: completeOptions(func() (options []string) { + for name := range globalConf.Tests { + options = append(options, name) + } + return + }), + Action: execAdapter(testExec), } func testExec(ctx context.Context, args []string) error { diff --git a/doc/dev/background-information/sg/index.md b/doc/dev/background-information/sg/index.md index fc9d7a38ce26..e7a5b0b5eeeb 100644 --- a/doc/dev/background-information/sg/index.md +++ b/doc/dev/background-information/sg/index.md @@ -110,6 +110,38 @@ On the next command run, if a new version is detected, `sg` will auto update bef See [configuration](#configuration) to learn more about configuring `sg` behaviour. +### Help + +You can get help about commands in a variety of ways: + +```sh +sg help # show all available commands + +# learn about a specific command or subcommand +sg -h +sg --help +``` + +### Autocompletion + +If you have used `sg setup`, you should have autocompletions set up for `sg`. To enable it, type out a partial command and press the Tab key twice. For example: + +```none +sg start +``` + +To get autocompletions for the available flags for a command, type out a command and `-` and press the Tab key twice. For example: + +```none +sg start - +``` + +Both of the above work if you provide partial values as well to narrow down the suggestions. For example, the following will suggest run sets that start with `web-`: + +```none +sg start web- +``` + ### `sg start` - Start dev environments ```bash