Repository: gruntwork-io/terragrunt (language: go)

Issue:
Clean up help for shortcuts.
Blocked by #3723

We need to clean up the help for shortcuts:

```bash
$ terragrunt plan --help
Usage: tofu [global options] plan [options]

  Generates a speculative execution plan, showing what actions OpenTofu
  would take to apply the current configuration. This command will not
  actually perform the planned actions.

  You can optionally save the plan to a file, which you can then pass to
  the "apply" command to perform exactly the actions described in the plan.

Plan Customization Options:

  The following options customize how OpenTofu will produce its plan. You
  can also use these options when you run "tofu apply" without passing
  it a saved plan, in order to plan and apply in a single command.

  -destroy            Select the "destroy" planning mode, which creates a plan
                      to destroy all objects currently managed by this
                      OpenTofu configuration instead of the usual behavior.

  -refresh-only       Select the "refresh only" planning mode, which checks
                      whether remote objects still match the outcome of the
                      most recent OpenTofu apply but does not propose any
                      actions to undo any changes made outside of OpenTofu.

  -refresh=false      Skip checking for external changes to remote objects
                      while creating the plan. This can potentially make
                      planning faster, but at the expense of possibly planning
                      against a stale record of the remote system state.

  -replace=resource   Force replacement of a particular resource instance using
                      its resource address. If the plan would've normally
                      produced an update or no-op action for this instance,
                      OpenTofu will plan to replace it instead. You can use
                      this option multiple times to replace more than one object.

  -target=resource    Limit the planning operation to only the given module,
                      resource, or resource instance and all of its
                      dependencies. You can use this option multiple times to
                      include more than one object. This is for exceptional
                      use only.

  -var 'foo=bar'      Set a value for one of the input variables in the root
                      module of the configuration. Use this option more than
                      once to set more than one variable.

  -var-file=filename  Load variable values from the given file, in addition
                      to the default files terraform.tfvars and *.auto.tfvars.
                      Use this option more than once to include more than one
                      variables file.

Other Options:

  -compact-warnings          If OpenTofu produces any warnings that are not
                             accompanied by errors, shows them in a more compact
                             form that includes only the summary messages.

  -detailed-exitcode         Return detailed exit codes when the command exits.
                             This will change the meaning of exit codes to:
                             0 - Succeeded, diff is empty (no changes)
                             1 - Errored
                             2 - Succeeded, there is a diff

  -generate-config-out=path  (Experimental) If import blocks are present in
                             configuration, instructs OpenTofu to generate HCL
                             for any imported resources not already present. The
                             configuration is written to a new file at PATH,
                             which must not already exist. OpenTofu may still
                             attempt to write configuration if the plan errors.

  -input=true                Ask for input for variables if not directly set.

  -lock=false                Don't hold a state lock during the operation. This
                             is dangerous if others might concurrently run
                             commands against the same workspace.

  -lock-timeout=0s           Duration to retry a state lock.

  -no-color                  If specified, output won't contain any color.

  -concise                   Displays plan output in a concise way, skipping the
                                                         refreshing log lines.

  -out=path                  Write a plan file to the given path. This can be
                             used as input to the "apply" command.

  -parallelism=n             Limit the number of concurrent operations. Defaults
                             to 10.

  -state=statefile           A legacy option used for the local backend only.
                             See the local backend's documentation for more
                             information.

$  terragrunt run --help
Usage: terragrunt run [options] -- <tofu/terraform command>

   Run a command, passing arguments to an orchestrated tofu/terraform binary.

   This is the explicit, and most flexible form of running an IaC command with Terragrunt. Shortcuts can be found in "terragrunt --help" for common use-cases.

Examples:
   # Run a plan
   terragrunt run -- plan
   # Shortcut:
   # terragrunt plan

   # Run output with -json flag
   terragrunt run -- output -json
   # Shortcut:
   # terragrunt output -json

   # Run a plan against a Stack of configurations in the current directory
   terragrunt run --all -- plan

Options:
   --all                                       Run the specified OpenTofu/Terraform command on the stack of units in the current directory. [$TG_ALL]
   --auth-provider-cmd value                   Run the provided command and arguments to authenticate Terragrunt dynamically when necessary. [$TG_AUTH_PROVIDER_CMD]
   --backend-require-bootstrap                 When this flag is set Terragrunt will fail if the remote state bucket needs to be created. [$TG_BACKEND_REQUIRE_BOOTSTRAP]
   --config value                              The path to the Terragrunt config file. Default is terragrunt.hcl. [$TG_CONFIG]
   --dependency-fetch-output-from-state        The option fetches dependency output directly from the state file instead of using tofu/terraform output. [$TG_DEPENDENCY_FETCH_OUTPUT_FROM_STATE]
   --disable-bucket-update                     When this flag is set Terragrunt will not update the remote state bucket. [$TG_DISABLE_BUCKET_UPDATE]
   --disable-command-validation                When this flag is set, Terragrunt will not validate the tofu/terraform command. [$TG_DISABLE_COMMAND_VALIDATION]
   --download-dir value                        The path to download OpenTofu/Terraform modules into. Default is .terragrunt-cache in the working directory. [$TG_DOWNLOAD_DIR]
   --engine-cache-path value                   Cache path for Terragrunt engine files. [$TG_ENGINE_CACHE_PATH]
   --engine-log-level value                    Terragrunt engine log level. [$TG_ENGINE_LOG_LEVEL]
   --engine-skip-check                         Skip checksum check for Terragrunt engine files. [$TG_ENGINE_SKIP_CHECK]
   --experimental-engine                       Enable Terragrunt experimental engine. [$TG_EXPERIMENTAL_ENGINE]
   --feature value                             Set feature flags for the HCL code. [$TG_FEATURE]
   --graph                                     Run the specified OpenTofu/Terraform command following the Directed Acyclic Graph (DAG) of dependencies. [$TG_GRAPH]
   --iam-assume-role value                     Assume the specified IAM role before executing OpenTofu/Terraform. [$TG_IAM_ASSUME_ROLE]
   --iam-assume-role-duration value            Session duration for IAM Assume Role session. [$TG_IAM_ASSUME_ROLE_DURATION]
   --iam-assume-role-session-name value        Name for the IAM Assumed Role session. [$TG_IAM_ASSUME_ROLE_SESSION_NAME]
   --iam-assume-role-web-identity-token value  For AssumeRoleWithWebIdentity, the WebIdentity token. [$TG_IAM_ASSUME_ROLE_WEB_IDENTITY_TOKEN]
   --inputs-debug                              Write debug.tfvars to working folder to help root-cause issues. [$TG_INPUTS_DEBUG]
   --no-auto-approve                           Don't automatically append '-auto-approve' to the underlying OpenTofu/Terraform commands run with 'run-all'. [$TG_NO_AUTO_APPROVE]
   --no-auto-init                              Don't automatically run 'terraform/tofu init' during other terragrunt commands. You must run 'terragrunt init' manually. [$TG_NO_AUTO_INIT]
   --no-auto-retry                             Don't automatically re-run command in case of transient errors. [$TG_NO_AUTO_RETRY]
   --no-destroy-dependencies-check             When this flag is set, Terragrunt will not check for dependent units when destroying. [$TG_NO_DESTROY_DEPENDENCIES_CHECK]
   --parallelism value                         Parallelism for --all commands. [$TG_PARALLELISM]
   --provider-cache                            Enables Terragrunt's provider caching. [$TG_PROVIDER_CACHE]
   --provider-cache-dir value                  The path to the Terragrunt provider cache directory. By default, 'terragrunt/providers' folder in the user cache directory. [$TG_PROVIDER_CACHE_DIR]
   --provider-cache-hostname value             The hostname of the Terragrunt Provider Cache server. By default, 'localhost'. [$TG_PROVIDER_CACHE_HOSTNAME]
   --provider-cache-port value                 The port of the Terragrunt Provider Cache server. By default, assigned automatically. [$TG_PROVIDER_CACHE_PORT]
   --provider-cache-registry-names value       The list of remote registries to cached by Terragrunt Provider Cache server. By default, 'registry.terraform.io', 'registry.opentofu.org'. [$TG_PROVIDER_CACHE_REGISTRY_NAMES]
   --provider-cache-token value                The token for authentication to the Terragrunt Provider Cache server. By default, assigned automatically. [$TG_PROVIDER_CACHE_TOKEN]
   --queue-exclude-dir value                   Unix-style glob of directories to exclude from the queue of Units to run. [$TG_QUEUE_EXCLUDE_DIR]
   --queue-exclude-external                    Ignore external dependencies for --all commands. [$TG_QUEUE_EXCLUDE_EXTERNAL]
   --queue-excludes-file value                 Path to a file with a list of directories that need to be excluded when running *-all commands. [$TG_QUEUE_EXCLUDES_FILE]
   --queue-ignore-dag-order                    Ignore DAG order for --all commands. [$TG_QUEUE_IGNORE_DAG_ORDER]
   --queue-ignore-errors                       Continue processing Units even if a dependency fails. [$TG_QUEUE_IGNORE_ERRORS]
   --queue-include-dir value                   Unix-style glob of directories to include from the queue of Units to run. [$TG_QUEUE_INCLUDE_DIR]
   --queue-include-external                    Include external dependencies for --all commands without asking. [$TG_QUEUE_INCLUDE_EXTERNAL]
   --queue-include-units-reading value         If flag is set, 'run-all' will only run the command against Terragrunt units that read the specified file via an HCL function or include. [$TG_QUEUE_INCLUDE_UNITS_READING]
   --queue-strict-include                      If flag is set, only modules under the directories passed in with '--include-dir' will be included. [$TG_QUEUE_STRICT_INCLUDE]
   --source value                              Download OpenTofu/Terraform configurations from the specified source into a temporary folder, and run Terraform in that temporary folder. [$TG_SOURCE]
   --source-map value                          Replace any source URL (including the source URL of a config pulled in with dependency blocks) that has root source with dest. [$TG_SOURCE_MAP]
   --source-update                             Delete the contents of the temporary folder to clear out any old, cached source code before downloading new source code into it. [$TG_SOURCE_UPDATE]
   --tf-forward-stdout                         If specified, the output of OpenTofu/Terraform commands will be printed as is, without being integrated into the Terragrunt log. [$TG_TF_FORWARD_STDOUT]
   --tf-path value                             Path to the OpenTofu/Terraform binary. Default is tofu (on PATH). [$TG_TF_PATH]
   --units-that-include value                  If flag is set, 'run-all' will only run the command against Terragrunt modules that include the specified file. [$TG_UNITS_THAT_INCLUDE]
   --use-partial-parse-config-cache            Enables caching of includes during partial parsing operations. Will also be used for the --iam-role option if provided. [$TG_USE_PARTIAL_PARSE_CONFIG_CACHE]

Global Options:
   --experiment value         Enables specific experiments. For a list of available experiments, see https://terragrunt.gruntwork.io/docs/reference/experiment-mode . [$TG_EXPERIMENT]
   --experiment-mode          Enables experiment mode for Terragrunt. For more information, see https://terragrunt.gruntwork.io/docs/reference/experiment-mode . [$TG_EXPERIMENT_MODE]
   --log-custom-format value  Set the custom log formatting. [$TG_LOG_CUSTOM_FORMAT]
   --log-disable              Disable logging. [$TG_LOG_DISABLE]
   --log-format value         Set the log format. [$TG_LOG_FORMAT]
   --log-level value          Sets the logging level for Terragrunt. Supported levels: stderr, stdout, error, warn, info, debug, trace. (default: info) [$TG_LOG_LEVEL]
   --log-show-abs-paths       Show absolute paths in logs. (default: false) [$TG_LOG_SHOW_ABS_PATHS]
   --no-color                 Disable color output. [$TG_NO_COLOR]
   --non-interactive          Assume "yes" for all prompts. (default: false) [$TG_NON_INTERACTIVE]
   --strict-control value     Enables specific strict controls. For a list of available controls, run 'terragrunt info strict'. [$TG_STRICT_CONTROL]
   --strict-mode              Enables strict mode for Terragrunt. For more information, run 'terragrunt info strict'. [$TG_STRICT_MODE]
   --working-dir value        The path to the directory of Terragrunt configurations. Default is current directory. [$TG_WORKING_DIR]
   --help, -h                 Show help.
   --version, -v              Show terragrunt version.

$ terragrunt plan --provider-cache --experiment cli-redesign
10:16:54.578 INFO   Terragrunt Cache server is listening on 127.0.0.1:51049
10:16:54.578 INFO   Start Terragrunt Cache server
10:16:54.878 INFO   Caching terraform providers for .
10:16:54.917 INFO   tofu: Initializing the backend...
10:16:54.918 INFO   tofu: Initializing provider plugins...
10:16:54.918 INFO   tofu: OpenTofu has been successfully initialized!
10:16:54.918 INFO   tofu:
10:16:54.918 INFO   tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see
10:16:54.918 INFO   tofu: any changes that are required for your infrastructure. All OpenTofu commands
10:16:54.918 INFO   tofu: should now work.
10:16:54.918 INFO   tofu: If you ever set or change modules or backend configuration for OpenTofu,
10:16:54.918 INFO   tofu: rerun this command to reinitialize your working directory. If you forget, other
10:16:54.918 INFO   tofu: commands will detect it and remind you to do so if necessary.
10:16:54.940 INFO   tofu: Initializing the backend...
10:16:54.940 INFO   tofu: Initializing provider plugins...
10:16:54.940 INFO   tofu: OpenTofu has been successfully initialized!
10:16:54.940 INFO   tofu:
10:16:54.940 INFO   tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see
10:16:54.940 INFO   tofu: any changes that are required for your infrastructure. All OpenTofu commands
10:16:54.940 INFO   tofu: should now work.
10:16:54.940 INFO   tofu: If you ever set or change modules or backend configuration for OpenTofu,
10:16:54.940 INFO   tofu: rerun this command to reinitialize your working directory. If you forget, other
10:16:54.940 INFO   tofu: commands will detect it and remind you to do so if necessary.
10:16:54.960 STDOUT tofu: No changes. Your infrastructure matches the configuration.
10:16:54.960 STDOUT tofu: OpenTofu has compared your real infrastructure against your configuration and
10:16:54.960 STDOUT tofu: found no differences, so no changes are needed.
10:16:54.960 INFO   Shutting down Terragrunt Cache server...
10:16:54.961 INFO   Terragrunt Cache server stopped
```

You only know about those `run` flags if you are using the `run` command, but they work in the shortcuts like `plan` as well. We should add some preamble, like in the RFC to make it clear that users have access to all the same capabilities that they do when using the `run` command.

Please don't address this now, as it doesn't need to block merging the PR. Adding an issue to address later.

_Originally posted by @yhakbar in https://github.com/gruntwork-io/terragrunt/issues/3723#issuecomment-2631307453_
            


Discussion / hints:
![Image](https://github.com/user-attachments/assets/efbd4d55-0d37-4312-92cb-28872e6b9208)

We also display `Subcommands`, I think it's a good advantage. Should we rename it to "OpenTofu commands"?
![Image](https://github.com/user-attachments/assets/2a1b40ab-d323-4611-a51d-73060945a3fe)


@yhakbar, In the RFC we have the following:

![Image](https://github.com/user-attachments/assets/a5b1273c-d1b7-47d9-b9f8-cf81efcff96c)

I can prepend a header and it will end up looking like this:

``` terragrunt plan --help
Usage: terragrunt plan [options]

  This is a shortcut for the command `terragrunt run`.
  
  It wraps the `plan` command of the binary defined by `tf-path`.

Usage: tofu [global options] plan [options]

  Generates a speculative execution plan, showing what actions OpenTofu
  would take to apply the current configuration. This command will not
  actually perform the planned actions.

  You can optionally save the plan to a file, which you can then pass to
  the "apply" command to perform exactly the actions described in the plan.

Plan Customization Options:

  The following options customize how OpenTofu will produce its plan. You
  can also use these options when you run "tofu apply" without passing
  it a saved plan, in order to plan and apply in a single command.

  -destroy            Select the "destroy" planning mode, which creates a plan
                      to destroy all objects currently managed by this
                      OpenTofu configuration instead of the usual behavior.

  -refresh-only       Select the "refresh only" planning mode, which checks
```

As you can see this is different from RFC. Are you OK with it?
As  the solution we can trim the first line from the tofu help, 

```
Usage: tofu [global options] plan [options]
```

and then add 

```
OpenTofu `plan` help:
```
@levkohimins let's do that for now. Trim the first line and prepend the message. 

It's awkward to see usage twice like that. This does lose information for users though, as they no longer are shown that apply can take an optional additional argument for the plan, for example.

Let's revisit later with a different design that makes it clearer what's going on. Maybe we indent the tofu help or render it with markdown, etc. to make it like ok different, but that will take time and experimentation we don't need to do right now.
Also, I don't know about showing subcommands for run.

If tofu releases a new command (or removes one), every version of Terragrunt released before then shows the wrong help, as there's a command it can run that isn't shown.

I would prefer that we just show tofu help.

Also for both run and the shortcuts, we need to make sure we test help when users don't have OpenTofu or Terraform installed. Users should still get valid help, just less information (maybe an indication that they should install OpenTofu to use run or a shortcut).

> Also, I don't know about showing subcommands for run.
> 
> If tofu releases a new command (or removes one), every version of Terragrunt released before then shows the wrong help, as there's a command it can run that isn't shown.
> 
> I would prefer that we just show tofu help.

@yhakbar I can hide them, but it still will not work for the new future tofu commands, since for correct command validation we need to explicitly define all subcommands.

`terragrunt run plan -wrong-flag`

Return tofu error

`terragrunt run -wrong-flag plan` or
`terragrunt run -wrong-flag -- plan` 

Return terragrunt error

---

[slack discussion](https://gruntwork-io.slack.com/archives/C06KU8JJU5V/p1738609948259149?thread_ts=1738592850.996259&cid=C06KU8JJU5V) 

I strongly recommend you not to go back when the TF command could be anything, any word:
1. We will not be able to parse the cli command accurately, and with different order of commands/flags the parsing will be incorrect. #3757 
2. A lot of work will have to be redone.

---

Users can still specify any TF command (even non-existent ones) after `--` (we can indicate this in the documentation)

```
terragrunt -- tf-command -tf-flag1 -tf-flag2
```

For now I won't hide this `Subcommands` in CLI help, the user should see what subcommand he can specify after `run`.


Fix patch:
diff --git a/cli/commands/commands.go b/cli/commands/commands.go
index 0157082fb4..ff38485fd5 100644
--- a/cli/commands/commands.go
+++ b/cli/commands/commands.go
@@ -29,7 +29,7 @@ import (
 
 // Command category names.
 const (
-	// MainCommandsCategoryName represents primary Terragrunt operations like run, run-all.
+	// MainCommandsCategoryName represents primary Terragrunt operations like run, exec.
 	MainCommandsCategoryName = "Main commands"
 	// CatalogCommandsCategoryName represents commands for managing Terragrunt catalogs.
 	CatalogCommandsCategoryName = "Catalog commands"
diff --git a/cli/commands/help/command.go b/cli/commands/help/command.go
index dcade9a03f..9eec9ea74c 100644
--- a/cli/commands/help/command.go
+++ b/cli/commands/help/command.go
@@ -38,7 +38,7 @@ func Action(ctx *cli.Context, opts *options.TerragruntOptions) error {
 		}
 	}
 
-	if args.CommandName() == "" {
+	if cmdName := args.CommandName(); cmdName == "" || cmds.Get(cmdName) == nil {
 		return cli.ShowAppHelp(ctx)
 	}
 
@@ -58,7 +58,7 @@ func Action(ctx *cli.Context, opts *options.TerragruntOptions) error {
 	}
 
 	if ctx.Command != nil {
-		return cli.NewExitError(cli.ShowCommandHelp(ctx), cli.ExitCodeGeneralError)
+		return cli.ShowCommandHelp(ctx)
 	}
 
 	return cli.NewExitError(errors.New(cli.InvalidCommandNameError(args.First())), cli.ExitCodeGeneralError)
diff --git a/cli/commands/run/command.go b/cli/commands/run/command.go
index 931f8c0c8d..bff4290a5c 100644
--- a/cli/commands/run/command.go
+++ b/cli/commands/run/command.go
@@ -67,15 +67,6 @@ func NewSubcommands(opts *options.TerragruntOptions) cli.Commands {
 	return subcommands
 }
 
-// ShowTFHelp prints TF help for the given `ctx.Command` command.
-func ShowTFHelp(opts *options.TerragruntOptions) cli.HelpFunc {
-	return func(ctx *cli.Context) error {
-		terraformHelpCmd := append([]string{tf.FlagNameHelpLong, ctx.Command.Name}, ctx.Args()...)
-
-		return tf.RunCommand(ctx, opts, terraformHelpCmd...)
-	}
-}
-
 func Action(opts *options.TerragruntOptions) cli.ActionFunc {
 	return func(ctx *cli.Context) error {
 		if opts.TerraformCommand == tf.CommandNameDestroy {
@@ -95,9 +86,13 @@ func validateCommand(opts *options.TerragruntOptions) error {
 		return nil
 	}
 
-	if strings.HasSuffix(opts.TerraformPath, options.TerraformDefaultPath) {
+	if isTerraformPath(opts) {
 		return WrongTerraformCommand(opts.TerraformCommand)
 	}
 
 	return WrongTofuCommand(opts.TerraformCommand)
 }
+
+func isTerraformPath(opts *options.TerragruntOptions) bool {
+	return strings.HasSuffix(opts.TerraformPath, options.TerraformDefaultPath)
+}
diff --git a/cli/commands/run/flags.go b/cli/commands/run/flags.go
index a7ea042102..adec97dec7 100644
--- a/cli/commands/run/flags.go
+++ b/cli/commands/run/flags.go
@@ -165,13 +165,7 @@ func NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
 		},
 			flags.WithDeprecatedNames(terragruntPrefix.FlagNames(DeprecatedConfigFlagName), terragruntPrefixControl)),
 
-		flags.NewFlag(&cli.GenericFlag[string]{
-			Name:        TFPathFlagName,
-			EnvVars:     tgPrefix.EnvVars(TFPathFlagName),
-			Destination: &opts.TerraformPath,
-			Usage:       "Path to the OpenTofu/Terraform binary. Default is tofu (on PATH).",
-		},
-			flags.WithDeprecatedNames(terragruntPrefix.FlagNames(DeprecatedTfpathFlagName), terragruntPrefixControl)),
+		NewTFPathFlag(opts, prefix),
 
 		flags.NewFlag(&cli.BoolFlag{
 			Name:        NoAutoInitFlagName,
@@ -547,3 +541,18 @@ func NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) cli.Flags {
 
 	return flags.Sort()
 }
+
+// NewTFPathFlag creates a flag for specifying the OpenTofu/Terraform binary path.
+func NewTFPathFlag(opts *options.TerragruntOptions, prefix flags.Prefix) *flags.Flag {
+	tgPrefix := prefix.Prepend(flags.TgPrefix)
+	terragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)
+	terragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)
+
+	return flags.NewFlag(&cli.GenericFlag[string]{
+		Name:        TFPathFlagName,
+		EnvVars:     tgPrefix.EnvVars(TFPathFlagName),
+		Destination: &opts.TerraformPath,
+		Usage:       "Path to the OpenTofu/Terraform binary. Default is tofu (on PATH).",
+	},
+		flags.WithDeprecatedNames(terragruntPrefix.FlagNames(DeprecatedTfpathFlagName), terragruntPrefixControl))
+}
diff --git a/cli/commands/run/help.go b/cli/commands/run/help.go
new file mode 100644
index 0000000000..126e54c5a8
--- /dev/null
+++ b/cli/commands/run/help.go
@@ -0,0 +1,81 @@
+package run
+
+import (
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/gruntwork-io/terragrunt/internal/cli"
+	"github.com/gruntwork-io/terragrunt/internal/errors"
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terragrunt/tf"
+	"github.com/gruntwork-io/terragrunt/util"
+)
+
+// TFCommandHelpTemplate is the TF command CLI help template.
+const TFCommandHelpTemplate = `Usage: {{ if .Command.UsageText }}{{ wrap .Command.UsageText 3 }}{{ else }}{{ range $parent := parentCommands . }}{{ $parent.HelpName }} {{ end }}[global options] {{ .Command.HelpName }} [options]{{ if eq .Command.Name "` + tf.CommandNameApply + `" }} [PLAN]{{ end }}{{ end }}{{ $description := .Command.Usage }}{{ if .Command.Description }}{{ $description = .Command.Description }}{{ end }}{{ if $description }}
+
+   {{ wrap $description 3 }}{{ end }}{{ if ne .Parent.Command.Name "` + CommandName + `" }}
+
+   This is a shortcut for the command ` + "`terragrunt " + CommandName + "`" + `.{{ end }}
+
+   It wraps the ` + "`{{ tfCommand }}`" + ` command of the binary defined by ` + "`tf-path`" + `.
+
+{{ if isTerraformPath }}Terraform{{ else }}OpenTofu{{ end }} ` + "`{{ tfCommand }}`" + ` help:{{ $tfHelp := runTFHelp }}{{ if $tfHelp }}
+
+{{ $tfHelp }}{{ end }}
+`
+
+// ShowTFHelp prints TF help for the given `ctx.Command` command.
+func ShowTFHelp(opts *options.TerragruntOptions) cli.HelpFunc {
+	return func(ctx *cli.Context) error {
+		if err := NewTFPathFlag(opts, nil).Parse(ctx.Args()); err != nil {
+			return err
+		}
+
+		cli.HelpPrinterCustom(ctx, TFCommandHelpTemplate, map[string]any{
+			"isTerraformPath": func() bool {
+				return isTerraformPath(opts)
+			},
+			"runTFHelp": func() string {
+				return runTFHelp(ctx, opts)
+			},
+			"tfCommand": func() string {
+				return ctx.Command.Name
+			},
+		})
+
+		return nil
+	}
+}
+
+func runTFHelp(ctx *cli.Context, opts *options.TerragruntOptions) string {
+	opts = opts.Clone()
+	opts.Writer = io.Discard
+
+	terraformHelpCmd := []string{tf.FlagNameHelpLong, ctx.Command.Name}
+
+	out, err := tf.RunCommandWithOutput(ctx, opts, terraformHelpCmd...)
+	if err != nil {
+		var processError util.ProcessExecutionError
+		if ok := errors.As(err, &processError); ok {
+			err = processError.Err
+		}
+
+		return fmt.Sprintf("Failed to execute \"%s %s\": %s", opts.TerraformPath, strings.Join(terraformHelpCmd, " "), err.Error())
+	}
+
+	result := out.Stdout.String()
+	lines := strings.Split(result, "\n")
+
+	// Trim first empty lines or that has prefix "Usage:".
+	for i := 0; i < len(lines); i++ {
+		if strings.TrimSpace(lines[i]) == "" || strings.HasPrefix(lines[i], "Usage:") {
+			continue
+		}
+
+		return strings.Join(lines[i:], "\n")
+	}
+
+	return result
+}
diff --git a/cli/flags/flag.go b/cli/flags/flag.go
index 23bfb2bc59..933ad24f56 100644
--- a/cli/flags/flag.go
+++ b/cli/flags/flag.go
@@ -4,6 +4,8 @@ package flags
 import (
 	"context"
 	"flag"
+	"io"
+	"strings"
 
 	"github.com/gruntwork-io/terragrunt/internal/cli"
 )
@@ -111,9 +113,31 @@ func (newFlag *Flag) RunAction(ctx *cli.Context) error {
 	return newFlag.Flag.RunAction(ctx)
 }
 
-// ParseEnvVars parses env vars values specified in the flag.
+// Parse parses the given `args` for the flag value and env vars values specified in the flag.
 // The value will be assigned to the `Destination` field.
 // The value can also be retrieved using `flag.Value().Get()`.
-func (newFlag *Flag) ParseEnvVars() error {
-	return newFlag.Apply(new(flag.FlagSet))
+func (newFlag *Flag) Parse(args cli.Args) error {
+	flagSet := flag.NewFlagSet("", flag.ContinueOnError)
+	flagSet.SetOutput(io.Discard)
+
+	if err := newFlag.Apply(flagSet); err != nil {
+		return err
+	}
+
+	const maxFlagsParse = 1000 // Maximum flags parse
+
+	for range maxFlagsParse {
+		err := flagSet.Parse(args)
+		if err == nil {
+			break
+		}
+
+		if errStr := err.Error(); !strings.HasPrefix(errStr, cli.ErrFlagUndefined) {
+			break
+		}
+
+		args = flagSet.Args()
+	}
+
+	return nil
 }
diff --git a/cli/help.go b/cli/help.go
index 18c3861720..198b4baa05 100644
--- a/cli/help.go
+++ b/cli/help.go
@@ -19,25 +19,25 @@ Author: {{ range .App.Authors }}{{ . }}{{ end }} {{ end }}
 `
 
 // CommandHelpTemplate is the command CLI help template.
-const CommandHelpTemplate = `Usage: {{if .Command.UsageText}}{{wrap .Command.UsageText 3}}{{else}}{{range $parent := parentCommands . }}{{$parent.HelpName}} {{end}}{{.Command.HelpName}}{{if .Command.VisibleSubcommands}} <command>{{end}}{{if .Command.VisibleFlags}} [options]{{end}}{{end}}{{$description := .Command.Usage}}{{if .Command.Description}}{{$description = .Command.Description}}{{end}}{{if $description}}
+const CommandHelpTemplate = `Usage: {{ if .Command.UsageText }}{{ wrap .Command.UsageText 3 }}{{ else }}{{ range $parent := parentCommands . }}{{ $parent.HelpName }} {{ end }}{{ .Command.HelpName }}{{ if .Command.VisibleSubcommands }} <command>{{ end }}{{ if .Command.VisibleFlags }} [options]{{ end }}{{ end }}{{ $description := .Command.Usage }}{{ if .Command.Description }}{{ $description = .Command.Description }}{{ end }}{{ if $description }}
 
-   {{wrap $description 3}}{{end}}{{if .Command.Examples}}
+   {{ wrap $description 3 }}{{ end }}{{ if .Command.Examples }}
 
 Examples:
-   {{$s := join .Command.Examples "\n\n"}}{{wrap $s 3}}{{end}}{{if .Command.VisibleSubcommands}}
+   {{ $s := join .Command.Examples "\n\n" }}{{ wrap $s 3 }}{{ end }}{{ if .Command.VisibleSubcommands }}
 
-Subcommands:{{ $cv := offsetCommands .Command.VisibleSubcommands 5}}{{range .Command.VisibleSubcommands}}
-   {{$s := .HelpName}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}} {{wrap .Usage $cv}}{{end}}{{end}}{{if .Command.VisibleFlags}}
+Subcommands:{{ $cv := offsetCommands .Command.VisibleSubcommands 5 }}{{ range .Command.VisibleSubcommands }}
+   {{ $s := .HelpName }}{{ $s }}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}} {{ wrap .Usage $cv }}{{ end }}{{ end }}{{ if .Command.VisibleFlags }}
 
 Options:
-   {{range $index, $option := .Command.VisibleFlags}}{{if $index}}
-   {{end}}{{wrap $option.String 6}}{{end}}{{end}}{{if .App.VisibleFlags}}
+   {{ range $index, $option := .Command.VisibleFlags }}{{ if $index }}
+   {{ end }}{{ wrap $option.String 6 }}{{ end }}{{ end }}{{ if .App.VisibleFlags }}
 
 Global Options:
-   {{range $index, $option := .App.VisibleFlags}}{{if $index}}
-   {{end}}{{wrap $option.String 6}}{{end}}{{end}}
+   {{ range $index, $option := .App.VisibleFlags }}{{ if $index }}
+   {{ end }}{{ wrap $option.String 6 }}{{ end }}{{ end }}
 
 `
 
-const AppVersionTemplate = `{{.App.Name}} version {{.App.Version}}
+const AppVersionTemplate = `{{ .App.Name }} version {{ .App.Version }}
 `
diff --git a/internal/cli/command.go b/internal/cli/command.go
index b1d9d4b78d..4fdf566978 100644
--- a/internal/cli/command.go
+++ b/internal/cli/command.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 )
 
-const errFlagUndefined = "flag provided but not defined:"
+const ErrFlagUndefined = "flag provided but not defined:"
 
 type Command struct {
 	// Name is the command name.
@@ -211,7 +211,9 @@ func (cmd *Command) flagSetParse(ctx *Context, flagSet *libflag.FlagSet, args Ar
 		return undefArgs, nil
 	}
 
-	for {
+	const maxFlagsParse = 1000 // Maximum flags parse
+
+	for range maxFlagsParse {
 		// check if the error is due to an undefArgs flag
 		var undefArg string
 
@@ -220,9 +222,9 @@ func (cmd *Command) flagSetParse(ctx *Context, flagSet *libflag.FlagSet, args Ar
 			break
 		}
 
-		if errStr := err.Error(); strings.HasPrefix(errStr, errFlagUndefined) {
+		if errStr := err.Error(); strings.HasPrefix(errStr, ErrFlagUndefined) {
 			err = UndefinedFlagError(errStr)
-			undefArg = strings.Trim(strings.TrimPrefix(errStr, errFlagUndefined), " -")
+			undefArg = strings.Trim(strings.TrimPrefix(errStr, ErrFlagUndefined), " -")
 		} else {
 			break
 		}
diff --git a/internal/cli/help.go b/internal/cli/help.go
index e521de3ddf..1be4c562e9 100644
--- a/internal/cli/help.go
+++ b/internal/cli/help.go
@@ -5,6 +5,8 @@ import (
 
 	"slices"
 
+	"maps"
+
 	"github.com/gruntwork-io/terragrunt/internal/errors"
 	"github.com/urfave/cli/v2"
 )
@@ -45,8 +47,16 @@ func ShowAppHelp(ctx *Context) error {
 
 // ShowCommandHelp prints command help for the given `ctx`.
 func ShowCommandHelp(ctx *Context) error {
+	if ctx.Command.HelpName == "" {
+		ctx.Command.HelpName = ctx.Command.Name
+	}
+
 	if ctx.Command.CustomHelp != nil {
-		return ctx.Command.CustomHelp(ctx)
+		if err := ctx.Command.CustomHelp(ctx); err != nil {
+			return err
+		}
+
+		return NewExitError(nil, ExitCodeSuccess)
 	}
 
 	tpl := ctx.Command.CustomHelpTemplate
@@ -58,16 +68,22 @@ func ShowCommandHelp(ctx *Context) error {
 		return errors.Errorf("command help template not defined")
 	}
 
-	if ctx.Command.HelpName == "" {
-		ctx.Command.HelpName = ctx.Command.Name
-	}
+	HelpPrinterCustom(ctx, tpl, nil)
 
-	cli.HelpPrinterCustom(ctx.App.Writer, tpl, ctx, map[string]any{
+	return NewExitError(nil, ExitCodeSuccess)
+}
+
+func HelpPrinterCustom(ctx *Context, tpl string, customFuncs map[string]any) {
+	var funcs = map[string]any{
 		"parentCommands": parentCommands,
 		"offsetCommands": offsetCommands,
-	})
+	}
 
-	return NewExitError(nil, ExitCodeSuccess)
+	if customFuncs != nil {
+		maps.Copy(funcs, customFuncs)
+	}
+
+	cli.HelpPrinterCustom(ctx.App.Writer, tpl, ctx, funcs)
 }
 
 func ShowVersion(ctx *Context) error {
diff --git a/main.go b/main.go
index d2ca7e9556..47f9aaa7d2 100644
--- a/main.go
+++ b/main.go
@@ -21,7 +21,7 @@ func main() {
 	opts := options.NewTerragruntOptions()
 
 	// Immediately parse the `TG_LOG_LEVEL` environment variable, e.g. to set the TRACE level.
-	if err := global.NewLogLevelFlag(opts, nil).ParseEnvVars(); err != nil {
+	if err := global.NewLogLevelFlag(opts, nil).Parse(os.Args); err != nil {
 		opts.Logger.Error(err.Error())
 		os.Exit(1)
 	}


Test patch:
diff --git a/cli/app_test.go b/cli/app_test.go
index 39100f33c5..a05910ac12 100644
--- a/cli/app_test.go
+++ b/cli/app_test.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"regexp"
 	"testing"
 
 	"github.com/gruntwork-io/terragrunt/cli"
@@ -456,15 +455,13 @@ func TestTerragruntHelp(t *testing.T) {
 func TestTerraformHelp(t *testing.T) {
 	t.Parallel()
 
-	wrappedBinary := options.DefaultWrappedPath
-
 	testCases := []struct {
 		args     []string
 		expected string
 	}{
-		{[]string{"terragrunt", tf.CommandNamePlan, "--help"}, "Usage: " + wrappedBinary + " .* plan"},
-		{[]string{"terragrunt", tf.CommandNameApply, "-help"}, "Usage: " + wrappedBinary + " .* apply"},
-		{[]string{"terragrunt", tf.CommandNameApply, "-h"}, "Usage: " + wrappedBinary + " .* apply"},
+		{[]string{"terragrunt", tf.CommandNamePlan, "--help"}, "(?s)Usage: terragrunt \\[global options\\] plan.*-detailed-exitcode"},
+		{[]string{"terragrunt", tf.CommandNameApply, "-help"}, "(?s)Usage: terragrunt \\[global options\\] apply.*-destroy"},
+		{[]string{"terragrunt", tf.CommandNameApply, "-h"}, "(?s)Usage: terragrunt \\[global options\\] apply.*-destroy"},
 	}
 
 	for _, testCase := range testCases {
@@ -474,10 +471,7 @@ func TestTerraformHelp(t *testing.T) {
 		err := app.Run(testCase.args)
 		require.NoError(t, err)
 
-		expectedRegex, err := regexp.Compile(testCase.expected)
-		require.NoError(t, err)
-
-		assert.Regexp(t, expectedRegex, output.String())
+		assert.Regexp(t, testCase.expected, output.String())
 	}
 }