diff --git a/command/cli.go b/command/cli.go index ebe2a2b83..fa0797392 100644 --- a/command/cli.go +++ b/command/cli.go @@ -66,6 +66,7 @@ type MetaArgs struct { // TODO(azr): in the future, I want to allow passing multiple path to // merge HCL confs together; but this will probably need an RFC first. Path string + Paths []string Only, Except []string Vars map[string]string VarFiles []string diff --git a/command/fmt.go b/command/fmt.go index 3a70bcf18..abd2d050a 100644 --- a/command/fmt.go +++ b/command/fmt.go @@ -36,12 +36,12 @@ func (c *FormatCommand) ParseArgs(args []string) (*FormatArgs, int) { } args = flags.Args() - if len(args) != 1 { + if len(args) == 0 { flags.Usage() return &cfg, 1 } - cfg.Path = args[0] + cfg.Paths = args return &cfg, 0 } @@ -57,7 +57,7 @@ func (c *FormatCommand) RunContext(ctx context.Context, cla *FormatArgs) int { Recursive: cla.Recursive, } - bytesModified, diags := formatter.Format(cla.Path) + bytesModified, diags := formatter.Format(cla.Paths) ret := writeDiags(c.Ui, nil, diags) if ret != 0 { return ret diff --git a/hcl2template/formatter.go b/hcl2template/formatter.go index 3835aea8b..a4d34e26e 100644 --- a/hcl2template/formatter.go +++ b/hcl2template/formatter.go @@ -55,52 +55,53 @@ func (f *HCL2Formatter) formatFile(path string, diags hcl.Diagnostics, bytesModi // If any error is encountered, zero bytes will be returned. // // Path can be a directory or a file. -func (f *HCL2Formatter) Format(path string) (int, hcl.Diagnostics) { +func (f *HCL2Formatter) Format(paths []string) (int, hcl.Diagnostics) { var diags hcl.Diagnostics var bytesModified int - if path == "" { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "path is empty, cannot format", - Detail: "path is empty, cannot format", - }) - return bytesModified, diags - } - if f.parser == nil { f.parser = hclparse.NewParser() } - if s, err := os.Stat(path); err != nil || !s.IsDir() { - return f.formatFile(path, diags, bytesModified) - } + for _, path := range paths { + s, err := os.Stat(path) - fileInfos, err := os.ReadDir(path) - if err != nil { - diag := &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Cannot read hcl directory", - Detail: err.Error(), - } - diags = append(diags, diag) - return bytesModified, diags - } - - for _, fileInfo := range fileInfos { - filename := filepath.Join(path, fileInfo.Name()) - if fileInfo.IsDir() { - if f.Recursive { - var tempDiags hcl.Diagnostics - var tempBytesModified int - tempBytesModified, tempDiags = f.Format(filename) - bytesModified += tempBytesModified - diags = diags.Extend(tempDiags) + if err != nil || !s.IsDir() { + bytesModified, diags = f.formatFile(path, diags, bytesModified) + } else { + fileInfos, err := os.ReadDir(path) + if err != nil { + diag := &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot read hcl directory", + Detail: err.Error(), + } + diags = append(diags, diag) + return bytesModified, diags + } + + for _, fileInfo := range fileInfos { + name := fileInfo.Name() + if f.shouldIgnoreFile(name) { + continue + } + filename := filepath.Join(path, name) + if fileInfo.IsDir() { + if f.Recursive { + var tempDiags hcl.Diagnostics + var tempBytesModified int + var newPaths []string + newPaths = append(newPaths, filename) + tempBytesModified, tempDiags = f.Format(newPaths) + bytesModified += tempBytesModified + diags = diags.Extend(tempDiags) + } + continue + } + if isHcl2FileOrVarFile(filename) { + bytesModified, diags = f.formatFile(filename, diags, bytesModified) + } } - continue - } - if isHcl2FileOrVarFile(filename) { - bytesModified, diags = f.formatFile(filename, diags, bytesModified) } } @@ -116,6 +117,10 @@ func (f *HCL2Formatter) processFile(filename string) ([]byte, error) { f.Output = os.Stdout } + if !(filename == "-") && !isHcl2FileOrVarFile(filename) { + return nil, fmt.Errorf("file %s is not a HCL file", filename) + } + var in io.Reader var err error @@ -175,6 +180,14 @@ func (f *HCL2Formatter) processFile(filename string) ([]byte, error) { return outSrc, nil } +// shouldIgnoreFile returns true if the given filename (which must not have a +// directory path ahead of it) should be ignored as e.g. an editor swap file. +func (f *HCL2Formatter) shouldIgnoreFile(name string) bool { + return strings.HasPrefix(name, ".") || // Unix-like hidden files + strings.HasSuffix(name, "~") || // vim + strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#") // emacs +} + // bytesDiff returns the unified diff of b1 and b2 // Shamelessly copied from Terraform's fmt command. func bytesDiff(b1, b2 []byte, path string) (data []byte, err error) { diff --git a/hcl2template/formatter_test.go b/hcl2template/formatter_test.go index 8cb909292..a5cc9d30a 100644 --- a/hcl2template/formatter_test.go +++ b/hcl2template/formatter_test.go @@ -16,13 +16,15 @@ import ( func TestHCL2Formatter_Format(t *testing.T) { tt := []struct { Name string - Path string + Paths []string FormatExpected bool }{ - {Name: "Unformatted file", Path: "testdata/format/unformatted.pkr.hcl", FormatExpected: true}, - {Name: "Unformatted vars file", Path: "testdata/format/unformatted.pkrvars.hcl", FormatExpected: true}, - {Name: "Formatted file", Path: "testdata/format/formatted.pkr.hcl"}, - {Name: "Directory", Path: "testdata/format", FormatExpected: true}, + {Name: "Unformatted file", Paths: []string{"testdata/format/unformatted.pkr.hcl"}, FormatExpected: true}, + {Name: "Unformatted vars file", Paths: []string{"testdata/format/unformatted.pkrvars.hcl"}, FormatExpected: true}, + {Name: "Formatted file", Paths: []string{"testdata/format/formatted.pkr.hcl"}}, + {Name: "Directory", Paths: []string{"testdata/format"}, FormatExpected: true}, + {Name: "No file", Paths: []string{}, FormatExpected: false}, + {Name: "Multi File", Paths: []string{"testdata/format/unformatted.pkr.hcl", "testdata/format/unformatted.pkrvars.hcl"}, FormatExpected: true}, } for _, tc := range tt { @@ -30,12 +32,12 @@ func TestHCL2Formatter_Format(t *testing.T) { var buf bytes.Buffer f := NewHCL2Formatter() f.Output = &buf - _, diags := f.Format(tc.Path) + _, diags := f.Format(tc.Paths) if diags.HasErrors() { t.Fatalf("the call to Format failed unexpectedly %s", diags.Error()) } if buf.String() != "" && tc.FormatExpected == false { - t.Errorf("Format(%q) should contain the name of the formatted file(s), but got %q", tc.Path, buf.String()) + t.Errorf("Format(%q) should contain the name of the formatted file(s), but got %q", tc.Paths, buf.String()) } } } @@ -61,7 +63,9 @@ func TestHCL2Formatter_Format_Write(t *testing.T) { _, _ = tf.Write(unformattedData) tf.Close() - _, diags := f.Format(tf.Name()) + var paths []string + paths = append(paths, tf.Name()) + _, diags := f.Format(paths) if diags.HasErrors() { t.Fatalf("the call to Format failed unexpectedly %s", diags.Error()) } @@ -94,7 +98,9 @@ func TestHCL2Formatter_Format_ShowDiff(t *testing.T) { ShowDiff: true, } - _, diags := f.Format("testdata/format/unformatted.pkr.hcl") + var paths []string + paths = append(paths, "testdata/format/unformatted.pkr.hcl") + _, diags := f.Format(paths) if diags.HasErrors() { t.Fatalf("the call to Format failed unexpectedly %s", diags.Error()) } @@ -109,3 +115,28 @@ func TestHCL2Formatter_Format_ShowDiff(t *testing.T) { } } + +func TestHCL2Formatter_FormatNegativeCases(t *testing.T) { + tt := []struct { + Name string + Paths []string + errExpected bool + }{ + {Name: "Unformatted file", Paths: []string{"testdata/format/test.json"}, errExpected: true}, + } + + for _, tc := range tt { + tc := tc + var buf bytes.Buffer + f := NewHCL2Formatter() + f.Output = &buf + _, diags := f.Format(tc.Paths) + if tc.errExpected && !diags.HasErrors() { + t.Fatalf("Expected error but got none") + } + + if diags[0].Detail != "file testdata/format/test.json is not a HCL file" { + t.Fatalf("Expected error messge did not received") + } + } +} diff --git a/hcl2template/testdata/format/test.json b/hcl2template/testdata/format/test.json new file mode 100644 index 000000000..eab6f7dd1 --- /dev/null +++ b/hcl2template/testdata/format/test.json @@ -0,0 +1,19 @@ +{ + "builders": [ + { + "type": "amazon-ebs", + "access_key": "YOUR_AWS_ACCESS_KEY", + "secret_key": "YOUR_AWS_SECRET_KEY", + "region": "us-east-1" + } + ], + "provisioners": [ + { + "type": "shell", + "inline": [ + "sudo apt-get update", + "sudo apt-get install -y nginx" + ] + } + ] +} diff --git a/website/content/docs/commands/fmt.mdx b/website/content/docs/commands/fmt.mdx index 03a24e800..23e7909ff 100644 --- a/website/content/docs/commands/fmt.mdx +++ b/website/content/docs/commands/fmt.mdx @@ -32,6 +32,15 @@ my-template.pkr.hcl ``` +Format multiple configuration files, writing the changes back to respective original files. + +```shell-session +$ packer fmt my-template.pkr.hcl my-varfile.pkrvars.hcl +my-template.pkr.hcl +my-varfile.pkrvars.hcl + +``` + Format a configuration file, reading from stdin and writing to stdout. ```shell-session