packer-cn/hcl2template/formatter.go
Wilken Rivera acabc1c1aa
Add packer fmt command (#10225)
* Add packer fmt command

This change adds a new command that allows users to format one or more
HCL2 Packer configuration template files.

Related to: #9174

* command/fmt: Add check flag

Packer's fmt command now supports a check flag that will output the name
of any file that would be changed by the HCL2 formatting engine. The
check flag is mutually exclusive with the write flag and will only check
if formatting is needed.

The update write flag will now overwrite the source files with the newly
formatted HCL2 source unless the `-write=false` or `-check` is passed at
the command line.

* Returns a diagnostic error if Format is unable to show a diff - equivalent to `terraform fmt`
* Updates testing to run against #Format and not the private methods of the HCL2Formatter; fixes ShowDiff test failure on Windows
* Updates comments for exported functions

* Add docs for fmt command
2020-11-11 11:49:39 -05:00

152 lines
3.5 KiB
Go

package hcl2template
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/hcl/v2/hclwrite"
)
type HCL2Formatter struct {
ShowDiff, Write bool
Output io.Writer
parser *hclparse.Parser
}
// NewHCL2Formatter creates a new formatter, ready to format configuration files.
func NewHCL2Formatter() *HCL2Formatter {
return &HCL2Formatter{
parser: hclparse.NewParser(),
}
}
// 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) {
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 f.parser == nil {
f.parser = hclparse.NewParser()
}
var bytesModified int
for _, fn := range allHclFiles {
data, err := f.processFile(fn)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("encountered an error while formatting %s", fn),
Detail: err.Error(),
})
}
bytesModified += len(data)
}
return bytesModified, diags
}
// processFile formats the source contents of filename and return the formatted data.
// 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
}
in, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %s", filename, err)
}
inSrc, err := ioutil.ReadAll(in)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %s", filename, err)
}
_, diags := f.parser.ParseHCL(inSrc, filename)
if diags.HasErrors() {
return nil, fmt.Errorf("failed to parse HCL %s", filename)
}
outSrc := hclwrite.Format(inSrc)
if bytes.Equal(inSrc, outSrc) {
return nil, nil
}
s := []byte(fmt.Sprintf("%s\n", filename))
_, _ = f.Output.Write(s)
if f.Write {
if err := ioutil.WriteFile(filename, outSrc, 0644); err != nil {
return nil, err
}
}
if f.ShowDiff {
diff, err := bytesDiff(inSrc, outSrc, filename)
if err != nil {
return outSrc, fmt.Errorf("failed to generate diff for %s: %s", filename, err)
}
_, _ = f.Output.Write(diff)
}
return outSrc, nil
}
// 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) {
f1, err := ioutil.TempFile("", "")
if err != nil {
return
}
defer os.Remove(f1.Name())
defer f1.Close()
f2, err := ioutil.TempFile("", "")
if err != nil {
return
}
defer os.Remove(f2.Name())
defer f2.Close()
_, _ = f1.Write(b1)
_, _ = f2.Write(b2)
data, err = exec.Command("diff", "--label=old/"+path, "--label=new/"+path, "-u", f1.Name(), f2.Name()).CombinedOutput()
if len(data) > 0 {
// diff exits with a non-zero status when the files don't match.
// Ignore that failure as long as we get output.
err = nil
}
return
}