Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
dev/sg: command, flag, and argument autocompletions (#33817)
Browse files Browse the repository at this point in the history
Adds a new dependency in `sg setup` that writes the appropriate completion script for your shell into `~/.sourcegraph` and adds a line to your shell config to use the completion script. Completions are generated by `sg` itself, so there is no need to be able to update this script (and if we do need to update it, we can consider a migration path then)

`urfave/cli` completions work by providing command suggestions by default, and if you use `-` <tab><tab> you can get flag completions as well.

Also adds:

1. Custom completions based on config for `sg start`, `sg run`, `sg test`
2. Custom completions based on valid values for`sg live`, `sg ci build`
  • Loading branch information
bobheadxi authored Apr 13, 2022
1 parent a8f4c52 commit 3fce7bc
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 2 deletions.
5 changes: 4 additions & 1 deletion dev/check/shellcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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[@]}")
Expand Down
18 changes: 18 additions & 0 deletions dev/sg/completions.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
23 changes: 23 additions & 0 deletions dev/sg/internal/usershell/autocomplete.go
Original file line number Diff line number Diff line change
@@ -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))
}
3 changes: 3 additions & 0 deletions dev/sg/internal/usershell/autocomplete/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# autocomplete scripts

Autocomplete scripts are sourced from [`urfave/cli/autocomplete`](https://github.com/urfave/cli/tree/master/autocomplete).
21 changes: 21 additions & 0 deletions dev/sg/internal/usershell/autocomplete/bash_autocomplete
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions dev/sg/internal/usershell/autocomplete/zsh_autocomplete
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions dev/sg/sg_ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions dev/sg/sg_live.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions dev/sg/sg_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
70 changes: 70 additions & 0 deletions dev/sg/sg_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}),
},
},
}
1 change: 1 addition & 0 deletions dev/sg/sg_setup_mac.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,5 @@ YOU NEED TO RESTART 'sg setup' AFTER RUNNING THIS COMMAND!`,
},
},
},
dependencyCategoryAdditionalSgConfiguration,
}
1 change: 1 addition & 0 deletions dev/sg/sg_setup_ubuntu.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,5 @@ YOU NEED TO RESTART 'sg setup' AFTER RUNNING THIS COMMAND!`,
},
},
},
dependencyCategoryAdditionalSgConfiguration,
}
6 changes: 6 additions & 0 deletions dev/sg/sg_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ var (

addToMacOSFirewallFlag,
},
BashComplete: completeOptions(func() (options []string) {
for name := range globalConf.Commandsets {
options = append(options, name)
}
return
}),
Action: execAdapter(startExec),
}
)
Expand Down
8 changes: 7 additions & 1 deletion dev/sg/sg_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ var testCommand = &cli.Command{
ArgsUsage: "<testsuite>",
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 {
Expand Down
32 changes: 32 additions & 0 deletions doc/dev/background-information/sg/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command> -h
sg <command> --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 <kbd>Tab</kbd> key twice. For example:

```none
sg start<tab><tab>
```

To get autocompletions for the available flags for a command, type out a command and `-` and press the <kbd>Tab</kbd> key twice. For example:

```none
sg start -<tab><tab>
```

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-<tab><tab>
```

### `sg start` - Start dev environments

```bash
Expand Down

0 comments on commit 3fce7bc

Please sign in to comment.