From 6743a2254ba3d642b7d3a0be506259a0842819ac Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 9 Aug 2018 18:10:00 -0700 Subject: [PATCH] cmd/hcldec: opt-in JSON-formatted diagnostics By default we generate human-readable diagnostics on the assumption that the caller is a simple program that is capturing stdin via a pipe and letting stderr go to the terminal. More sophisticated callers may wish to analyze the diagnostics themselves and perhaps present them in a different way, such as via a GUI. --- cmd/hcldec/diags_json.go | 101 +++++++++++++++++++++++++++++++++++++++ cmd/hcldec/main.go | 26 +++++++--- 2 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 cmd/hcldec/diags_json.go diff --git a/cmd/hcldec/diags_json.go b/cmd/hcldec/diags_json.go new file mode 100644 index 0000000..e24181f --- /dev/null +++ b/cmd/hcldec/diags_json.go @@ -0,0 +1,101 @@ +package main + +import ( + "encoding/json" + "io" + + "github.com/hashicorp/hcl2/hcl" +) + +type jsonDiagWriter struct { + w io.Writer + diags hcl.Diagnostics +} + +var _ hcl.DiagnosticWriter = &jsonDiagWriter{} + +func (wr *jsonDiagWriter) WriteDiagnostic(diag *hcl.Diagnostic) error { + wr.diags = append(wr.diags, diag) + return nil +} + +func (wr *jsonDiagWriter) WriteDiagnostics(diags hcl.Diagnostics) error { + wr.diags = append(wr.diags, diags...) + return nil +} + +func (wr *jsonDiagWriter) Flush() error { + if len(wr.diags) == 0 { + return nil + } + + type PosJSON struct { + Line int `json:"line"` + Column int `json:"column"` + Byte int `json:"byte"` + } + type RangeJSON struct { + Filename string `json:"filename"` + Start PosJSON `json:"start"` + End PosJSON `json:"end"` + } + type DiagnosticJSON struct { + Severity string `json:"severity"` + Summary string `json:"summary"` + Detail string `json:"detail,omitempty"` + Subject *RangeJSON `json:"subject,omitempty"` + } + type DiagnosticsJSON struct { + Diagnostics []DiagnosticJSON `json:"diagnostics"` + } + + diagsJSON := make([]DiagnosticJSON, 0, len(wr.diags)) + for _, diag := range wr.diags { + var diagJSON DiagnosticJSON + + switch diag.Severity { + case hcl.DiagError: + diagJSON.Severity = "error" + case hcl.DiagWarning: + diagJSON.Severity = "warning" + default: + diagJSON.Severity = "(unknown)" // should never happen + } + + diagJSON.Summary = diag.Summary + diagJSON.Detail = diag.Detail + if diag.Subject != nil { + diagJSON.Subject = &RangeJSON{} + sJSON := diagJSON.Subject + rng := diag.Subject + sJSON.Filename = rng.Filename + sJSON.Start.Line = rng.Start.Line + sJSON.Start.Column = rng.Start.Column + sJSON.Start.Byte = rng.Start.Byte + sJSON.End.Line = rng.End.Line + sJSON.End.Column = rng.End.Column + sJSON.End.Byte = rng.End.Byte + } + + diagsJSON = append(diagsJSON, diagJSON) + } + + src, err := json.MarshalIndent(DiagnosticsJSON{diagsJSON}, "", " ") + if err != nil { + return err + } + _, err = wr.w.Write(src) + wr.w.Write([]byte{'\n'}) + return err +} + +type flusher interface { + Flush() error +} + +func flush(maybeFlusher interface{}) error { + if f, ok := maybeFlusher.(flusher); ok { + return f.Flush() + } + return nil +} diff --git a/cmd/hcldec/main.go b/cmd/hcldec/main.go index dde87a5..4f14226 100644 --- a/cmd/hcldec/main.go +++ b/cmd/hcldec/main.go @@ -26,6 +26,7 @@ var vars = &varSpecs{} var ( specFile = flag.StringP("spec", "s", "", "path to spec file (required)") outputFile = flag.StringP("out", "o", "", "write to the given file, instead of stdout") + diagsFormat = flag.StringP("diags", "", "", "format any returned diagnostics in the given format; currently only \"json\" is accepted") showVarRefs = flag.BoolP("var-refs", "", false, "rather than decoding input, produce a JSON description of the variables referenced by it") showVersion = flag.BoolP("version", "v", false, "show the version number and immediately exit") ) @@ -35,13 +36,6 @@ var diagWr hcl.DiagnosticWriter // initialized in init func init() { flag.VarP(vars, "vars", "V", "provide variables to the given configuration file(s)") - - color := terminal.IsTerminal(int(os.Stderr.Fd())) - w, _, err := terminal.GetSize(int(os.Stdout.Fd())) - if err != nil { - w = 80 - } - diagWr = hcl.NewDiagnosticTextWriter(os.Stderr, parser.Files(), uint(w), color) } func main() { @@ -55,6 +49,21 @@ func main() { args := flag.Args() + switch *diagsFormat { + case "": + color := terminal.IsTerminal(int(os.Stderr.Fd())) + w, _, err := terminal.GetSize(int(os.Stdout.Fd())) + if err != nil { + w = 80 + } + diagWr = hcl.NewDiagnosticTextWriter(os.Stderr, parser.Files(), uint(w), color) + case "json": + diagWr = &jsonDiagWriter{w: os.Stderr} + default: + fmt.Fprintf(os.Stderr, "Invalid diagnostics format %q: only \"json\" is supported.\n", *diagsFormat) + os.Exit(2) + } + err := realmain(args) if err != nil { @@ -75,6 +84,7 @@ func realmain(args []string) error { diags = append(diags, specDiags...) if specDiags.HasErrors() { diagWr.WriteDiagnostics(diags) + flush(diagWr) os.Exit(2) } @@ -146,6 +156,7 @@ func realmain(args []string) error { if diags.HasErrors() { diagWr.WriteDiagnostics(diags) + flush(diagWr) os.Exit(2) } @@ -170,6 +181,7 @@ func realmain(args []string) error { if diags.HasErrors() { diagWr.WriteDiagnostics(diags) + flush(diagWr) os.Exit(2) }