packer-cn/hcl2template/formatter.go

152 lines
3.5 KiB
Go
Raw Normal View History

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
}