diff --git a/command/cli.go b/command/cli.go index 56d598d7f..85a24dad6 100644 --- a/command/cli.go +++ b/command/cli.go @@ -147,12 +147,12 @@ func (va *FormatArgs) AddFlagSets(flags *flag.FlagSet) { flags.BoolVar(&va.Check, "check", false, "check if the input is formatted") flags.BoolVar(&va.Diff, "diff", false, "display the diff of formatting changes") flags.BoolVar(&va.Write, "write", true, "overwrite source files instead of writing to stdout") - + flags.BoolVar(&va.Recursive, "recursive", true, "Also process files in subdirectories") va.MetaArgs.AddFlagSets(flags) } // FormatArgs represents a parsed cli line for `packer fmt` type FormatArgs struct { MetaArgs - Check, Diff, Write bool + Check, Diff, Write, Recursive bool } diff --git a/command/fmt.go b/command/fmt.go index 5b15ed772..6cf896381 100644 --- a/command/fmt.go +++ b/command/fmt.go @@ -48,9 +48,10 @@ func (c *FormatCommand) RunContext(ctx context.Context, cla *FormatArgs) int { } formatter := hclutils.HCL2Formatter{ - ShowDiff: cla.Diff, - Write: cla.Write, - Output: os.Stdout, + ShowDiff: cla.Diff, + Write: cla.Write, + Output: os.Stdout, + Recursive: cla.Recursive, } bytesModified, diags := formatter.Format(cla.Path) @@ -90,6 +91,8 @@ Options: -write=false Don't write to source files (always disabled if using -check) + -recursive Also process files in subdirectories. By default, only the + given directory (or current directory) is processed. ` return strings.TrimSpace(helpText) @@ -105,8 +108,9 @@ func (*FormatCommand) AutocompleteArgs() complete.Predictor { func (*FormatCommand) AutocompleteFlags() complete.Flags { return complete.Flags{ - "-check": complete.PredictNothing, - "-diff": complete.PredictNothing, - "-write": complete.PredictNothing, + "-check": complete.PredictNothing, + "-diff": complete.PredictNothing, + "-write": complete.PredictNothing, + "-recursive": complete.PredictNothing, } } diff --git a/command/fmt_test.go b/command/fmt_test.go index 242845f38..ee7ef9832 100644 --- a/command/fmt_test.go +++ b/command/fmt_test.go @@ -50,3 +50,15 @@ func TestFmt_unfomattedTemlateDirectory(t *testing.T) { fatalCommand(t, c.Meta) } } + +func TestFmt_Recursive(t *testing.T) { + c := &FormatCommand{ + Meta: testMeta(t), + } + + args := []string{"-check=true", "-recursive=true", filepath.Join(testFixture("fmt"), "")} + + if code := c.Run(args); code != 3 { + fatalCommand(t, c.Meta) + } +} diff --git a/hcl2template/formatter.go b/hcl2template/formatter.go index d02d421a4..aa275a35e 100644 --- a/hcl2template/formatter.go +++ b/hcl2template/formatter.go @@ -7,6 +7,8 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" + "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclparse" @@ -14,9 +16,9 @@ import ( ) type HCL2Formatter struct { - ShowDiff, Write bool - Output io.Writer - parser *hclparse.Parser + ShowDiff, Write, Recursive bool + Output io.Writer + parser *hclparse.Parser } // NewHCL2Formatter creates a new formatter, ready to format configuration files. @@ -26,55 +28,88 @@ func NewHCL2Formatter() *HCL2Formatter { } } +func isHcl2FileOrVarFile(path string) bool { + if strings.HasSuffix(path, hcl2FileExt) || strings.HasSuffix(path, hcl2VarFileExt) { + return true + } + return false +} + +func (f *HCL2Formatter) formatFile(path string, diags hcl.Diagnostics, bytesModified int) (int, hcl.Diagnostics) { + data, err := f.processFile(path) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("encountered an error while formatting %s", path), + Detail: err.Error(), + }) + } + bytesModified += len(data) + return bytesModified, diags +} + // Format all HCL2 files in path and return the total bytes formatted. // 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) { + var diags hcl.Diagnostics + var bytesModified int - var allHclFiles []string - var diags []*hcl.Diagnostic - - if path == "-" { - allHclFiles = []string{"-"} - } else { - hclFiles, _, diags := GetHCL2Files(path, hcl2FileExt, hcl2JsonFileExt) - if diags.HasErrors() { - return 0, diags - } - - hclVarFiles, _, diags := GetHCL2Files(path, hcl2VarFileExt, hcl2VarJsonFileExt) - if diags.HasErrors() { - return 0, diags - } - - allHclFiles = append(hclFiles, hclVarFiles...) - - if len(allHclFiles) == 0 { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Cannot tell whether %s contains HCL2 configuration data", path), - }) - - return 0, diags - } + 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() } - var bytesModified int - for _, fn := range allHclFiles { - data, err := f.processFile(fn) + isDir, err := isDir(path) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot tell wether " + path + " is a directory", + Detail: err.Error(), + }) + return bytesModified, diags + } + + if !isDir { + bytesModified, diags = f.formatFile(path, diags, bytesModified) + } else { + fileInfos, err := ioutil.ReadDir(path) if err != nil { - diags = append(diags, &hcl.Diagnostic{ + diag := &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: fmt.Sprintf("encountered an error while formatting %s", fn), + 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) + } else { + continue + } + } + if isHcl2FileOrVarFile(filename) { + bytesModified, diags = f.formatFile(filename, diags, bytesModified) + } } - bytesModified += len(data) } return bytesModified, diags @@ -84,6 +119,7 @@ func (f *HCL2Formatter) Format(path string) (int, hcl.Diagnostics) { // overwriting the contents of the original when the f.Write is true; a diff of the changes // will be outputted if f.ShowDiff is true. func (f *HCL2Formatter) processFile(filename string) ([]byte, error) { + if f.Output == nil { f.Output = os.Stdout } diff --git a/hcl2template/formatter_test.go b/hcl2template/formatter_test.go index 4d1474f71..bb31e002d 100644 --- a/hcl2template/formatter_test.go +++ b/hcl2template/formatter_test.go @@ -32,11 +32,18 @@ func TestHCL2Formatter_Format(t *testing.T) { 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()) } + } +} +func TestHCL2Formatter_Recursive(t *testing.T) { + f := NewHCL2Formatter() + f.Recursive = true + _, diags := f.Format("testdata/format") + if diags.HasErrors() { + t.Fatalf("the call to Format failed unexpectedly %s", diags.Error()) } } diff --git a/hcl2template/testdata/format/sub_directory/unformatted.pkr.hcl b/hcl2template/testdata/format/sub_directory/unformatted.pkr.hcl new file mode 100644 index 000000000..86aa3a154 --- /dev/null +++ b/hcl2template/testdata/format/sub_directory/unformatted.pkr.hcl @@ -0,0 +1,149 @@ + +// starts resources to provision them. +build { + sources = [ + "source.amazon-ebs.ubuntu-1604", + "source.virtualbox-iso.ubuntu-1204", + ] + + provisioner "shell" { + string = coalesce(null, "", "string") + int = "${41 + 1}" + int64 = "${42 + 1}" + bool = "true" + trilean = true + duration = "${9 + 1}s" + map_string_string = { + a = "b" + c = "d" + } + slice_string = [ + "a", + "b", + "c", + ] + slice_slice_string = [ + ["a","b"], + ["c","d"] + ] + + nested { + string = "string" + int = 42 + int64 = 43 + bool = true + trilean = true + duration = "10s" + map_string_string = { + a = "b" + c = "d" + } + slice_string = [ + "a", + "b", + "c", + ] + slice_slice_string = [ + ["a","b"], + ["c","d"] + ] + } + + nested_slice { + } + } + + provisioner "file" { + string = "string" + int = 42 + int64 = 43 + bool = true + trilean = true + duration = "10s" + map_string_string = { + a = "b" + c = "d" + } + slice_string = [ + "a", + "b", + "c", + ] + slice_slice_string = [ + ["a","b"], + ["c","d"] + ] + + nested { + string = "string" + int = 42 + int64 = 43 + bool = true + trilean = true + duration = "10s" + map_string_string = { + a = "b" + c = "d" + } + slice_string = [ + "a", + "b", + "c", + ] + slice_slice_string = [ + ["a","b"], + ["c","d"] + ] + } + + nested_slice { + } + } + + post-processor "amazon-import" { + string = "string" + int = 42 + int64 = 43 + bool = true + trilean = true + duration = "10s" + map_string_string = { + a = "b" + c = "d" + } + slice_string = [ + "a", + "b", + "c", + ] + slice_slice_string = [ + ["a","b"], + ["c","d"] + ] + + nested { + string = "string" + int = 42 + int64 = 43 + bool = true + trilean = true + duration = "10s" + map_string_string = { + a = "b" + c = "d" + } + slice_string = [ + "a", + "b", + "c", + ] + slice_slice_string = [ + ["a","b"], + ["c","d"] + ] + } + + nested_slice { + } + } +} diff --git a/hcl2template/testdata/format/sub_directory/unformatted.pkrvars.hcl b/hcl2template/testdata/format/sub_directory/unformatted.pkrvars.hcl new file mode 100644 index 000000000..7ddc9df79 --- /dev/null +++ b/hcl2template/testdata/format/sub_directory/unformatted.pkrvars.hcl @@ -0,0 +1,3 @@ +ami_filter_name ="amzn2-ami-hvm-*-x86_64-gp2" +ami_filter_owners =[ "137112412989" ] +