github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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/configs" 20 "github.com/hashicorp/terraform/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, err := c.Meta.process(args, false) 45 if err != nil { 46 return 1 47 } 48 49 cmdFlags := c.Meta.defaultFlagSet("fmt") 50 cmdFlags.BoolVar(&c.list, "list", true, "list") 51 cmdFlags.BoolVar(&c.write, "write", true, "write") 52 cmdFlags.BoolVar(&c.diff, "diff", false, "diff") 53 cmdFlags.BoolVar(&c.check, "check", false, "check") 54 cmdFlags.BoolVar(&c.recursive, "recursive", false, "recursive") 55 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 56 if err := cmdFlags.Parse(args); err != nil { 57 c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) 58 return 1 59 } 60 61 args = cmdFlags.Args() 62 if len(args) > 1 { 63 c.Ui.Error("The fmt command expects at most one argument.") 64 cmdFlags.Usage() 65 return 1 66 } 67 68 var paths []string 69 if len(args) == 0 { 70 paths = []string{"."} 71 } else if args[0] == stdinArg { 72 c.list = false 73 c.write = false 74 } else { 75 paths = []string{args[0]} 76 } 77 78 var output io.Writer 79 list := c.list // preserve the original value of -list 80 if c.check { 81 // set to true so we can use the list output to check 82 // if the input needs formatting 83 c.list = true 84 c.write = false 85 output = &bytes.Buffer{} 86 } else { 87 output = &cli.UiWriter{Ui: c.Ui} 88 } 89 90 diags := c.fmt(paths, c.input, output) 91 c.showDiagnostics(diags) 92 if diags.HasErrors() { 93 return 2 94 } 95 96 if c.check { 97 buf := output.(*bytes.Buffer) 98 ok := buf.Len() == 0 99 if list { 100 io.Copy(&cli.UiWriter{Ui: c.Ui}, buf) 101 } 102 if ok { 103 return 0 104 } else { 105 return 3 106 } 107 } 108 109 return 0 110 } 111 112 func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdiags.Diagnostics { 113 var diags tfdiags.Diagnostics 114 115 if len(paths) == 0 { // Assuming stdin, then. 116 if c.write { 117 diags = diags.Append(fmt.Errorf("Option -write cannot be used when reading from stdin")) 118 return diags 119 } 120 fileDiags := c.processFile("<stdin>", stdin, stdout, true) 121 diags = diags.Append(fileDiags) 122 return diags 123 } 124 125 for _, path := range paths { 126 path = c.normalizePath(path) 127 info, err := os.Stat(path) 128 if err != nil { 129 diags = diags.Append(fmt.Errorf("No file or directory at %s", path)) 130 return diags 131 } 132 if info.IsDir() { 133 dirDiags := c.processDir(path, stdout) 134 diags = diags.Append(dirDiags) 135 } else { 136 switch filepath.Ext(path) { 137 case ".tf", ".tfvars": 138 f, err := os.Open(path) 139 if err != nil { 140 // Open does not produce error messages that are end-user-appropriate, 141 // so we'll need to simplify here. 142 diags = diags.Append(fmt.Errorf("Failed to read file %s", path)) 143 continue 144 } 145 146 fileDiags := c.processFile(c.normalizePath(path), f, stdout, false) 147 diags = diags.Append(fileDiags) 148 f.Close() 149 default: 150 diags = diags.Append(fmt.Errorf("Only .tf and .tfvars files can be processed with terraform fmt")) 151 continue 152 } 153 } 154 } 155 156 return diags 157 } 158 159 func (c *FmtCommand) processFile(path string, r io.Reader, w io.Writer, isStdout bool) tfdiags.Diagnostics { 160 var diags tfdiags.Diagnostics 161 162 log.Printf("[TRACE] terraform fmt: Formatting %s", path) 163 164 src, err := ioutil.ReadAll(r) 165 if err != nil { 166 diags = diags.Append(fmt.Errorf("Failed to read %s", path)) 167 return diags 168 } 169 170 // File must be parseable as HCL native syntax before we'll try to format 171 // it. If not, the formatter is likely to make drastic changes that would 172 // be hard for the user to undo. 173 _, syntaxDiags := hclsyntax.ParseConfig(src, path, hcl.Pos{Line: 1, Column: 1}) 174 if syntaxDiags.HasErrors() { 175 diags = diags.Append(syntaxDiags) 176 return diags 177 } 178 179 result := hclwrite.Format(src) 180 181 if !bytes.Equal(src, result) { 182 // Something was changed 183 if c.list { 184 fmt.Fprintln(w, path) 185 } 186 if c.write { 187 err := ioutil.WriteFile(path, result, 0644) 188 if err != nil { 189 diags = diags.Append(fmt.Errorf("Failed to write %s", path)) 190 return diags 191 } 192 } 193 if c.diff { 194 diff, err := bytesDiff(src, result, path) 195 if err != nil { 196 diags = diags.Append(fmt.Errorf("Failed to generate diff for %s: %s", path, err)) 197 return diags 198 } 199 w.Write(diff) 200 } 201 } 202 203 if !c.list && !c.write && !c.diff { 204 _, err = w.Write(result) 205 if err != nil { 206 diags = diags.Append(fmt.Errorf("Failed to write result")) 207 } 208 } 209 210 return diags 211 } 212 213 func (c *FmtCommand) processDir(path string, stdout io.Writer) tfdiags.Diagnostics { 214 var diags tfdiags.Diagnostics 215 216 log.Printf("[TRACE] terraform fmt: looking for files in %s", path) 217 218 entries, err := ioutil.ReadDir(path) 219 if err != nil { 220 switch { 221 case os.IsNotExist(err): 222 diags = diags.Append(fmt.Errorf("There is no configuration directory at %s", path)) 223 default: 224 // ReadDir does not produce error messages that are end-user-appropriate, 225 // so we'll need to simplify here. 226 diags = diags.Append(fmt.Errorf("Cannot read directory %s", path)) 227 } 228 return diags 229 } 230 231 for _, info := range entries { 232 name := info.Name() 233 if configs.IsIgnoredFile(name) { 234 continue 235 } 236 subPath := filepath.Join(path, name) 237 if info.IsDir() { 238 if c.recursive { 239 subDiags := c.processDir(subPath, stdout) 240 diags = diags.Append(subDiags) 241 } 242 243 // We do not recurse into child directories by default because we 244 // want to mimic the file-reading behavior of "terraform plan", etc, 245 // operating on one module at a time. 246 continue 247 } 248 249 ext := filepath.Ext(name) 250 switch ext { 251 case ".tf", ".tfvars": 252 f, err := os.Open(subPath) 253 if err != nil { 254 // Open does not produce error messages that are end-user-appropriate, 255 // so we'll need to simplify here. 256 diags = diags.Append(fmt.Errorf("Failed to read file %s", subPath)) 257 continue 258 } 259 260 fileDiags := c.processFile(c.normalizePath(subPath), f, stdout, false) 261 diags = diags.Append(fileDiags) 262 f.Close() 263 } 264 } 265 266 return diags 267 } 268 269 func (c *FmtCommand) Help() string { 270 helpText := ` 271 Usage: terraform fmt [options] [DIR] 272 273 Rewrites all Terraform configuration files to a canonical format. Both 274 configuration files (.tf) and variables files (.tfvars) are updated. 275 JSON files (.tf.json or .tfvars.json) are not modified. 276 277 If DIR is not specified then the current working directory will be used. 278 If DIR is "-" then content will be read from STDIN. The given content must 279 be in the Terraform language native syntax; JSON is not supported. 280 281 Options: 282 283 -list=false Don't list files whose formatting differs 284 (always disabled if using STDIN) 285 286 -write=false Don't write to source files 287 (always disabled if using STDIN or -check) 288 289 -diff Display diffs of formatting changes 290 291 -check Check if the input is formatted. Exit status will be 0 if all 292 input is properly formatted and non-zero otherwise. 293 294 -no-color If specified, output won't contain any color. 295 296 -recursive Also process files in subdirectories. By default, only the 297 given directory (or current directory) is processed. 298 ` 299 return strings.TrimSpace(helpText) 300 } 301 302 func (c *FmtCommand) Synopsis() string { 303 return "Rewrites config files to canonical format" 304 } 305 306 func bytesDiff(b1, b2 []byte, path string) (data []byte, err error) { 307 f1, err := ioutil.TempFile("", "") 308 if err != nil { 309 return 310 } 311 defer os.Remove(f1.Name()) 312 defer f1.Close() 313 314 f2, err := ioutil.TempFile("", "") 315 if err != nil { 316 return 317 } 318 defer os.Remove(f2.Name()) 319 defer f2.Close() 320 321 f1.Write(b1) 322 f2.Write(b2) 323 324 data, err = exec.Command("diff", "--label=old/"+path, "--label=new/"+path, "-u", f1.Name(), f2.Name()).CombinedOutput() 325 if len(data) > 0 { 326 // diff exits with a non-zero status when the files don't match. 327 // Ignore that failure as long as we get output. 328 err = nil 329 } 330 return 331 }