294 lines
6 KiB
Go
294 lines
6 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package command
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/hashicorp/hcl/v2/hclwrite"
|
|
"github.com/posener/complete"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const (
|
|
stdinArg = "-"
|
|
stdinPath = "<stdin>"
|
|
)
|
|
|
|
type FormatCommand struct {
|
|
Meta
|
|
|
|
diagWr hcl.DiagnosticWriter
|
|
|
|
parser *hclparse.Parser
|
|
hclDiags hcl.Diagnostics
|
|
|
|
errs *multierror.Error
|
|
|
|
list bool
|
|
check bool
|
|
checkSuccess bool
|
|
recursive bool
|
|
writeFile bool
|
|
writeStdout bool
|
|
paths []string
|
|
|
|
stdin io.Reader
|
|
}
|
|
|
|
func (*FormatCommand) Help() string {
|
|
helpText := `
|
|
Usage: nomad fmt [flags] paths ...
|
|
|
|
Formats Nomad agent configuration and job file to a canonical format.
|
|
If a path is a directory, it will recursively format all files
|
|
with .nomad and .hcl extensions in the directory.
|
|
|
|
If you provide a single dash (-) as argument, fmt will read from standard
|
|
input (STDIN) and output the processed output to standard output (STDOUT).
|
|
|
|
Format Options:
|
|
|
|
-check
|
|
Check if the files are valid HCL files. If not, exit status of the
|
|
command will be 1 and the incorrect files will not be formatted. This
|
|
flag overrides any -write flag value.
|
|
|
|
-list
|
|
List the files which contain formatting inconsistencies. Defaults
|
|
to -list=true.
|
|
|
|
-recursive
|
|
Process files in subdirectories. By default only the given (or current)
|
|
directory is processed.
|
|
|
|
-write
|
|
Overwrite the input files. Defaults to -write=true. Ignored if the input
|
|
comes from STDIN.
|
|
`
|
|
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (*FormatCommand) Synopsis() string {
|
|
return "Rewrites Nomad config and job files to canonical format"
|
|
}
|
|
|
|
func (*FormatCommand) AutocompleteArgs() complete.Predictor {
|
|
return complete.PredictFiles("*")
|
|
}
|
|
|
|
func (*FormatCommand) AutocompleteFlags() complete.Flags {
|
|
return complete.Flags{
|
|
"-check": complete.PredictNothing,
|
|
"-write": complete.PredictNothing,
|
|
"-list": complete.PredictNothing,
|
|
"-recursive": complete.PredictNothing,
|
|
}
|
|
}
|
|
|
|
func (f *FormatCommand) Name() string { return "fmt" }
|
|
|
|
func (f *FormatCommand) Run(args []string) int {
|
|
if f.stdin == nil {
|
|
f.stdin = os.Stdin
|
|
}
|
|
|
|
flags := f.Meta.FlagSet(f.Name(), FlagSetClient)
|
|
flags.Usage = func() { f.Ui.Output(f.Help()) }
|
|
flags.BoolVar(&f.check, "check", false, "")
|
|
flags.BoolVar(&f.writeFile, "write", true, "")
|
|
flags.BoolVar(&f.list, "list", true, "")
|
|
flags.BoolVar(&f.recursive, "recursive", false, "")
|
|
|
|
if err := flags.Parse(args); err != nil {
|
|
return 1
|
|
}
|
|
f.checkSuccess = true
|
|
f.parser = hclparse.NewParser()
|
|
|
|
color := terminal.IsTerminal(int(os.Stderr.Fd()))
|
|
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
w = 80
|
|
}
|
|
|
|
f.diagWr = hcl.NewDiagnosticTextWriter(os.Stderr, f.parser.Files(), uint(w), color)
|
|
|
|
if len(flags.Args()) == 0 {
|
|
f.paths = []string{"."}
|
|
} else if flags.Args()[0] == stdinArg {
|
|
f.writeStdout = true
|
|
f.writeFile = false
|
|
f.list = false
|
|
} else {
|
|
f.paths = flags.Args()
|
|
}
|
|
if f.check {
|
|
f.writeFile = false
|
|
}
|
|
if !f.list && !f.writeFile {
|
|
f.writeStdout = true
|
|
}
|
|
|
|
f.fmt()
|
|
|
|
if f.hclDiags.HasErrors() {
|
|
f.diagWr.WriteDiagnostics(f.hclDiags)
|
|
}
|
|
|
|
if f.errs != nil {
|
|
f.Ui.Error(f.errs.Error())
|
|
f.Ui.Error(commandErrorText(f))
|
|
}
|
|
|
|
if f.hclDiags.HasErrors() || f.errs != nil {
|
|
return 1
|
|
}
|
|
|
|
if !f.checkSuccess {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (f *FormatCommand) fmt() {
|
|
if len(f.paths) == 0 {
|
|
f.processFile(stdinPath, f.stdin)
|
|
return
|
|
}
|
|
|
|
for _, path := range f.paths {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
f.appendError(fmt.Errorf("No file or directory at %s", path))
|
|
continue
|
|
}
|
|
|
|
if info.IsDir() {
|
|
f.processDir(path)
|
|
} else {
|
|
if isNomadFile(info) {
|
|
fp, err := os.Open(path)
|
|
if err != nil {
|
|
f.appendError(fmt.Errorf("Failed to open file %s: %w", path, err))
|
|
continue
|
|
}
|
|
|
|
f.processFile(path, fp)
|
|
|
|
fp.Close()
|
|
} else {
|
|
f.appendError(fmt.Errorf("Only .nomad and .hcl files can be processed using nomad fmt"))
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *FormatCommand) processDir(path string) {
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
f.appendError(fmt.Errorf("Failed to list directory %s", path))
|
|
return
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
subpath := filepath.Join(path, name)
|
|
|
|
if entry.IsDir() {
|
|
if f.recursive {
|
|
f.processDir(subpath)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
f.appendError(err)
|
|
continue
|
|
}
|
|
|
|
if isNomadFile(info) {
|
|
fp, err := os.Open(subpath)
|
|
if err != nil {
|
|
f.appendError(fmt.Errorf("Failed to open file %s: %w", path, err))
|
|
continue
|
|
}
|
|
|
|
f.processFile(subpath, fp)
|
|
|
|
fp.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *FormatCommand) processFile(path string, r io.Reader) {
|
|
src, err := io.ReadAll(r)
|
|
if err != nil {
|
|
f.appendError(fmt.Errorf("Failed to read file %s: %w", path, err))
|
|
return
|
|
}
|
|
|
|
f.parser.AddFile(path, &hcl.File{
|
|
Body: hcl.EmptyBody(),
|
|
Bytes: src,
|
|
})
|
|
|
|
_, syntaxDiags := hclsyntax.ParseConfig(src, path, hcl.InitialPos)
|
|
if syntaxDiags.HasErrors() {
|
|
f.hclDiags = append(f.hclDiags, syntaxDiags...)
|
|
return
|
|
}
|
|
formattedFile, diags := hclwrite.ParseConfig(src, path, hcl.InitialPos)
|
|
if diags.HasErrors() {
|
|
f.hclDiags = append(f.hclDiags, diags...)
|
|
return
|
|
}
|
|
|
|
out := formattedFile.Bytes()
|
|
|
|
if !bytes.Equal(src, out) {
|
|
if f.list {
|
|
f.Ui.Output(path)
|
|
}
|
|
|
|
if f.check {
|
|
f.checkSuccess = false
|
|
}
|
|
|
|
if f.writeFile {
|
|
if err := os.WriteFile(path, out, 0644); err != nil {
|
|
f.appendError(fmt.Errorf("Failed to write file %s: %w", path, err))
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if f.writeStdout {
|
|
f.Ui.Output(string(out))
|
|
}
|
|
}
|
|
|
|
func isNomadFile(file fs.FileInfo) bool {
|
|
return !file.IsDir() && (filepath.Ext(file.Name()) == ".nomad" || filepath.Ext(file.Name()) == ".hcl")
|
|
}
|
|
|
|
func (f *FormatCommand) appendError(err error) {
|
|
f.errs = multierror.Append(f.errs, err)
|
|
}
|