formatting multiple hcl files (#13362)

* formatting multiple hcl files

* recursive formating

* test case fixing

* test case fix

* removed not required code

* existing test code fix

* go formatting

* more test cases for new cases

* doc changes

* added additional check on error message in negative test case
This commit is contained in:
anshulSharma 2025-05-02 10:16:00 +05:30 committed by GitHub
parent f16ca7fa03
commit e8d5ddb38a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 122 additions and 49 deletions

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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")
}
}
}

19
hcl2template/testdata/format/test.json vendored Normal file
View file

@ -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"
]
}
]
}

View file

@ -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