github.com/hashicorp/hcl/v2@v2.20.0/cmd/hcldec/main.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"strings"
    13  
    14  	"github.com/hashicorp/hcl/v2"
    15  	"github.com/hashicorp/hcl/v2/hcldec"
    16  	"github.com/hashicorp/hcl/v2/hclparse"
    17  	flag "github.com/spf13/pflag"
    18  	"github.com/zclconf/go-cty/cty"
    19  	"github.com/zclconf/go-cty/cty/function"
    20  	ctyjson "github.com/zclconf/go-cty/cty/json"
    21  	"golang.org/x/crypto/ssh/terminal"
    22  )
    23  
    24  const versionStr = "0.0.1-dev"
    25  
    26  // vars is populated from --vars arguments on the command line, via a flag
    27  // registration in init() below.
    28  var vars = &varSpecs{}
    29  
    30  var (
    31  	specFile    = flag.StringP("spec", "s", "", "path to spec file (required)")
    32  	outputFile  = flag.StringP("out", "o", "", "write to the given file, instead of stdout")
    33  	diagsFormat = flag.StringP("diags", "", "", "format any returned diagnostics in the given format; currently only \"json\" is accepted")
    34  	showVarRefs = flag.BoolP("var-refs", "", false, "rather than decoding input, produce a JSON description of the variables referenced by it")
    35  	withType    = flag.BoolP("with-type", "", false, "include an additional object level at the top describing the HCL-oriented type of the result value")
    36  	showVersion = flag.BoolP("version", "v", false, "show the version number and immediately exit")
    37  	keepNulls   = flag.BoolP("keep-nulls", "", false, "retain object properties that have null as their value (they are removed by default)")
    38  )
    39  
    40  var parser = hclparse.NewParser()
    41  var diagWr hcl.DiagnosticWriter // initialized in init
    42  
    43  func init() {
    44  	flag.VarP(vars, "vars", "V", "provide variables to the given configuration file(s)")
    45  }
    46  
    47  func main() {
    48  	flag.Usage = usage
    49  	flag.Parse()
    50  
    51  	if *showVersion {
    52  		fmt.Println(versionStr)
    53  		os.Exit(0)
    54  	}
    55  
    56  	args := flag.Args()
    57  
    58  	switch *diagsFormat {
    59  	case "":
    60  		color := terminal.IsTerminal(int(os.Stderr.Fd()))
    61  		w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
    62  		if err != nil {
    63  			w = 80
    64  		}
    65  		diagWr = hcl.NewDiagnosticTextWriter(os.Stderr, parser.Files(), uint(w), color)
    66  	case "json":
    67  		diagWr = &jsonDiagWriter{w: os.Stderr}
    68  	default:
    69  		fmt.Fprintf(os.Stderr, "Invalid diagnostics format %q: only \"json\" is supported.\n", *diagsFormat)
    70  		os.Exit(2)
    71  	}
    72  
    73  	err := realmain(args)
    74  
    75  	if err != nil {
    76  		fmt.Fprintf(os.Stderr, "Error: %s\n\n", err.Error())
    77  		os.Exit(1)
    78  	}
    79  }
    80  
    81  func realmain(args []string) error {
    82  
    83  	if *specFile == "" {
    84  		return fmt.Errorf("the --spec=... argument is required")
    85  	}
    86  
    87  	var diags hcl.Diagnostics
    88  
    89  	specContent, specDiags := loadSpecFile(*specFile)
    90  	diags = append(diags, specDiags...)
    91  	if specDiags.HasErrors() {
    92  		diagWr.WriteDiagnostics(diags)
    93  		flush(diagWr)
    94  		os.Exit(2)
    95  	}
    96  
    97  	spec := specContent.RootSpec
    98  
    99  	ctx := &hcl.EvalContext{
   100  		Variables: map[string]cty.Value{},
   101  		Functions: map[string]function.Function{},
   102  	}
   103  	for name, val := range specContent.Variables {
   104  		ctx.Variables[name] = val
   105  	}
   106  	for name, f := range specContent.Functions {
   107  		ctx.Functions[name] = f
   108  	}
   109  	if len(*vars) != 0 {
   110  		for i, varsSpec := range *vars {
   111  			var vals map[string]cty.Value
   112  			var valsDiags hcl.Diagnostics
   113  			if strings.HasPrefix(strings.TrimSpace(varsSpec), "{") {
   114  				// literal JSON object on the command line
   115  				vals, valsDiags = parseVarsArg(varsSpec, i)
   116  			} else {
   117  				// path to a file containing either HCL or JSON (by file extension)
   118  				vals, valsDiags = parseVarsFile(varsSpec)
   119  			}
   120  			diags = append(diags, valsDiags...)
   121  			for k, v := range vals {
   122  				ctx.Variables[k] = v
   123  			}
   124  		}
   125  	}
   126  
   127  	// If we have empty context elements then we'll nil them out so that
   128  	// we'll produce e.g. "variables are not allowed" errors instead of
   129  	// "variable not found" errors.
   130  	if len(ctx.Variables) == 0 {
   131  		ctx.Variables = nil
   132  	}
   133  	if len(ctx.Functions) == 0 {
   134  		ctx.Functions = nil
   135  	}
   136  	if ctx.Variables == nil && ctx.Functions == nil {
   137  		ctx = nil
   138  	}
   139  
   140  	var bodies []hcl.Body
   141  
   142  	if len(args) == 0 {
   143  		src, err := ioutil.ReadAll(os.Stdin)
   144  		if err != nil {
   145  			return fmt.Errorf("failed to read stdin: %s", err)
   146  		}
   147  
   148  		f, fDiags := parser.ParseHCL(src, "<stdin>")
   149  		diags = append(diags, fDiags...)
   150  		if !fDiags.HasErrors() {
   151  			bodies = append(bodies, f.Body)
   152  		}
   153  	} else {
   154  		for _, filename := range args {
   155  			var f *hcl.File
   156  			var fDiags hcl.Diagnostics
   157  			if strings.HasSuffix(filename, ".json") {
   158  				f, fDiags = parser.ParseJSONFile(filename)
   159  			} else {
   160  				f, fDiags = parser.ParseHCLFile(filename)
   161  			}
   162  			diags = append(diags, fDiags...)
   163  			if !fDiags.HasErrors() {
   164  				bodies = append(bodies, f.Body)
   165  			}
   166  		}
   167  	}
   168  
   169  	if diags.HasErrors() {
   170  		diagWr.WriteDiagnostics(diags)
   171  		flush(diagWr)
   172  		os.Exit(2)
   173  	}
   174  
   175  	var body hcl.Body
   176  	switch len(bodies) {
   177  	case 0:
   178  		// should never happen, but... okay?
   179  		body = hcl.EmptyBody()
   180  	case 1:
   181  		body = bodies[0]
   182  	default:
   183  		body = hcl.MergeBodies(bodies)
   184  	}
   185  
   186  	if *showVarRefs {
   187  		vars := hcldec.Variables(body, spec)
   188  		return showVarRefsJSON(vars, ctx)
   189  	}
   190  
   191  	val, decDiags := hcldec.Decode(body, spec, ctx)
   192  	diags = append(diags, decDiags...)
   193  
   194  	if diags.HasErrors() {
   195  		diagWr.WriteDiagnostics(diags)
   196  		flush(diagWr)
   197  		os.Exit(2)
   198  	}
   199  
   200  	wantType := val.Type()
   201  	if *withType {
   202  		// We'll instead ask to encode as dynamic, which will make the
   203  		// marshaler include type information.
   204  		wantType = cty.DynamicPseudoType
   205  	}
   206  	out, err := ctyjson.Marshal(val, wantType)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	// hcldec will include explicit nulls where an ObjectSpec has a spec
   212  	// that refers to a missing item, but that'll probably be annoying for
   213  	// a consumer of our output to deal with so we'll just strip those
   214  	// out and reduce to only the non-null values.
   215  	if !*keepNulls {
   216  		out = stripJSONNullProperties(out)
   217  	}
   218  
   219  	target := os.Stdout
   220  	if *outputFile != "" {
   221  		target, err = os.OpenFile(*outputFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
   222  		if err != nil {
   223  			return fmt.Errorf("can't open %s for writing: %s", *outputFile, err)
   224  		}
   225  	}
   226  
   227  	fmt.Fprintf(target, "%s\n", out)
   228  
   229  	return nil
   230  }
   231  
   232  func usage() {
   233  	fmt.Fprintf(os.Stderr, "usage: hcldec --spec=<spec-file> [options] [hcl-file ...]\n")
   234  	flag.PrintDefaults()
   235  	os.Exit(2)
   236  }
   237  
   238  func showVarRefsJSON(vars []hcl.Traversal, ctx *hcl.EvalContext) error {
   239  	type PosJSON struct {
   240  		Line   int `json:"line"`
   241  		Column int `json:"column"`
   242  		Byte   int `json:"byte"`
   243  	}
   244  	type RangeJSON struct {
   245  		Filename string  `json:"filename"`
   246  		Start    PosJSON `json:"start"`
   247  		End      PosJSON `json:"end"`
   248  	}
   249  	type StepJSON struct {
   250  		Kind  string          `json:"kind"`
   251  		Name  string          `json:"name,omitempty"`
   252  		Key   json.RawMessage `json:"key,omitempty"`
   253  		Range RangeJSON       `json:"range"`
   254  	}
   255  	type TraversalJSON struct {
   256  		RootName string          `json:"root_name"`
   257  		Value    json.RawMessage `json:"value,omitempty"`
   258  		Steps    []StepJSON      `json:"steps"`
   259  		Range    RangeJSON       `json:"range"`
   260  	}
   261  
   262  	ret := make([]TraversalJSON, 0, len(vars))
   263  	for _, traversal := range vars {
   264  		tJSON := TraversalJSON{
   265  			Steps: make([]StepJSON, 0, len(traversal)),
   266  		}
   267  
   268  		for _, step := range traversal {
   269  			var sJSON StepJSON
   270  			rng := step.SourceRange()
   271  			sJSON.Range.Filename = rng.Filename
   272  			sJSON.Range.Start.Line = rng.Start.Line
   273  			sJSON.Range.Start.Column = rng.Start.Column
   274  			sJSON.Range.Start.Byte = rng.Start.Byte
   275  			sJSON.Range.End.Line = rng.End.Line
   276  			sJSON.Range.End.Column = rng.End.Column
   277  			sJSON.Range.End.Byte = rng.End.Byte
   278  			switch ts := step.(type) {
   279  			case hcl.TraverseRoot:
   280  				sJSON.Kind = "root"
   281  				sJSON.Name = ts.Name
   282  				tJSON.RootName = ts.Name
   283  			case hcl.TraverseAttr:
   284  				sJSON.Kind = "attr"
   285  				sJSON.Name = ts.Name
   286  			case hcl.TraverseIndex:
   287  				sJSON.Kind = "index"
   288  				src, err := ctyjson.Marshal(ts.Key, ts.Key.Type())
   289  				if err == nil {
   290  					sJSON.Key = json.RawMessage(src)
   291  				}
   292  			default:
   293  				// Should never get here, since the above should be exhaustive
   294  				// for all possible traversal step types.
   295  				sJSON.Kind = "(unknown)"
   296  			}
   297  			tJSON.Steps = append(tJSON.Steps, sJSON)
   298  		}
   299  
   300  		// Best effort, we'll try to include the current known value of this
   301  		// traversal, if any.
   302  		val, diags := traversal.TraverseAbs(ctx)
   303  		if !diags.HasErrors() {
   304  			enc, err := ctyjson.Marshal(val, val.Type())
   305  			if err == nil {
   306  				tJSON.Value = json.RawMessage(enc)
   307  			}
   308  		}
   309  
   310  		rng := traversal.SourceRange()
   311  		tJSON.Range.Filename = rng.Filename
   312  		tJSON.Range.Start.Line = rng.Start.Line
   313  		tJSON.Range.Start.Column = rng.Start.Column
   314  		tJSON.Range.Start.Byte = rng.Start.Byte
   315  		tJSON.Range.End.Line = rng.End.Line
   316  		tJSON.Range.End.Column = rng.End.Column
   317  		tJSON.Range.End.Byte = rng.End.Byte
   318  
   319  		ret = append(ret, tJSON)
   320  	}
   321  
   322  	out, err := json.MarshalIndent(ret, "", "  ")
   323  	if err != nil {
   324  		return fmt.Errorf("failed to marshal variable references as JSON: %s", err)
   325  	}
   326  
   327  	target := os.Stdout
   328  	if *outputFile != "" {
   329  		target, err = os.OpenFile(*outputFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
   330  		if err != nil {
   331  			return fmt.Errorf("can't open %s for writing: %s", *outputFile, err)
   332  		}
   333  	}
   334  
   335  	fmt.Fprintf(target, "%s\n", out)
   336  
   337  	return nil
   338  }
   339  
   340  func stripJSONNullProperties(src []byte) []byte {
   341  	dec := json.NewDecoder(bytes.NewReader(src))
   342  	dec.UseNumber()
   343  
   344  	var v interface{}
   345  	err := dec.Decode(&v)
   346  	if err != nil {
   347  		// We expect valid JSON
   348  		panic(err)
   349  	}
   350  
   351  	v = stripNullMapElements(v)
   352  
   353  	new, err := json.Marshal(v)
   354  	if err != nil {
   355  		panic(err)
   356  	}
   357  	return new
   358  }
   359  
   360  func stripNullMapElements(v interface{}) interface{} {
   361  	switch tv := v.(type) {
   362  	case map[string]interface{}:
   363  		for k, ev := range tv {
   364  			if ev == nil {
   365  				delete(tv, k)
   366  			} else {
   367  				tv[k] = stripNullMapElements(ev)
   368  			}
   369  		}
   370  		return v
   371  	case []interface{}:
   372  		for i, ev := range tv {
   373  			tv[i] = stripNullMapElements(ev)
   374  		}
   375  		return v
   376  	default:
   377  		return v
   378  	}
   379  }