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  }