Repository: gruntwork-io/terragrunt

Issue:
add shell completion
add shell completion for bash, zsh, and probably other shells as well.

Hints from issue comments:
@mavogel: If switching to `spf13/cobra` as CLI is an option I'd be easy to provide the completion out of the box: `rootCmd.GenBashCompletion(os.Stdout)`

@autero1 is this an option or too big for a change? On the other hand we could also quickly rebuild the structure of the `terragrunt` commands and generate the completion. The downside is that is has to be kept in sync with the code.@yorinasub17: `urfave/cli` supports [bash completion](https://github.com/urfave/cli#bash-completion), but I think we will need something custom regardless of the underlying CLI library for this to work intuitively because of the way `terragrunt` works. E.g if you take a look at [how the commands are implemented](https://github.com/gruntwork-io/terragrunt/blob/master/cli/cli_app.go#L163), we aren't defining them as direct commands in the cli struct. This is because we can't define all the available options for most of the commands, since they are passed through to `terraform`.

Additionally, we need to somehow merge the `terragrunt` completion with the `terraform` completions. Otherwise, you get completion for `terragrunt` specific commands like `plan-all`, but not for `terraform` commands that are not explicitly defined in `terragrunt`.@noahingh: I started [the project](https://github.com/hanjunlee/terragrunt-oh-my-zsh-plugin) `oh-my-zsh` plugin of `terragrunt`. I have complete about zsh completion for `terraform` subcommands, options and resources (especially when you type `-target` option). But  I haven't complete all the options of `terragrunt` yet (only complete subcommand). 

I know It is not solution for this issue, just I hope it can help to someone who are using `oh-my-zsh`. Thanks.@GergelyKalmar: Until this is implemented one can just set up terragrunt to at least complete terraform commands in bash by adding this line to ``.bashrc`` or ``.bash_aliases``:

``complete -C ~/.bin/terraform terragrunt``

Of course with the appropriate path to the terraform binary.

It also works fine with aliases:

```
alias tg='terragrunt'
complete -C ~/.bin/terraform tg
```@sobi3ch: This didn't work for me. I've instead use this brute-force solution
In `.bashrc` or `.bash_aliases` add this:
```
complete -W "$(terragrunt | grep -A123 "COMMANDS" | head -n-7 | grep '^   ' | awk '{ print $1 }' | grep -v '*' | xargs)" terragrunt tg
```@lev-ok: Resolved in [v0.53.4](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.53.4) release.

How to enable tab completion [docs](https://terragrunt.gruntwork.io/docs/getting-started/install/#enable-tab-completion).

@so-jelly: on zsh for both terragrunt and terraform completion

`complete -o nospace -C /opt/homebrew/bin/terragrunt -C /opt/homebrew/bin/terraform terragrunt`

eg:

```shell
alias tg=terragrunt
tg <tab>
❯ tg
apply                 env                   graph-dependencies    logout                providers             scaffold              test                  workspace           
aws-provider-patch    fmt                   hclfmt                metadata              push                  show                  untaint                                   
catalog               force-unlock          import                output                refresh               state                 validate                                  
console               get                   init                  output-module-groups  render-json           taint                 validate-inputs                           
destroy               graph                 login                 plan                  run-all               terragrunt-info       version   
❯ tg run-all <tab>
apply               env                 graph               init                output              refresh             taint               validate                              
aws-provider-patch  fmt                 graph-dependencies  login               plan                render-json         terragrunt-info     validate-inputs                       
console             force-unlock        hclfmt              logout              providers           show                test                version                               
destroy             get                 import              metadata            push                state               untaint             workspace                             
```

@antspy: Is there a way to specify the .bashrc file to use when calling `terragrunt --install-autocomplete`? 

I would like to add this to my dockerfile, but the final user of the OS is different than the root user who is installing everything, so I would like to specify an output .bashrc file explicitly. I guess I can just add `complete -C /usr/bin/terragrunt terragrunt` myself to the bashrc file, but it would be nice to have a solution that doesn't drift from the proper one. @eggbean: @antspy not tested this, but I reckon it could work.

    bash --rcfile /home/rcfile -c "terragrunt --install-autocomplete"@antspy: Thank you, I can try! :) @foreignmeloman: To anyone coming here to find a better solution than `complete -C ~/.bin/terraform terragrunt` here is my GNU bash completion script which also completes flags and even suggests file/directory completion for the flags that expect it. Additionally it allows for a functional aliases like `tg` with same set of completions ([Solution above](https://github.com/gruntwork-io/terragrunt/issues/689#issuecomment-629161874) didn't work for me).

The script was written using GNU utils, so mac users might not have such a good experience. This is still work in progress, so please report any related issues in my repo.

To install just place it into your `~/.local/share/bash_completion.d/` or source it in your RC file.

UPDATE: Now the script also suggests completion for the tofu/terraform shortcuts as well.

https://github.com/foreignmeloman/dotfiles/blob/master/pyinfra/files/bash_completion/terragrunt.sh

Solution patch:
diff --git a/cli/app.go b/cli/app.go
index 8c5b0f2f..105eb555 100644
--- a/cli/app.go
+++ b/cli/app.go
@@ -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)...)
diff --git a/cli/commands/flags.go b/cli/commands/flags.go
index 144b484a..a87f7708 100644
--- a/cli/commands/flags.go
+++ b/cli/commands/flags.go
@@ -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 {
diff --git a/cli/commands/run-all/command.go b/cli/commands/run-all/command.go
index 12c990f8..3b2efaed 100644
--- a/cli/commands/run-all/command.go
+++ b/cli/commands/run-all/command.go
@@ -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"
@@ -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),
 	}
diff --git a/cli/commands/terraform/command.go b/cli/commands/terraform/command.go
index c5f3be49..bea60b41 100644
--- a/cli/commands/terraform/command.go
+++ b/cli/commands/terraform/command.go
@@ -8,7 +8,8 @@ import (
 )
 
 const (
-	CommandName = "terraform"
+	CommandName     = ""
+	CommandHelpName = "*"
 )
 
 var (
@@ -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),
 	}
diff --git a/cli/deprecated_commands.go b/cli/deprecated_commands.go
index 55b15c10..65526f9b 100644
--- a/cli/deprecated_commands.go
+++ b/cli/deprecated_commands.go
@@ -55,7 +55,7 @@ func replaceDeprecatedCommandFunc(terragruntCommandName, terraformCommandName st
 				deprecatedCommandName,
 			)
 
-			err := command.Run(ctx, args...)
+			err := command.Run(ctx, args)
 			return err
 		}
 	}
diff --git a/docs/_docs/01_getting-started/install.md b/docs/_docs/01_getting-started/install.md
index 0c0ec219..87fe7cd8 100644
--- a/docs/_docs/01_getting-started/install.md
+++ b/docs/_docs/01_getting-started/install.md
@@ -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).
diff --git a/go.mod b/go.mod
index 22cbb8ed..3f39132e 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index f3432932..97bf6507 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/pkg/cli/app.go b/pkg/cli/app.go
index d29432a8..f56ba213 100644
--- a/pkg/cli/app.go
+++ b/pkg/cli/app.go
@@ -4,6 +4,7 @@ import (
 	"os"
 	"strings"
 
+	"github.com/gruntwork-io/go-commons/errors"
 	"github.com/urfave/cli/v2"
 )
 
@@ -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
@@ -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,
 	}
 }
 
@@ -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
 	}
 
@@ -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)
 }
diff --git a/pkg/cli/args.go b/pkg/cli/args.go
index 2bdffc90..227440ae 100644
--- a/pkg/cli/args.go
+++ b/pkg/cli/args.go
@@ -13,8 +13,8 @@ const (
 )
 
 var (
-	singleDashRegexp = regexp.MustCompile(`^-[^-]`)
-	doubleDashRegexp = regexp.MustCompile(`^--[^-]`)
+	singleDashRegexp = regexp.MustCompile(`^-([^-]|$)`)
+	doubleDashRegexp = regexp.MustCompile(`^--([^-]|$)`)
 )
 
 type NormalizeActsType byte
@@ -23,23 +23,28 @@ type NormalizeActsType byte
 type Args []string
 
 // Get returns the nth argument, or else a blank string
-func (args *Args) Get(n int) string {
-	if len(*args) > n {
-		return (*args)[n]
+func (args Args) Get(n int) string {
+	if len(args) > 0 && len(args) > n {
+		return (args)[n]
 	}
 	return ""
 }
 
-// First returns the first argument, or else a blank string
-func (args *Args) First() string {
+// First returns the first argument or a blank string
+func (args Args) First() string {
 	return args.Get(0)
 }
 
+// Last returns the last argument or a blank string
+func (args Args) Last() string {
+	return args.Get(len(args) - 1)
+}
+
 // Tail returns the rest of the arguments (not the first one)
 // or else an empty string slice
-func (args *Args) Tail() []string {
+func (args Args) Tail() []string {
 	if args.Len() >= minTailLen {
-		tail := []string((*args)[1:])
+		tail := []string((args)[1:])
 		ret := make([]string, len(tail))
 		copy(ret, tail)
 		return ret
@@ -48,19 +53,19 @@ func (args *Args) Tail() []string {
 }
 
 // Len returns the length of the wrapped slice
-func (args *Args) Len() int {
-	return len(*args)
+func (args Args) Len() int {
+	return len(args)
 }
 
 // Present checks if there are any arguments present
-func (args *Args) Present() bool {
+func (args Args) Present() bool {
 	return args.Len() != 0
 }
 
 // Slice returns a copy of the internal slice
-func (args *Args) Slice() []string {
-	ret := make([]string, len(*args))
-	copy(ret, *args)
+func (args Args) Slice() []string {
+	ret := make([]string, len(args))
+	copy(ret, args)
 	return ret
 }
 
@@ -69,7 +74,7 @@ func (args *Args) Slice() []string {
 //
 //	`SingleDashFlag` - converts all arguments containing double dashes to single dashes
 //	`DoubleDashFlag` - converts all arguments containing signle dashes to double dashes
-func (args *Args) Normalize(acts ...NormalizeActsType) *Args {
+func (args Args) Normalize(acts ...NormalizeActsType) Args {
 	strArgs := make(Args, 0, len(args.Slice()))
 
 	for _, arg := range args.Slice() {
@@ -89,11 +94,11 @@ func (args *Args) Normalize(acts ...NormalizeActsType) *Args {
 		strArgs = append(strArgs, arg)
 	}
 
-	return &strArgs
+	return strArgs
 }
 
 // CommandName returns the first value if it starts without a dash `-`, otherwise that means the args do not consist any command and an empty string is returned.
-func (args *Args) CommandName() string {
+func (args Args) CommandName() string {
 	name := args.First()
 
 	if !strings.HasPrefix(name, "-") {
diff --git a/pkg/cli/autocomplete.go b/pkg/cli/autocomplete.go
new file mode 100644
index 00000000..128126bc
--- /dev/null
+++ b/pkg/cli/autocomplete.go
@@ -0,0 +1,138 @@
+package cli
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"unicode/utf8"
+
+	"github.com/gruntwork-io/go-commons/errors"
+	"github.com/posener/complete/cmd/install"
+)
+
+// defaultAutocompleteInstallFlag and defaultAutocompleteUninstallFlag are the
+// default values for the autocomplete install and uninstall flags.
+const (
+	defaultAutocompleteInstallFlag   = "install-autocomplete"
+	defaultAutocompleteUninstallFlag = "uninstall-autocomplete"
+
+	envCompleteLine = "COMP_LINE"
+
+	maxDashesInFlag = 2
+)
+
+var DefaultComplete = defaultComplete
+
+// AutocompleteInstaller is an interface to be implemented to perform the
+// autocomplete installation and uninstallation with a CLI.
+//
+// This interface is not exported because it only exists for unit tests
+// to be able to test that the installation is called properly.
+type AutocompleteInstaller interface {
+	Install(string) error
+	Uninstall(string) error
+}
+
+// autocompleteInstaller uses the install package to do the
+// install/uninstall.
+type autocompleteInstaller struct{}
+
+func (i *autocompleteInstaller) Install(cmd string) error {
+	if err := install.Install(cmd); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (i *autocompleteInstaller) Uninstall(cmd string) error {
+	if err := install.Uninstall(cmd); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+// ShowCompletions prints the lists of commands within a given context
+func ShowCompletions(ctx *Context) error {
+	if cmd := ctx.Command; cmd != nil && cmd.Complete != nil {
+		return cmd.Complete(ctx)
+	}
+
+	return DefaultComplete(ctx)
+}
+
+func defaultComplete(ctx *Context) error {
+	arg := ctx.Args().Last()
+
+	if strings.HasPrefix(arg, "-") {
+		if cmd := ctx.Command; cmd != nil {
+			return printFlagSuggestions(arg, cmd.Flags, ctx.App.Writer)
+		}
+
+		return printFlagSuggestions(arg, ctx.App.Flags, ctx.App.Writer)
+	}
+
+	return printCommandSuggestions(arg, ctx.Command.Subcommands, ctx.App.Writer)
+}
+
+func printCommandSuggestions(arg string, commands []*Command, writer io.Writer) error {
+	for _, command := range commands {
+		if command.Hidden {
+			continue
+		}
+
+		for _, name := range command.Names() {
+			if name != "" && (arg == "" || strings.HasPrefix(name, arg)) {
+				fmt.Fprintln(writer, name)
+			}
+		}
+	}
+
+	return nil
+}
+
+func printFlagSuggestions(arg string, flags []Flag, writer io.Writer) error {
+	cur := strings.TrimPrefix(arg, "-")
+
+	for _, flag := range flags {
+		for _, name := range flag.Names() {
+			name = strings.TrimSpace(name)
+			// this will get total count utf8 letters in flag name
+			count := utf8.RuneCountInString(name)
+			if count > maxDashesInFlag {
+				count = maxDashesInFlag // reuse this count to generate single - or -- in flag completion
+			}
+			// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
+			// skip flag completion for short flags example -v or -x
+			if strings.HasPrefix(arg, "--") && count == 1 {
+				continue
+			}
+			// match if last argument matches this flag and it is not repeated
+			if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) {
+				flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
+				fmt.Fprintln(writer, flagCompletion)
+			}
+		}
+	}
+
+	return nil
+}
+
+func cliArgContains(flagName string) bool {
+	for _, name := range strings.Split(flagName, ",") {
+		name = strings.TrimSpace(name)
+		count := utf8.RuneCountInString(name)
+		if count > maxDashesInFlag {
+			count = maxDashesInFlag
+		}
+		flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
+		for _, a := range os.Args {
+			if a == flag {
+				return true
+			}
+		}
+	}
+	return false
+}
diff --git a/pkg/cli/command.go b/pkg/cli/command.go
index 734b2f8b..d8ab0ab4 100644
--- a/pkg/cli/command.go
+++ b/pkg/cli/command.go
@@ -35,6 +35,8 @@ type Command struct {
 	SkipFlagParsing bool
 	// Boolean to disable the parsing command, but it will still be shown in the help.
 	SkipRunning bool
+	// 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
@@ -52,7 +54,7 @@ func (cmd *Command) Names() []string {
 // HasName returns true if Command.Name matches given name
 func (cmd *Command) HasName(name string) bool {
 	for _, n := range cmd.Names() {
-		if n == name {
+		if n == name && name != "" {
 			return true
 		}
 	}
@@ -86,14 +88,28 @@ func (cmd Command) VisibleSubcommands() []*cli.Command {
 
 // Run parses the given args for the presence of flags as well as subcommands.
 // If this is the final command, starts its execution.
-func (cmd *Command) Run(ctx *Context, args ...string) (err error) {
-	args, err = cmd.parseFlags(args)
+func (cmd *Command) Run(ctx *Context, args Args) (err error) {
+	args, err = cmd.parseFlags(args.Slice())
 	if err != nil {
 		return err
 	}
 
 	ctx = ctx.Clone(cmd, args)
 
+	subCmdName := ctx.Args().CommandName()
+	subCmdArgs := ctx.Args().Tail()
+	subCmd := cmd.Subcommand(subCmdName)
+
+	if ctx.shellComplete {
+		if cmd := ctx.Command.Subcommand(args.CommandName()); cmd == nil {
+			return ShowCompletions(ctx)
+		}
+
+		if subCmd != nil {
+			return subCmd.Run(ctx, subCmdArgs)
+		}
+	}
+
 	if err := cmd.Flags.RunActions(ctx); err != nil {
 		return ctx.App.handleExitCoder(err)
 	}
@@ -111,15 +127,12 @@ func (cmd *Command) Run(ctx *Context, args ...string) (err error) {
 		}
 	}
 
-	subCmdName := ctx.Args().CommandName()
-	if subCmd := cmd.Subcommand(subCmdName); subCmd != nil && !subCmd.SkipRunning {
-		args := ctx.Args().Tail()
-		err = subCmd.Run(ctx, args...)
-		return err
+	if subCmd != nil && !subCmd.SkipRunning {
+		return subCmd.Run(ctx, subCmdArgs)
 	}
 
 	if cmd.IsRoot && ctx.App.DefaultCommand != nil {
-		err = ctx.App.DefaultCommand.Run(ctx, args...)
+		err = ctx.App.DefaultCommand.Run(ctx, args)
 		return err
 	}
 
diff --git a/pkg/cli/context.go b/pkg/cli/context.go
index 80bfc911..c3ec6246 100644
--- a/pkg/cli/context.go
+++ b/pkg/cli/context.go
@@ -8,8 +8,9 @@ import (
 type Context struct {
 	context.Context
 	*App
-	Command *Command
-	args    *Args
+	Command       *Command
+	args          Args
+	shellComplete bool
 }
 
 func newContext(parentCtx context.Context, app *App) *Context {
@@ -21,10 +22,11 @@ func newContext(parentCtx context.Context, app *App) *Context {
 
 func (ctx *Context) Clone(command *Command, args Args) *Context {
 	return &Context{
-		Context: ctx.Context,
-		App:     ctx.App,
-		Command: command,
-		args:    &args,
+		Context:       ctx.Context,
+		App:           ctx.App,
+		shellComplete: ctx.shellComplete,
+		Command:       command,
+		args:          args,
 	}
 }
 
@@ -38,6 +40,6 @@ func (ctx *Context) Value(key any) any {
 }
 
 // Args returns the command line arguments associated with the context.
-func (ctx *Context) Args() *Args {
+func (ctx *Context) Args() Args {
 	return ctx.args
 }
diff --git a/pkg/cli/funcs.go b/pkg/cli/funcs.go
index 82a2c85b..5986a852 100644
--- a/pkg/cli/funcs.go
+++ b/pkg/cli/funcs.go
@@ -1,5 +1,8 @@
 package cli
 
+// CompleteFunc is an action to execute when the shell completion flag is set
+type CompleteFunc func(*Context) error
+
 // ActionFunc is the action to execute when no commands/subcommands are specified.
 type ActionFunc func(*Context) error
 



Test patch:
diff --git a/cli/app_test.go b/cli/app_test.go
index 1da440ff..6135c058 100644
--- a/cli/app_test.go
+++ b/cli/app_test.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"strings"
 	"testing"
 
 	"github.com/gruntwork-io/go-commons/errors"
@@ -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...)
@@ -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"))
+	}
+}
diff --git a/pkg/cli/command_test.go b/pkg/cli/command_test.go
index 11bc7480..025f62e9 100644
--- a/pkg/cli/command_test.go
+++ b/pkg/cli/command_test.go
@@ -182,7 +182,7 @@ func TestCommandRun(t *testing.T) {
 			app := &App{App: &cli.App{Writer: io.Discard}}
 			ctx := newContext(context.Background(), app)
 
-			err := testCase.command.Run(ctx, testCase.args...)
+			err := testCase.command.Run(ctx, testCase.args)
 			if testCase.expectedErr != nil {
 				require.EqualError(t, err, testCase.expectedErr.Error(), testCase)
 			} else {