Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shell completion #2788

Merged
merged 4 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ func NewApp(writer io.Writer, errWriter io.Writer) *cli.App {
app.Version = version.GetVersion()
app.Writer = writer
app.ErrWriter = errWriter
app.Flags = commands.NewGlobalFlags(opts)
app.Flags = append(
commands.NewGlobalFlags(opts),
commands.NewHelpVersionFlags(opts)...)
app.Commands = append(
deprecatedCommands(opts),
terragruntCommands(opts)...)
Expand Down
43 changes: 42 additions & 1 deletion cli/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/gruntwork-io/go-commons/errors"
Expand Down Expand Up @@ -441,7 +442,9 @@ func runAppTest(args []string, opts *options.TerragruntOptions) (*options.Terrag
app := cli.NewApp()
app.Writer = &bytes.Buffer{}
app.ErrWriter = &bytes.Buffer{}
app.Flags = commands.NewGlobalFlags(opts)
app.Flags = append(
commands.NewGlobalFlags(opts),
commands.NewHelpVersionFlags(opts)...)
app.Commands = append(
deprecatedCommands(opts),
terragruntCommands...)
Expand All @@ -463,3 +466,41 @@ type argMissingValueError string
func (err argMissingValueError) Error() string {
return fmt.Sprintf("flag needs an argument: -%s", string(err))
}

func TestAutocomplete(t *testing.T) {
defer os.Unsetenv("COMP_LINE")

testCases := []struct {
compLine string
expectedCompletes []string
}{
{
"",
[]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"},
},
{
"--versio",
[]string{"--version"},
},
{
"render-json -",
[]string{"--terragrunt-json-out", "--with-metadata"},
},
{
"run-all ren",
[]string{"render-json"},
},
}

for _, testCase := range testCases {
os.Setenv("COMP_LINE", "terragrunt "+testCase.compLine)

output := &bytes.Buffer{}
app := NewApp(output, os.Stderr)

err := app.Run([]string{"terragrunt"})
require.NoError(t, err)

assert.Contains(t, output.String(), strings.Join(testCase.expectedCompletes, "\n"))
}
}
11 changes: 6 additions & 5 deletions cli/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,14 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags {

flags.Sort()

// add auxiliary flags after sorting to put the flag at the end of the flag list in the help.
flags.Add(
return flags
}

func NewHelpVersionFlags(opts *options.TerragruntOptions) cli.Flags {
return cli.Flags{
NewHelpFlag(opts),
NewVersionFlag(opts),
)

return flags
}
}

func NewHelpFlag(opts *options.TerragruntOptions) cli.Flag {
Expand Down
2 changes: 2 additions & 0 deletions cli/commands/run-all/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package runall
import (
"sort"

"github.com/gruntwork-io/terragrunt/cli/commands"
awsproviderpatch "github.com/gruntwork-io/terragrunt/cli/commands/aws-provider-patch"
graphdependencies "github.com/gruntwork-io/terragrunt/cli/commands/graph-dependencies"
"github.com/gruntwork-io/terragrunt/cli/commands/hclfmt"
Expand All @@ -23,6 +24,7 @@ func NewCommand(opts *options.TerragruntOptions) *cli.Command {
Name: CommandName,
Usage: "Run a terraform command against a 'stack' by running the specified command in each subfolder.",
Description: "The command will recursively find terragrunt modules in the current directory tree and run the terraform command in dependency order (unless the command is destroy, in which case the command is run in reverse dependency order).",
Flags: commands.NewGlobalFlags(opts),
Subcommands: subCommands(opts).SkipRunning(),
Action: action(opts),
}
Expand Down
5 changes: 3 additions & 2 deletions cli/commands/terraform/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
)

const (
CommandName = "terraform"
CommandName = ""
CommandHelpName = "*"
)

var (
Expand All @@ -18,7 +19,7 @@ var (
func NewCommand(opts *options.TerragruntOptions) *cli.Command {
return &cli.Command{
Name: CommandName,
HelpName: "*",
HelpName: CommandHelpName,
Usage: "Terragrunt forwards all other commands directly to Terraform",
Action: action(opts),
}
Expand Down
2 changes: 1 addition & 1 deletion cli/deprecated_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func replaceDeprecatedCommandFunc(terragruntCommandName, terraformCommandName st
deprecatedCommandName,
)

err := command.Run(ctx, args...)
err := command.Run(ctx, args)
return err
}
}
Expand Down
24 changes: 24 additions & 0 deletions docs/_docs/01_getting-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ If you want the latest version, the recommended installation option is to [down

* **FreeBSD**: You can install Terragrunt on FreeBSD using [Pkg](https://www.freebsd.org/cgi/man.cgi?pkg(7)): `pkg install terragrunt`.

### Enable tab completion

If you use either Bash or Zsh, you can enable tab completion for Terragrunt commands. To enable autocomplete, first ensure that a config file exists for your chosen shell.


For Bash shell.
``` shell
touch ~/.bashrc
```

For Zsh shell.
``` shell
touch ~/.zshrc
```

Then install the autocomplete package.

``` shell
terragrunt --install-autocomplete
```

Once the autocomplete support is installed, you will need to restart your shell.


### Terragrunt GitHub Action

Terragrunt is also available as a GitHub Action. Instructions on how to use it can be found at [https://github.com/gruntwork-io/terragrunt-action](https://github.com/gruntwork-io/terragrunt-action).
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ require (
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d h1:PinQItctnaL2LtkaSM678+ZLLy5TajwOeXzWvYC7tII=
github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
Expand Down
106 changes: 101 additions & 5 deletions pkg/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"strings"

"github.com/gruntwork-io/go-commons/errors"
"github.com/urfave/cli/v2"
)

Expand Down Expand Up @@ -31,6 +32,8 @@ type App struct {
// The difference between `Before` is that `CommonBefore` runs only once for the target command, while `Before` is different for each command and is performed by each command.
// Useful when some steps need to to performed for all commands without exception, when all flags are parsed and the context contains the target command.
CommonBefore ActionFunc
// The function to call when checking for command completions
Complete CompleteFunc
// An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run
Before ActionFunc
Expand All @@ -43,13 +46,36 @@ type App struct {
DefaultCommand *Command
// OsExiter is the function used when the app exits. If not set defaults to os.Exit.
OsExiter func(code int)

// Autocomplete enables or disables subcommand auto-completion support.
// This is enabled by default when NewApp is called. Otherwise, this
// must enabled explicitly.
//
// Autocomplete requires the "Name" option to be set on CLI. This name
// should be set exactly to the binary name that is autocompleted.
Autocomplete bool

// AutocompleteInstallFlag and AutocompleteUninstallFlag are the global flag
// names for installing and uninstalling the autocompletion handlers
// for the user's shell. The flag should omit the hyphen(s) in front of
// the value. Both single and double hyphens will automatically be supported
// for the flag name. These default to `autocomplete-install` and
// `autocomplete-uninstall` respectively.
AutocompleteInstallFlag string
AutocompleteUninstallFlag string

// Autocompletion is supported via the github.com/posener/complete
// library. This library supports bash, zsh and fish. To add support
// for other shells, please see that library.
AutocompleteInstaller AutocompleteInstaller
}

// NewApp returns app new App instance.
func NewApp() *App {
return &App{
App: cli.NewApp(),
OsExiter: os.Exit,
App: cli.NewApp(),
OsExiter: os.Exit,
Autocomplete: true,
}
}

Expand Down Expand Up @@ -78,11 +104,27 @@ func (app *App) Run(arguments []string) error {
app.Authors = []*cli.Author{{Name: app.Author}}

app.App.Action = func(parentCtx *cli.Context) error {
args := parentCtx.Args().Slice()
cmd := app.newRootCommand()

args := Args(parentCtx.Args().Slice())
ctx := newContext(parentCtx.Context, app)

cmd := ctx.App.newRootCommand()
err := cmd.Run(ctx, args...)
if app.Autocomplete {
if err := app.setupAutocomplete(args); err != nil {
return app.handleExitCoder(err)
}

if compLine := os.Getenv(envCompleteLine); compLine != "" {
args = strings.Fields(compLine)
if args[0] == app.Name {
args = args[1:]
}

ctx.shellComplete = true
}
}

err := cmd.Run(ctx, args.Normalize(SingleDashFlag))
return err
}

Expand Down Expand Up @@ -113,10 +155,64 @@ func (app *App) newRootCommand() *Command {
Description: app.Description,
Flags: app.Flags,
Subcommands: app.Commands,
Complete: app.Complete,
IsRoot: true,
}
}

func (app *App) setupAutocomplete(arguments []string) error {
var (
isAutocompleteInstall bool
isAutocompleteUninstall bool
)

if app.AutocompleteInstallFlag == "" {
app.AutocompleteInstallFlag = defaultAutocompleteInstallFlag
}

if app.AutocompleteUninstallFlag == "" {
app.AutocompleteUninstallFlag = defaultAutocompleteUninstallFlag
}

if app.AutocompleteInstaller == nil {
app.AutocompleteInstaller = &autocompleteInstaller{}
}

for _, arg := range arguments {
switch {
// Check for autocomplete flags
case arg == "-"+app.AutocompleteInstallFlag || arg == "--"+app.AutocompleteInstallFlag:
isAutocompleteInstall = true

case arg == "-"+app.AutocompleteUninstallFlag || arg == "--"+app.AutocompleteUninstallFlag:
isAutocompleteUninstall = true
}
}

// Autocomplete requires the "Name" to be set so that we know what command to setup the autocomplete on.
if app.Name == "" {
return errors.Errorf("internal error: App.Name must be specified for autocomplete to work")
}

// If both install and uninstall flags are specified, then error
if isAutocompleteInstall && isAutocompleteUninstall {
return errors.Errorf("either the autocomplete install or uninstall flag may be specified, but not both")
}

// If the install flag is specified, perform the install or uninstall and exit
if isAutocompleteInstall {
err := app.AutocompleteInstaller.Install(app.Name)
return NewExitError(err, 0)
}

if isAutocompleteUninstall {
err := app.AutocompleteInstaller.Uninstall(app.Name)
return NewExitError(err, 0)
}

return nil
}

func (app *App) handleExitCoder(err error) error {
return handleExitCoder(err, app.OsExiter)
}
Loading