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 }