github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/fmt.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "log" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 14 "github.com/hashicorp/hcl/v2" 15 "github.com/hashicorp/hcl/v2/hclsyntax" 16 "github.com/hashicorp/hcl/v2/hclwrite" 17 "github.com/mitchellh/cli" 18 19 "github.com/hashicorp/terraform/internal/configs" 20 "github.com/hashicorp/terraform/internal/tfdiags" 21 ) 22 23 const ( 24 stdinArg = "-" 25 ) 26 27 // FmtCommand is a Command implementation that rewrites Terraform config 28 // files to a canonical format and style. 29 type FmtCommand struct { 30 Meta 31 list bool 32 write bool 33 diff bool 34 check bool 35 recursive bool 36 input io.Reader // STDIN if nil 37 } 38 39 func (c *FmtCommand) Run(args []string) int { 40 if c.input == nil { 41 c.input = os.Stdin 42 } 43 44 args = c.Meta.process(args) 45 cmdFlags := c.Meta.defaultFlagSet("fmt") 46 cmdFlags.BoolVar(&c.list, "list", true, "list") 47 cmdFlags.BoolVar(&c.write, "write", true, "write") 48 cmdFlags.BoolVar(&c.diff, "diff", false, "diff") 49 cmdFlags.BoolVar(&c.check, "check", false, "check") 50 cmdFlags.BoolVar(&c.recursive, "recursive", false, "recursive") 51 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 52 if err := cmdFlags.Parse(args); err != nil { 53 c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) 54 return 1 55 } 56 57 args = cmdFlags.Args() 58 59 var paths []string 60 if len(args) == 0 { 61 paths = []string{"."} 62 } else if args[0] == stdinArg { 63 c.list = false 64 c.write = false 65 } else { 66 paths = args 67 } 68 69 var output io.Writer 70 list := c.list // preserve the original value of -list 71 if c.check { 72 // set to true so we can use the list output to check 73 // if the input needs formatting 74 c.list = true 75 c.write = false 76 output = &bytes.Buffer{} 77 } else { 78 output = &cli.UiWriter{Ui: c.Ui} 79 } 80 81 diags := c.fmt(paths, c.input, output) 82 c.showDiagnostics(diags) 83 if diags.HasErrors() { 84 return 2 85 } 86 87 if c.check { 88 buf := output.(*bytes.Buffer) 89 ok := buf.Len() == 0 90 if list { 91 io.Copy(&cli.UiWriter{Ui: c.Ui}, buf) 92 } 93 if ok { 94 return 0 95 } else { 96 return 3 97 } 98 } 99 100 return 0 101 } 102 103 func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdiags.Diagnostics { 104 var diags tfdiags.Diagnostics 105 106 if len(paths) == 0 { // Assuming stdin, then. 107 if c.write { 108 diags = diags.Append(fmt.Errorf("Option -write cannot be used when reading from stdin")) 109 return diags 110 } 111 fileDiags := c.processFile("<stdin>", stdin, stdout, true) 112 diags = diags.Append(fileDiags) 113 return diags 114 } 115 116 for _, path := range paths { 117 path = c.normalizePath(path) 118 info, err := os.Stat(path) 119 if err != nil { 120 diags = diags.Append(fmt.Errorf("No file or directory at %s", path)) 121 return diags 122 } 123 if info.IsDir() { 124 dirDiags := c.processDir(path, stdout) 125 diags = diags.Append(dirDiags) 126 } else { 127 switch filepath.Ext(path) { 128 case ".tf", ".tfvars": 129 f, err := os.Open(path) 130 if err != nil { 131 // Open does not produce error messages that are end-user-appropriate, 132 // so we'll need to simplify here. 133 diags = diags.Append(fmt.Errorf("Failed to read file %s", path)) 134 continue 135 } 136 137 fileDiags := c.processFile(c.normalizePath(path), f, stdout, false) 138 diags = diags.Append(fileDiags) 139 f.Close() 140 default: 141 diags = diags.Append(fmt.Errorf("Only .tf and .tfvars files can be processed with terraform fmt")) 142 continue 143 } 144 } 145 } 146 147 return diags 148 } 149 150 func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, isStdout bool) tfdiags.Diagnostics { 151 var diags tfdiags.Diagnostics 152 153 log.Printf("[TRACE] terraform fmt: Formatting %s", path) 154 155 src, err := ioutil.ReadAll(r) 156 if err != nil { 157 diags = diags.Append(fmt.Errorf("Failed to read %s", path)) 158 return diags 159 } 160 161 // Register this path as a synthetic configuration source, so that any 162 // diagnostic errors can include the source code snippet 163 c.registerSynthConfigSource(path, src) 164 165 // File must be parseable as HCL native syntax before we'll try to format 166 // it. If not, the formatter is likely to make drastic changes that would 167 // be hard for the user to undo. 168 _, syntaxDiags := hclsyntax.ParseConfig(src, path, hcl.Pos{Line: 1, Column: 1}) 169 if syntaxDiags.HasErrors() { 170 diags = diags.Append(syntaxDiags) 171 return diags 172 } 173 174 result := c.formatSourceCode(src, path) 175 176 if !bytes.Equal(src, result) { 177 // Something was changed 178 if c.list { 179 fmt.Fprintln(w, path) 180 } 181 if c.write { 182 err := ioutil.WriteFile(path, result, 0644) 183 if err != nil { 184 diags = diags.Append(fmt.Errorf("Failed to write %s", path)) 185 return diags 186 } 187 } 188 if c.diff { 189 diff, err := bytesDiff(src, result, path) 190 if err != nil { 191 diags = diags.Append(fmt.Errorf("Failed to generate diff for %s: %s", path, err)) 192 return diags 193 } 194 w.Write(diff) 195 } 196 } 197 198 if !c.list && !c.write && !c.diff { 199 _, err = w.Write(result) 200 if err != nil { 201 diags = diags.Append(fmt.Errorf("Failed to write result")) 202 } 203 } 204 205 return diags 206 } 207 208 func (c *FmtCommand) processDir(path string, stdout io.Writer) tfdiags.Diagnostics { 209 var diags tfdiags.Diagnostics 210 211 log.Printf("[TRACE] terraform fmt: looking for files in %s", path) 212 213 entries, err := ioutil.ReadDir(path) 214 if err != nil { 215 switch { 216 case os.IsNotExist(err): 217 diags = diags.Append(fmt.Errorf("There is no configuration directory at %s", path)) 218 default: 219 // ReadDir does not produce error messages that are end-user-appropriate, 220 // so we'll need to simplify here. 221 diags = diags.Append(fmt.Errorf("Cannot read directory %s", path)) 222 } 223 return diags 224 } 225 226 for _, info := range entries { 227 name := info.Name() 228 if configs.IsIgnoredFile(name) { 229 continue 230 } 231 subPath := filepath.Join(path, name) 232 if info.IsDir() { 233 if c.recursive { 234 subDiags := c.processDir(subPath, stdout) 235 diags = diags.Append(subDiags) 236 } 237 238 // We do not recurse into child directories by default because we 239 // want to mimic the file-reading behavior of "terraform plan", etc, 240 // operating on one module at a time. 241 continue 242 } 243 244 ext := filepath.Ext(name) 245 switch ext { 246 case ".tf", ".tfvars": 247 f, err := os.Open(subPath) 248 if err != nil { 249 // Open does not produce error messages that are end-user-appropriate, 250 // so we'll need to simplify here. 251 diags = diags.Append(fmt.Errorf("Failed to read file %s", subPath)) 252 continue 253 } 254 255 fileDiags := c.processFile(c.normalizePath(subPath), f, stdout, false) 256 diags = diags.Append(fileDiags) 257 f.Close() 258 } 259 } 260 261 return diags 262 } 263 264 // formatSourceCode is the formatting logic itself, applied to each file that 265 // is selected (directly or indirectly) on the command line. 266 func (c *FmtCommand) formatSourceCode(src []byte, filename string) []byte { 267 f, diags := hclwrite.ParseConfig(src, filename, hcl.InitialPos) 268 if diags.HasErrors() { 269 // It would be weird to get here because the caller should already have 270 // checked for syntax errors and returned them. We'll just do nothing 271 // in this case, returning the input exactly as given. 272 return src 273 } 274 275 c.formatBody(f.Body(), nil) 276 277 return f.Bytes() 278 } 279 280 func (c *FmtCommand) formatBody(body *hclwrite.Body, inBlocks []string) { 281 attrs := body.Attributes() 282 for name, attr := range attrs { 283 if len(inBlocks) == 1 && inBlocks[0] == "variable" && name == "type" { 284 cleanedExprTokens := c.formatTypeExpr(attr.Expr().BuildTokens(nil)) 285 body.SetAttributeRaw(name, cleanedExprTokens) 286 continue 287 } 288 cleanedExprTokens := c.formatValueExpr(attr.Expr().BuildTokens(nil)) 289 body.SetAttributeRaw(name, cleanedExprTokens) 290 } 291 292 blocks := body.Blocks() 293 for _, block := range blocks { 294 // Normalize the label formatting, removing any weird stuff like 295 // interleaved inline comments and using the idiomatic quoted 296 // label syntax. 297 block.SetLabels(block.Labels()) 298 299 inBlocks := append(inBlocks, block.Type()) 300 c.formatBody(block.Body(), inBlocks) 301 } 302 } 303 304 func (c *FmtCommand) formatValueExpr(tokens hclwrite.Tokens) hclwrite.Tokens { 305 if len(tokens) < 5 { 306 // Can't possibly be a "${ ... }" sequence without at least enough 307 // tokens for the delimiters and one token inside them. 308 return tokens 309 } 310 oQuote := tokens[0] 311 oBrace := tokens[1] 312 cBrace := tokens[len(tokens)-2] 313 cQuote := tokens[len(tokens)-1] 314 if oQuote.Type != hclsyntax.TokenOQuote || oBrace.Type != hclsyntax.TokenTemplateInterp || cBrace.Type != hclsyntax.TokenTemplateSeqEnd || cQuote.Type != hclsyntax.TokenCQuote { 315 // Not an interpolation sequence at all, then. 316 return tokens 317 } 318 319 inside := tokens[2 : len(tokens)-2] 320 321 // We're only interested in sequences that are provable to be single 322 // interpolation sequences, which we'll determine by hunting inside 323 // the interior tokens for any other interpolation sequences. This is 324 // likely to produce false negatives sometimes, but that's better than 325 // false positives and we're mainly interested in catching the easy cases 326 // here. 327 quotes := 0 328 for _, token := range inside { 329 if token.Type == hclsyntax.TokenOQuote { 330 quotes++ 331 continue 332 } 333 if token.Type == hclsyntax.TokenCQuote { 334 quotes-- 335 continue 336 } 337 if quotes > 0 { 338 // Interpolation sequences inside nested quotes are okay, because 339 // they are part of a nested expression. 340 // "${foo("${bar}")}" 341 continue 342 } 343 if token.Type == hclsyntax.TokenTemplateInterp || token.Type == hclsyntax.TokenTemplateSeqEnd { 344 // We've found another template delimiter within our interior 345 // tokens, which suggests that we've found something like this: 346 // "${foo}${bar}" 347 // That isn't unwrappable, so we'll leave the whole expression alone. 348 return tokens 349 } 350 if token.Type == hclsyntax.TokenQuotedLit { 351 // If there's any literal characters in the outermost 352 // quoted sequence then it is not unwrappable. 353 return tokens 354 } 355 } 356 357 // If we got down here without an early return then this looks like 358 // an unwrappable sequence, but we'll trim any leading and trailing 359 // newlines that might result in an invalid result if we were to 360 // naively trim something like this: 361 // "${ 362 // foo 363 // }" 364 trimmed := c.trimNewlines(inside) 365 366 // Finally, we check if the unwrapped expression is on multiple lines. If 367 // so, we ensure that it is surrounded by parenthesis to make sure that it 368 // parses correctly after unwrapping. This may be redundant in some cases, 369 // but is required for at least multi-line ternary expressions. 370 isMultiLine := false 371 hasLeadingParen := false 372 hasTrailingParen := false 373 for i, token := range trimmed { 374 switch { 375 case i == 0 && token.Type == hclsyntax.TokenOParen: 376 hasLeadingParen = true 377 case token.Type == hclsyntax.TokenNewline: 378 isMultiLine = true 379 case i == len(trimmed)-1 && token.Type == hclsyntax.TokenCParen: 380 hasTrailingParen = true 381 } 382 } 383 if isMultiLine && !(hasLeadingParen && hasTrailingParen) { 384 wrapped := make(hclwrite.Tokens, 0, len(trimmed)+2) 385 wrapped = append(wrapped, &hclwrite.Token{ 386 Type: hclsyntax.TokenOParen, 387 Bytes: []byte("("), 388 }) 389 wrapped = append(wrapped, trimmed...) 390 wrapped = append(wrapped, &hclwrite.Token{ 391 Type: hclsyntax.TokenCParen, 392 Bytes: []byte(")"), 393 }) 394 395 return wrapped 396 } 397 398 return trimmed 399 } 400 401 func (c *FmtCommand) formatTypeExpr(tokens hclwrite.Tokens) hclwrite.Tokens { 402 switch len(tokens) { 403 case 1: 404 kwTok := tokens[0] 405 if kwTok.Type != hclsyntax.TokenIdent { 406 // Not a single type keyword, then. 407 return tokens 408 } 409 410 // Collection types without an explicit element type mean 411 // the element type is "any", so we'll normalize that. 412 switch string(kwTok.Bytes) { 413 case "list", "map", "set": 414 return hclwrite.Tokens{ 415 kwTok, 416 { 417 Type: hclsyntax.TokenOParen, 418 Bytes: []byte("("), 419 }, 420 { 421 Type: hclsyntax.TokenIdent, 422 Bytes: []byte("any"), 423 }, 424 { 425 Type: hclsyntax.TokenCParen, 426 Bytes: []byte(")"), 427 }, 428 } 429 default: 430 return tokens 431 } 432 433 case 3: 434 // A pre-0.12 legacy quoted string type, like "string". 435 oQuote := tokens[0] 436 strTok := tokens[1] 437 cQuote := tokens[2] 438 if oQuote.Type != hclsyntax.TokenOQuote || strTok.Type != hclsyntax.TokenQuotedLit || cQuote.Type != hclsyntax.TokenCQuote { 439 // Not a quoted string sequence, then. 440 return tokens 441 } 442 443 // Because this quoted syntax is from Terraform 0.11 and 444 // earlier, which didn't have the idea of "any" as an, 445 // element type, we use string as the default element 446 // type. That will avoid oddities if somehow the configuration 447 // was relying on numeric values being auto-converted to 448 // string, as 0.11 would do. This mimicks what terraform 449 // 0.12upgrade used to do, because we'd found real-world 450 // modules that were depending on the auto-stringing.) 451 switch string(strTok.Bytes) { 452 case "string": 453 return hclwrite.Tokens{ 454 { 455 Type: hclsyntax.TokenIdent, 456 Bytes: []byte("string"), 457 }, 458 } 459 case "list": 460 return hclwrite.Tokens{ 461 { 462 Type: hclsyntax.TokenIdent, 463 Bytes: []byte("list"), 464 }, 465 { 466 Type: hclsyntax.TokenOParen, 467 Bytes: []byte("("), 468 }, 469 { 470 Type: hclsyntax.TokenIdent, 471 Bytes: []byte("string"), 472 }, 473 { 474 Type: hclsyntax.TokenCParen, 475 Bytes: []byte(")"), 476 }, 477 } 478 case "map": 479 return hclwrite.Tokens{ 480 { 481 Type: hclsyntax.TokenIdent, 482 Bytes: []byte("map"), 483 }, 484 { 485 Type: hclsyntax.TokenOParen, 486 Bytes: []byte("("), 487 }, 488 { 489 Type: hclsyntax.TokenIdent, 490 Bytes: []byte("string"), 491 }, 492 { 493 Type: hclsyntax.TokenCParen, 494 Bytes: []byte(")"), 495 }, 496 } 497 default: 498 // Something else we're not expecting, then. 499 return tokens 500 } 501 default: 502 return tokens 503 } 504 } 505 506 func (c *FmtCommand) trimNewlines(tokens hclwrite.Tokens) hclwrite.Tokens { 507 if len(tokens) == 0 { 508 return nil 509 } 510 var start, end int 511 for start = 0; start < len(tokens); start++ { 512 if tokens[start].Type != hclsyntax.TokenNewline { 513 break 514 } 515 } 516 for end = len(tokens); end > 0; end-- { 517 if tokens[end-1].Type != hclsyntax.TokenNewline { 518 break 519 } 520 } 521 return tokens[start:end] 522 } 523 524 func (c *FmtCommand) Help() string { 525 helpText := ` 526 Usage: terraform [global options] fmt [options] [target...] 527 528 Rewrites all Terraform configuration files to a canonical format. Both 529 configuration files (.tf) and variables files (.tfvars) are updated. 530 JSON files (.tf.json or .tfvars.json) are not modified. 531 532 By default, fmt scans the current directory for configuration files. If you 533 provide a directory for the target argument, then fmt will scan that 534 directory instead. If you provide a file, then fmt will process just that 535 file. If you provide a single dash ("-"), then fmt will read from standard 536 input (STDIN). 537 538 The content must be in the Terraform language native syntax; JSON is not 539 supported. 540 541 Options: 542 543 -list=false Don't list files whose formatting differs 544 (always disabled if using STDIN) 545 546 -write=false Don't write to source files 547 (always disabled if using STDIN or -check) 548 549 -diff Display diffs of formatting changes 550 551 -check Check if the input is formatted. Exit status will be 0 if all 552 input is properly formatted and non-zero otherwise. 553 554 -no-color If specified, output won't contain any color. 555 556 -recursive Also process files in subdirectories. By default, only the 557 given directory (or current directory) is processed. 558 ` 559 return strings.TrimSpace(helpText) 560 } 561 562 func (c *FmtCommand) Synopsis() string { 563 return "Reformat your configuration in the standard style" 564 } 565 566 func bytesDiff(b1, b2 []byte, path string) (data []byte, err error) { 567 f1, err := ioutil.TempFile("", "") 568 if err != nil { 569 return 570 } 571 defer os.Remove(f1.Name()) 572 defer f1.Close() 573 574 f2, err := ioutil.TempFile("", "") 575 if err != nil { 576 return 577 } 578 defer os.Remove(f2.Name()) 579 defer f2.Close() 580 581 f1.Write(b1) 582 f2.Write(b2) 583 584 data, err = exec.Command("diff", "--label=old/"+path, "--label=new/"+path, "-u", f1.Name(), f2.Name()).CombinedOutput() 585 if len(data) > 0 { 586 // diff exits with a non-zero status when the files don't match. 587 // Ignore that failure as long as we get output. 588 err = nil 589 } 590 return 591 }