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

Issue:
Improve HCL formatting to work with stdin and stdout
**Describe the solution you'd like**
Improve Terragrunt `hclfmt` command to work with stdin and stdout to make Terragrunt easier to integrate into auto-formatting invocations from IDEs

**Describe alternatives you've considered**
Used script which read stdin, save in a temp file, format file, write to stdout...

**Additional context**
[Add any other context or screenshots about the feature request here.](https://github.com/orgs/gruntwork-io/discussions/801)

Improve HCL formatting to work with stdin and stdout
**Describe the solution you'd like**
Improve Terragrunt `hclfmt` command to work with stdin and stdout to make Terragrunt easier to integrate into auto-formatting invocations from IDEs

**Describe alternatives you've considered**
Used script which read stdin, save in a temp file, format file, write to stdout...

**Additional context**
[Add any other context or screenshots about the feature request here.](https://github.com/orgs/gruntwork-io/discussions/801)



Discussion / hints:
Need this to work with zed as well
In the meantime simple wrapper bash script can help: 

```shell
#!/bin/sh
# wrapper to format hcl passed via stdin
# user for for VSCcode Custom Local Formatters extension (https://marketplace.visualstudio.com/items?itemName=jkillian.custom-local-formatters)
# or any other editor relying on stdin/stdout for formatting, like Zed
# relevant issues:
# * https://github.com/gruntwork-io/terragrunt/issues/1037
# * https://github.com/gruntwork-io/terragrunt/issues/2926
TMPFILE=$(mktemp)
cat - > $TMPFILE
terragrunt hclfmt --terragrunt-hclfmt-file $TMPFILE
cat $TMPFILE
rm $TMPFILE
```



> In the meantime simple wrapper bash script can help: 
> 
> 
> 
> ```shell
> 
> #!/bin/sh
> 
> # wrapper to format hcl passed via stdin
> 
> # user for for VSCcode Custom Local Formatters extension (https://marketplace.visualstudio.com/items?itemName=jkillian.custom-local-formatters)
> 
> # or any other editor relying on stdin/stdout for formatting, like Zed
> 
> # relevant issues:
> 
> # * https://github.com/gruntwork-io/terragrunt/issues/1037
> 
> # * https://github.com/gruntwork-io/terragrunt/issues/2926
> 
> TMPFILE=$(mktemp)
> 
> cat - > $TMPFILE
> 
> terragrunt hclfmt --terragrunt-hclfmt-file $TMPFILE
> 
> cat $TMPFILE
> 
> rm $TMPFILE
> 
> ```
> 
> 
> 
> 
> 
> 

This is perfect, thank you!
Need this to work with zed as well
In the meantime simple wrapper bash script can help: 

```shell
#!/bin/sh
# wrapper to format hcl passed via stdin
# user for for VSCcode Custom Local Formatters extension (https://marketplace.visualstudio.com/items?itemName=jkillian.custom-local-formatters)
# or any other editor relying on stdin/stdout for formatting, like Zed
# relevant issues:
# * https://github.com/gruntwork-io/terragrunt/issues/1037
# * https://github.com/gruntwork-io/terragrunt/issues/2926
TMPFILE=$(mktemp)
cat - > $TMPFILE
terragrunt hclfmt --terragrunt-hclfmt-file $TMPFILE
cat $TMPFILE
rm $TMPFILE
```



> In the meantime simple wrapper bash script can help: 
> 
> 
> 
> ```shell
> 
> #!/bin/sh
> 
> # wrapper to format hcl passed via stdin
> 
> # user for for VSCcode Custom Local Formatters extension (https://marketplace.visualstudio.com/items?itemName=jkillian.custom-local-formatters)
> 
> # or any other editor relying on stdin/stdout for formatting, like Zed
> 
> # relevant issues:
> 
> # * https://github.com/gruntwork-io/terragrunt/issues/1037
> 
> # * https://github.com/gruntwork-io/terragrunt/issues/2926
> 
> TMPFILE=$(mktemp)
> 
> cat - > $TMPFILE
> 
> terragrunt hclfmt --terragrunt-hclfmt-file $TMPFILE
> 
> cat $TMPFILE
> 
> rm $TMPFILE
> 
> ```
> 
> 
> 
> 
> 
> 

This is perfect, thank you!

Fix patch:
diff --git a/cli/commands/hclfmt/action.go b/cli/commands/hclfmt/action.go
index e28c2aedd8..15f7e3ae2a 100644
--- a/cli/commands/hclfmt/action.go
+++ b/cli/commands/hclfmt/action.go
@@ -4,8 +4,10 @@
 package hclfmt
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
+	"io"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -27,6 +29,15 @@ import (
 func Run(opts *options.TerragruntOptions) error {
 	workingDir := opts.WorkingDir
 	targetFile := opts.HclFile
+	stdIn := opts.HclFromStdin
+
+	if stdIn {
+		if targetFile != "" {
+			return errors.Errorf("both stdin and path flags are specified")
+		}
+
+		return formatFromStdin(opts)
+	}
 
 	// handle when option specifies a particular file
 	if targetFile != "" {
@@ -80,6 +91,40 @@ func Run(opts *options.TerragruntOptions) error {
 	return formatErrors.ErrorOrNil()
 }
 
+func formatFromStdin(opts *options.TerragruntOptions) error {
+	contents, err := io.ReadAll(os.Stdin)
+
+	if err != nil {
+		opts.Logger.Errorf("Error reading from stdin: %s", err)
+
+		return fmt.Errorf("error reading from stdin: %w", err)
+	}
+
+	if err = checkErrors(opts.Logger, opts.DisableLogColors, contents, "stdin"); err != nil {
+		opts.Logger.Errorf("Error parsing hcl from stdin")
+
+		return fmt.Errorf("error parsing hcl from stdin: %w", err)
+	}
+
+	newContents := hclwrite.Format(contents)
+
+	buf := bufio.NewWriter(opts.Writer)
+
+	if _, err = buf.Write(newContents); err != nil {
+		opts.Logger.Errorf("Failed to write to stdout")
+
+		return fmt.Errorf("failed to write to stdout: %w", err)
+	}
+
+	if err = buf.Flush(); err != nil {
+		opts.Logger.Errorf("Failed to flush to stdout")
+
+		return fmt.Errorf("failed to flush to stdout: %w", err)
+	}
+
+	return nil
+}
+
 // formatTgHCL uses the hcl2 library to format the hcl file. This will attempt to parse the HCL file first to
 // ensure that there are no syntax errors, before attempting to format it.
 func formatTgHCL(opts *options.TerragruntOptions, tgHclFile string) error {
diff --git a/cli/commands/hclfmt/command.go b/cli/commands/hclfmt/command.go
index cdb5a96ad2..82d76e8490 100644
--- a/cli/commands/hclfmt/command.go
+++ b/cli/commands/hclfmt/command.go
@@ -9,9 +9,10 @@ import (
 const (
 	CommandName = "hclfmt"
 
-	FlagNameTerragruntHCLFmt = "terragrunt-hclfmt-file"
-	FlagNameTerragruntCheck  = "terragrunt-check"
-	FlagNameTerragruntDiff   = "terragrunt-diff"
+	FlagNameTerragruntHCLFmt      = "terragrunt-hclfmt-file"
+	FlagNameTerragruntCheck       = "terragrunt-check"
+	FlagNameTerragruntDiff        = "terragrunt-diff"
+	FlagNameTerragruntHCLFmtStdin = "terragrunt-hclfmt-stdin"
 )
 
 func NewFlags(opts *options.TerragruntOptions) cli.Flags {
@@ -33,6 +34,12 @@ func NewFlags(opts *options.TerragruntOptions) cli.Flags {
 			EnvVar:      "TERRAGRUNT_DIFF",
 			Usage:       "Print diff between original and modified file versions when running with 'hclfmt'.",
 		},
+		&cli.BoolFlag{
+			Name:        FlagNameTerragruntHCLFmtStdin,
+			Destination: &opts.HclFromStdin,
+			EnvVar:      "TERRAGRUNT_HCLFMT_STDIN",
+			Usage:       "Format HCL from stdin and print result to stdout.",
+		},
 	}
 }
 
diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md
index 00fbfd25d7..fc077e79bd 100644
--- a/docs/_docs/04_reference/cli-options.md
+++ b/docs/_docs/04_reference/cli-options.md
@@ -65,6 +65,7 @@ This page documents the CLI commands and options available with Terragrunt:
   - [terragrunt-check](#terragrunt-check)
   - [terragrunt-diff](#terragrunt-diff)
   - [terragrunt-hclfmt-file](#terragrunt-hclfmt-file)
+  - [terragrunt-hclfmt-stdin](#terragrunt-hclfmt-stdin)
   - [terragrunt-hclvalidate-json](#terragrunt-hclvalidate-json)
   - [terragrunt-hclvalidate-show-config-path](#terragrunt-hclvalidate-show-config-path)
   - [terragrunt-override-attr](#terragrunt-override-attr)
@@ -769,6 +770,7 @@ prefix `--terragrunt-` (e.g., `--terragrunt-config`). The currently available op
   - [terragrunt-check](#terragrunt-check)
   - [terragrunt-diff](#terragrunt-diff)
   - [terragrunt-hclfmt-file](#terragrunt-hclfmt-file)
+  - [terragrunt-hclfmt-stdin](#terragrunt-hclfmt-stdin)
   - [terragrunt-hclvalidate-json](#terragrunt-hclvalidate-json)
   - [terragrunt-hclvalidate-show-config-path](#terragrunt-hclvalidate-show-config-path)
   - [terragrunt-override-attr](#terragrunt-override-attr)
@@ -1149,7 +1151,15 @@ When passed in, running `hclfmt` will print diff between original and modified f
 
 - [hclfmt](#hclfmt)
 
-When passed in, run `hclfmt` only on specified hcl file.
+### terragrunt-hclfmt-stdin
+
+**CLI Arg**: `--terragrunt-hclfmt-stdin`<br/>
+**Environment Variable**: `TERRAGRUNT_HCLFMT_STDIN` (set to `true`)<br/>
+**Commands**:
+
+- [hclfmt](#hclfmt)
+
+When passed in, run `hclfmt` only on hcl passed to `stdin`, result is printed to `stdout`.
 
 ### terragrunt-hclvalidate-json
 
diff --git a/options/options.go b/options/options.go
index 42f45c09ed..bb19eaae46 100644
--- a/options/options.go
+++ b/options/options.go
@@ -238,6 +238,9 @@ type TerragruntOptions struct {
 	// The file which hclfmt should be specifically run on
 	HclFile string
 
+	// If True then HCL from StdIn must should be formatted.
+	HclFromStdin bool
+
 	// The file path that terragrunt should use when rendering the terragrunt.hcl config as json.
 	JSONOut string
 
@@ -582,6 +585,7 @@ func (opts *TerragruntOptions) Clone(terragruntConfigPath string) (*TerragruntOp
 		RunTerragrunt:                  opts.RunTerragrunt,
 		AwsProviderPatchOverrides:      opts.AwsProviderPatchOverrides,
 		HclFile:                        opts.HclFile,
+		HclFromStdin:                   opts.HclFromStdin,
 		JSONOut:                        opts.JSONOut,
 		JSONLogFormat:                  opts.JSONLogFormat,
 		Check:                          opts.Check,


Test patch:
diff --git a/cli/commands/hclfmt/action_test.go b/cli/commands/hclfmt/action_test.go
index 2cc085113d..7ac79353d1 100644
--- a/cli/commands/hclfmt/action_test.go
+++ b/cli/commands/hclfmt/action_test.go
@@ -263,6 +263,41 @@ func TestHCLFmtFile(t *testing.T) {
 	}
 }
 
+func TestHCLFmtStdin(t *testing.T) {
+	t.Parallel()
+
+	realStdin := os.Stdin
+	realStdout := os.Stdout
+
+	tempStdoutFile, err := os.CreateTemp(t.TempDir(), "stdout.hcl")
+	defer func() {
+		_ = tempStdoutFile.Close()
+	}()
+	require.NoError(t, err)
+
+	os.Stdout = tempStdoutFile
+	defer func() { os.Stdout = realStdout }()
+
+	os.Stdin, err = os.Open("../../../test/fixtures/hclfmt-stdin/terragrunt.hcl")
+	defer func() { os.Stdin = realStdin }()
+	require.NoError(t, err)
+
+	expected, err := os.ReadFile("../../../test/fixtures/hclfmt-stdin/expected.hcl")
+	require.NoError(t, err)
+
+	tgOptions, err := options.NewTerragruntOptionsForTest("")
+	require.NoError(t, err)
+
+	// format hcl from stdin
+	tgOptions.HclFromStdin = true
+	err = hclfmt.Run(tgOptions)
+	require.NoError(t, err)
+
+	formatted, err := os.ReadFile(tempStdoutFile.Name())
+	require.NoError(t, err)
+	assert.Equal(t, expected, formatted)
+}
+
 func TestHCLFmtHeredoc(t *testing.T) {
 	t.Parallel()
 
diff --git a/test/fixtures/hclfmt-stdin/expected.hcl b/test/fixtures/hclfmt-stdin/expected.hcl
new file mode 100644
index 0000000000..a9d035a9e7
--- /dev/null
+++ b/test/fixtures/hclfmt-stdin/expected.hcl
@@ -0,0 +1,13 @@
+inputs = {
+  # comments
+  foo = "bar"
+  bar = "baz"
+
+  inputs   = "disjoint"
+  disjoint = true
+
+  listInput = [
+    "foo",
+    "bar",
+  ]
+}
diff --git a/test/fixtures/hclfmt-stdin/terragrunt.hcl b/test/fixtures/hclfmt-stdin/terragrunt.hcl
new file mode 100644
index 0000000000..e7727b8065
--- /dev/null
+++ b/test/fixtures/hclfmt-stdin/terragrunt.hcl
@@ -0,0 +1,13 @@
+inputs = {
+# comments
+  foo =                               "bar"
+  bar="baz"
+
+  inputs = "disjoint"
+  disjoint = true
+
+  listInput = [
+"foo",
+"bar",
+]
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index f76d730b69..a11180320e 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -66,6 +66,7 @@ const (
 	testFixtureGraphDependencies              = "fixtures/graph-dependencies"
 	testFixtureHclfmtDiff                     = "fixtures/hclfmt-diff"
 	testFixtureHclvalidate                    = "fixtures/hclvalidate"
+	testFixtureHclfmtStdin                    = "fixtures/hclfmt-stdin"
 	testFixtureIamRolesMultipleModules        = "fixtures/read-config/iam_roles_multiple_modules"
 	testFixtureIncludeParent                  = "fixtures/include-parent"
 	testFixtureInfoError                      = "fixtures/terragrunt-info-error"
@@ -3322,6 +3323,24 @@ func TestHclFmtDiff(t *testing.T) {
 	assert.Contains(t, output, string(expectedDiff))
 }
 
+func TestHclFmtStdin(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, testFixtureHclfmtStdin)
+	tmpEnvPath := copyEnvironment(t, testFixtureHclfmtStdin)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureHclfmtStdin)
+
+	os.Stdin, _ = os.Open(util.JoinPath(rootPath, "terragrunt.hcl"))
+
+	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt hclfmt --terragrunt-hclfmt-stdin")
+	require.NoError(t, err)
+
+	expectedDiff, err := os.ReadFile(util.JoinPath(rootPath, "expected.hcl"))
+	require.NoError(t, err)
+
+	assert.Contains(t, stdout, string(expectedDiff))
+}
+
 func TestDownloadSourceWithRef(t *testing.T) {
 	t.Parallel()