2018-02-03 23:37:11 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
2019-09-09 23:08:19 +00:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
2019-09-09 22:39:31 +00:00
|
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
2018-02-03 23:37:11 +00:00
|
|
|
flag "github.com/spf13/pflag"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
2018-02-04 19:05:23 +00:00
|
|
|
"github.com/zclconf/go-cty/cty/function"
|
2018-02-03 23:37:11 +00:00
|
|
|
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")
|
2018-08-10 01:10:00 +00:00
|
|
|
diagsFormat = flag.StringP("diags", "", "", "format any returned diagnostics in the given format; currently only \"json\" is accepted")
|
2018-08-10 00:40:14 +00:00
|
|
|
showVarRefs = flag.BoolP("var-refs", "", false, "rather than decoding input, produce a JSON description of the variables referenced by it")
|
2018-08-10 15:49:43 +00:00
|
|
|
withType = flag.BoolP("with-type", "", false, "include an additional object level at the top describing the HCL-oriented type of the result value")
|
2018-02-03 23:37:11 +00:00
|
|
|
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()
|
|
|
|
|
2018-08-10 01:10:00 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-02-03 23:37:11 +00:00
|
|
|
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
|
|
|
|
|
2018-02-04 19:05:23 +00:00
|
|
|
specContent, specDiags := loadSpecFile(*specFile)
|
2018-02-03 23:37:11 +00:00
|
|
|
diags = append(diags, specDiags...)
|
|
|
|
if specDiags.HasErrors() {
|
|
|
|
diagWr.WriteDiagnostics(diags)
|
2018-08-10 01:10:00 +00:00
|
|
|
flush(diagWr)
|
2018-02-03 23:37:11 +00:00
|
|
|
os.Exit(2)
|
|
|
|
}
|
|
|
|
|
2018-02-04 19:05:23 +00:00
|
|
|
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
|
|
|
|
}
|
2018-02-03 23:37:11 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-04 19:05:23 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2018-02-03 23:37:11 +00:00
|
|
|
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 {
|
2018-08-10 15:49:43 +00:00
|
|
|
var f *hcl.File
|
|
|
|
var fDiags hcl.Diagnostics
|
|
|
|
if strings.HasSuffix(filename, ".json") {
|
|
|
|
f, fDiags = parser.ParseJSONFile(filename)
|
|
|
|
} else {
|
|
|
|
f, fDiags = parser.ParseHCLFile(filename)
|
|
|
|
}
|
2018-02-03 23:37:11 +00:00
|
|
|
diags = append(diags, fDiags...)
|
|
|
|
if !fDiags.HasErrors() {
|
|
|
|
bodies = append(bodies, f.Body)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
diagWr.WriteDiagnostics(diags)
|
2018-08-10 01:10:00 +00:00
|
|
|
flush(diagWr)
|
2018-02-03 23:37:11 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-08-10 00:40:14 +00:00
|
|
|
if *showVarRefs {
|
|
|
|
vars := hcldec.Variables(body, spec)
|
|
|
|
return showVarRefsJSON(vars, ctx)
|
|
|
|
}
|
|
|
|
|
2018-02-03 23:37:11 +00:00
|
|
|
val, decDiags := hcldec.Decode(body, spec, ctx)
|
|
|
|
diags = append(diags, decDiags...)
|
|
|
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
diagWr.WriteDiagnostics(diags)
|
2018-08-10 01:10:00 +00:00
|
|
|
flush(diagWr)
|
2018-02-03 23:37:11 +00:00
|
|
|
os.Exit(2)
|
|
|
|
}
|
|
|
|
|
2018-08-10 15:49:43 +00:00
|
|
|
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)
|
2018-02-03 23:37:11 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-08-10 00:40:14 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-02-03 23:37:11 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|