6c4344623b
The main HCL package is more visible this way, and so it's easier than having to pick it out from dozens of other package directories.
370 lines
9.1 KiB
Go
370 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
|
flag "github.com/spf13/pflag"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
ctyjson "github.com/zclconf/go-cty/cty/json"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const versionStr = "0.0.1-dev"
|
|
|
|
// vars is populated from --vars arguments on the command line, via a flag
|
|
// registration in init() below.
|
|
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")
|
|
withType = flag.BoolP("with-type", "", false, "include an additional object level at the top describing the HCL-oriented type of the result value")
|
|
showVersion = flag.BoolP("version", "v", false, "show the version number and immediately exit")
|
|
)
|
|
|
|
var parser = hclparse.NewParser()
|
|
var diagWr hcl.DiagnosticWriter // initialized in init
|
|
|
|
func init() {
|
|
flag.VarP(vars, "vars", "V", "provide variables to the given configuration file(s)")
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
|
|
if *showVersion {
|
|
fmt.Println(versionStr)
|
|
os.Exit(0)
|
|
}
|
|
|
|
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 {
|
|
fmt.Fprintf(os.Stderr, "Error: %s\n\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func realmain(args []string) error {
|
|
|
|
if *specFile == "" {
|
|
return fmt.Errorf("the --spec=... argument is required")
|
|
}
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
specContent, specDiags := loadSpecFile(*specFile)
|
|
diags = append(diags, specDiags...)
|
|
if specDiags.HasErrors() {
|
|
diagWr.WriteDiagnostics(diags)
|
|
flush(diagWr)
|
|
os.Exit(2)
|
|
}
|
|
|
|
spec := specContent.RootSpec
|
|
|
|
ctx := &hcl.EvalContext{
|
|
Variables: map[string]cty.Value{},
|
|
Functions: map[string]function.Function{},
|
|
}
|
|
for name, val := range specContent.Variables {
|
|
ctx.Variables[name] = val
|
|
}
|
|
for name, f := range specContent.Functions {
|
|
ctx.Functions[name] = f
|
|
}
|
|
if len(*vars) != 0 {
|
|
for i, varsSpec := range *vars {
|
|
var vals map[string]cty.Value
|
|
var valsDiags hcl.Diagnostics
|
|
if strings.HasPrefix(strings.TrimSpace(varsSpec), "{") {
|
|
// literal JSON object on the command line
|
|
vals, valsDiags = parseVarsArg(varsSpec, i)
|
|
} else {
|
|
// path to a file containing either HCL or JSON (by file extension)
|
|
vals, valsDiags = parseVarsFile(varsSpec)
|
|
}
|
|
diags = append(diags, valsDiags...)
|
|
for k, v := range vals {
|
|
ctx.Variables[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we have empty context elements then we'll nil them out so that
|
|
// we'll produce e.g. "variables are not allowed" errors instead of
|
|
// "variable not found" errors.
|
|
if len(ctx.Variables) == 0 {
|
|
ctx.Variables = nil
|
|
}
|
|
if len(ctx.Functions) == 0 {
|
|
ctx.Functions = nil
|
|
}
|
|
if ctx.Variables == nil && ctx.Functions == nil {
|
|
ctx = nil
|
|
}
|
|
|
|
var bodies []hcl.Body
|
|
|
|
if len(args) == 0 {
|
|
src, err := ioutil.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read stdin: %s", err)
|
|
}
|
|
|
|
f, fDiags := parser.ParseHCL(src, "<stdin>")
|
|
diags = append(diags, fDiags...)
|
|
if !fDiags.HasErrors() {
|
|
bodies = append(bodies, f.Body)
|
|
}
|
|
} else {
|
|
for _, filename := range args {
|
|
var f *hcl.File
|
|
var fDiags hcl.Diagnostics
|
|
if strings.HasSuffix(filename, ".json") {
|
|
f, fDiags = parser.ParseJSONFile(filename)
|
|
} else {
|
|
f, fDiags = parser.ParseHCLFile(filename)
|
|
}
|
|
diags = append(diags, fDiags...)
|
|
if !fDiags.HasErrors() {
|
|
bodies = append(bodies, f.Body)
|
|
}
|
|
}
|
|
}
|
|
|
|
if diags.HasErrors() {
|
|
diagWr.WriteDiagnostics(diags)
|
|
flush(diagWr)
|
|
os.Exit(2)
|
|
}
|
|
|
|
var body hcl.Body
|
|
switch len(bodies) {
|
|
case 0:
|
|
// should never happen, but... okay?
|
|
body = hcl.EmptyBody()
|
|
case 1:
|
|
body = bodies[0]
|
|
default:
|
|
body = hcl.MergeBodies(bodies)
|
|
}
|
|
|
|
if *showVarRefs {
|
|
vars := hcldec.Variables(body, spec)
|
|
return showVarRefsJSON(vars, ctx)
|
|
}
|
|
|
|
val, decDiags := hcldec.Decode(body, spec, ctx)
|
|
diags = append(diags, decDiags...)
|
|
|
|
if diags.HasErrors() {
|
|
diagWr.WriteDiagnostics(diags)
|
|
flush(diagWr)
|
|
os.Exit(2)
|
|
}
|
|
|
|
wantType := val.Type()
|
|
if *withType {
|
|
// We'll instead ask to encode as dynamic, which will make the
|
|
// marshaler include type information.
|
|
wantType = cty.DynamicPseudoType
|
|
}
|
|
out, err := ctyjson.Marshal(val, wantType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// hcldec will include explicit nulls where an ObjectSpec has a spec
|
|
// that refers to a missing item, but that'll probably be annoying for
|
|
// a consumer of our output to deal with so we'll just strip those
|
|
// out and reduce to only the non-null values.
|
|
out = stripJSONNullProperties(out)
|
|
|
|
target := os.Stdout
|
|
if *outputFile != "" {
|
|
target, err = os.OpenFile(*outputFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("can't open %s for writing: %s", *outputFile, err)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(target, "%s\n", out)
|
|
|
|
return nil
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintf(os.Stderr, "usage: hcldec --spec=<spec-file> [options] [hcl-file ...]\n")
|
|
flag.PrintDefaults()
|
|
os.Exit(2)
|
|
}
|
|
|
|
func showVarRefsJSON(vars []hcl.Traversal, ctx *hcl.EvalContext) error {
|
|
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 StepJSON struct {
|
|
Kind string `json:"kind"`
|
|
Name string `json:"name,omitempty"`
|
|
Key json.RawMessage `json:"key,omitempty"`
|
|
Range RangeJSON `json:"range"`
|
|
}
|
|
type TraversalJSON struct {
|
|
RootName string `json:"root_name"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
Steps []StepJSON `json:"steps"`
|
|
Range RangeJSON `json:"range"`
|
|
}
|
|
|
|
ret := make([]TraversalJSON, 0, len(vars))
|
|
for _, traversal := range vars {
|
|
tJSON := TraversalJSON{
|
|
Steps: make([]StepJSON, 0, len(traversal)),
|
|
}
|
|
|
|
for _, step := range traversal {
|
|
var sJSON StepJSON
|
|
rng := step.SourceRange()
|
|
sJSON.Range.Filename = rng.Filename
|
|
sJSON.Range.Start.Line = rng.Start.Line
|
|
sJSON.Range.Start.Column = rng.Start.Column
|
|
sJSON.Range.Start.Byte = rng.Start.Byte
|
|
sJSON.Range.End.Line = rng.End.Line
|
|
sJSON.Range.End.Column = rng.End.Column
|
|
sJSON.Range.End.Byte = rng.End.Byte
|
|
switch ts := step.(type) {
|
|
case hcl.TraverseRoot:
|
|
sJSON.Kind = "root"
|
|
sJSON.Name = ts.Name
|
|
tJSON.RootName = ts.Name
|
|
case hcl.TraverseAttr:
|
|
sJSON.Kind = "attr"
|
|
sJSON.Name = ts.Name
|
|
case hcl.TraverseIndex:
|
|
sJSON.Kind = "index"
|
|
src, err := ctyjson.Marshal(ts.Key, ts.Key.Type())
|
|
if err == nil {
|
|
sJSON.Key = json.RawMessage(src)
|
|
}
|
|
default:
|
|
// Should never get here, since the above should be exhaustive
|
|
// for all possible traversal step types.
|
|
sJSON.Kind = "(unknown)"
|
|
}
|
|
tJSON.Steps = append(tJSON.Steps, sJSON)
|
|
}
|
|
|
|
// Best effort, we'll try to include the current known value of this
|
|
// traversal, if any.
|
|
val, diags := traversal.TraverseAbs(ctx)
|
|
if !diags.HasErrors() {
|
|
enc, err := ctyjson.Marshal(val, val.Type())
|
|
if err == nil {
|
|
tJSON.Value = json.RawMessage(enc)
|
|
}
|
|
}
|
|
|
|
rng := traversal.SourceRange()
|
|
tJSON.Range.Filename = rng.Filename
|
|
tJSON.Range.Start.Line = rng.Start.Line
|
|
tJSON.Range.Start.Column = rng.Start.Column
|
|
tJSON.Range.Start.Byte = rng.Start.Byte
|
|
tJSON.Range.End.Line = rng.End.Line
|
|
tJSON.Range.End.Column = rng.End.Column
|
|
tJSON.Range.End.Byte = rng.End.Byte
|
|
|
|
ret = append(ret, tJSON)
|
|
}
|
|
|
|
out, err := json.MarshalIndent(ret, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal variable references as JSON: %s", err)
|
|
}
|
|
|
|
target := os.Stdout
|
|
if *outputFile != "" {
|
|
target, err = os.OpenFile(*outputFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("can't open %s for writing: %s", *outputFile, err)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(target, "%s\n", out)
|
|
|
|
return nil
|
|
}
|
|
|
|
func stripJSONNullProperties(src []byte) []byte {
|
|
var v interface{}
|
|
err := json.Unmarshal(src, &v)
|
|
if err != nil {
|
|
// We expect valid JSON
|
|
panic(err)
|
|
}
|
|
|
|
v = stripNullMapElements(v)
|
|
|
|
new, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return new
|
|
}
|
|
|
|
func stripNullMapElements(v interface{}) interface{} {
|
|
switch tv := v.(type) {
|
|
case map[string]interface{}:
|
|
for k, ev := range tv {
|
|
if ev == nil {
|
|
delete(tv, k)
|
|
} else {
|
|
tv[k] = stripNullMapElements(ev)
|
|
}
|
|
}
|
|
return v
|
|
case []interface{}:
|
|
for i, ev := range tv {
|
|
tv[i] = stripNullMapElements(ev)
|
|
}
|
|
return v
|
|
default:
|
|
return v
|
|
}
|
|
}
|