From cc41c7cfa03f05717374055e60c5761466e76e5e Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Tue, 2 Feb 2016 22:10:22 +0000 Subject: [PATCH] command/fmt: Add new fmt command This uses the `fmtcmd` package which has recently been merged into HCL. Per the usage text, this rewrites Terraform config files to their canonical formatting and style. Some notes about the implementation for this initial commit: - all of the fmtcmd options are exposed as CLI flags - it operates on all files that have a `.tf` suffix - it currently only operates on the working directory and doesn't accept a directory argument, but I'll extend this in subsequent commits - output is proxied through `cli.UiWriter` so that we write in the same way as other commands and we can capture the output during tests - the test uses a very simple fixture just to ensure that it is working correctly end-to-end; the fmtcmd package has more exhaustive tests - we have to write the fixture to a file in a temporary directory because it will be modified and for this reason it was easier to define the fixture contents as a raw string --- Godeps/Godeps.json | 8 + command/fmt.go | 75 +++ command/fmt_test.go | 121 ++++ commands.go | 6 + .../hashicorp/hcl/hcl/fmtcmd/fmtcmd.go | 164 +++++ .../hashicorp/hcl/hcl/printer/nodes.go | 575 ++++++++++++++++++ .../hashicorp/hcl/hcl/printer/printer.go | 64 ++ .../source/docs/commands/fmt.html.markdown | 24 + website/source/layouts/docs.erb | 4 + 9 files changed, 1041 insertions(+) create mode 100644 command/fmt.go create mode 100644 command/fmt_test.go create mode 100644 vendor/github.com/hashicorp/hcl/hcl/fmtcmd/fmtcmd.go create mode 100644 vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go create mode 100644 vendor/github.com/hashicorp/hcl/hcl/printer/printer.go create mode 100644 website/source/docs/commands/fmt.html.markdown diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 42d88c119..1ebe088b9 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -641,10 +641,18 @@ "ImportPath": "github.com/hashicorp/hcl/hcl/ast", "Rev": "71c7409f1abba841e528a80556ed2c67671744c3" }, + { + "ImportPath": "github.com/hashicorp/hcl/hcl/fmtcmd", + "Rev": "71c7409f1abba841e528a80556ed2c67671744c3" + }, { "ImportPath": "github.com/hashicorp/hcl/hcl/parser", "Rev": "71c7409f1abba841e528a80556ed2c67671744c3" }, + { + "ImportPath": "github.com/hashicorp/hcl/hcl/printer", + "Rev": "71c7409f1abba841e528a80556ed2c67671744c3" + }, { "ImportPath": "github.com/hashicorp/hcl/hcl/scanner", "Rev": "71c7409f1abba841e528a80556ed2c67671744c3" diff --git a/command/fmt.go b/command/fmt.go new file mode 100644 index 000000000..e4e9aad2f --- /dev/null +++ b/command/fmt.go @@ -0,0 +1,75 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/hcl/hcl/fmtcmd" + "github.com/mitchellh/cli" +) + +const ( + fileExtension = "tf" +) + +// FmtCommand is a Command implementation that rewrites Terraform config +// files to a canonical format and style. +type FmtCommand struct { + Meta + opts fmtcmd.Options +} + +func (c *FmtCommand) Run(args []string) int { + args = c.Meta.process(args, false) + + cmdFlags := flag.NewFlagSet("fmt", flag.ContinueOnError) + cmdFlags.BoolVar(&c.opts.List, "list", false, "list") + cmdFlags.BoolVar(&c.opts.Write, "write", false, "write") + cmdFlags.BoolVar(&c.opts.Diff, "diff", false, "diff") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + args = cmdFlags.Args() + if len(args) > 0 { + c.Ui.Error("The fmt command expects no arguments.") + cmdFlags.Usage() + return 1 + } + + dir := "." + output := &cli.UiWriter{Ui: c.Ui} + err := fmtcmd.Run([]string{dir}, []string{fileExtension}, nil, output, c.opts) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error running fmt: %s", err)) + return 2 + } + + return 0 +} + +func (c *FmtCommand) Help() string { + helpText := ` +Usage: terraform fmt [options] + + Rewrites all Terraform configuration files in the current working + directory to a canonical format. + +Options: + + -list List files whose formatting differs + + -write Write result to source file instead of STDOUT + + -diff Display diffs instead of rewriting files + +` + return strings.TrimSpace(helpText) +} + +func (c *FmtCommand) Synopsis() string { + return "Rewrites config files to canonical format" +} diff --git a/command/fmt_test.go b/command/fmt_test.go new file mode 100644 index 000000000..3d284e466 --- /dev/null +++ b/command/fmt_test.go @@ -0,0 +1,121 @@ +package command + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func TestFmt_errorReporting(t *testing.T) { + tempDir, err := fmtFixtureWriteDir() + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tempDir) + + ui := new(cli.MockUi) + c := &FmtCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + dummy_file := filepath.Join(tempDir, "doesnotexist") + args := []string{dummy_file} + if code := c.Run(args); code != 2 { + t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String()) + } + + expected := fmt.Sprintf("Error running fmt: stat %s: no such file or directory", dummy_file) + if actual := ui.ErrorWriter.String(); !strings.Contains(actual, expected) { + t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) + } +} + +func TestFmt_tooManyArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &FmtCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{"bad"} + if code := c.Run(args); code != 1 { + t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String()) + } + + expected := "The fmt command expects no arguments." + if actual := ui.ErrorWriter.String(); !strings.Contains(actual, expected) { + t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) + } +} + +func TestFmt_workingDirectory(t *testing.T) { + tempDir, err := fmtFixtureWriteDir() + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tempDir) + + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + err = os.Chdir(tempDir) + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + ui := new(cli.MockUi) + c := &FmtCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String()) + } + + expected := fmtFixture.golden + if actual := ui.OutputWriter.Bytes(); !bytes.Equal(actual, expected) { + t.Fatalf("got: %q\nexpected: %q", actual, expected) + } +} + +var fmtFixture = struct { + filename string + input, golden []byte +}{ + "main.tf", + []byte(` foo = "bar" +`), + []byte(`foo = "bar" +`), +} + +func fmtFixtureWriteDir() (string, error) { + dir, err := ioutil.TempDir("", "tf") + if err != nil { + return "", err + } + + err = ioutil.WriteFile(filepath.Join(dir, fmtFixture.filename), fmtFixture.input, 0644) + if err != nil { + os.RemoveAll(dir) + return "", err + } + + return dir, nil +} diff --git a/commands.go b/commands.go index 45783d3a5..000200213 100644 --- a/commands.go +++ b/commands.go @@ -50,6 +50,12 @@ func init() { }, nil }, + "fmt": func() (cli.Command, error) { + return &command.FmtCommand{ + Meta: meta, + }, nil + }, + "get": func() (cli.Command, error) { return &command.GetCommand{ Meta: meta, diff --git a/vendor/github.com/hashicorp/hcl/hcl/fmtcmd/fmtcmd.go b/vendor/github.com/hashicorp/hcl/hcl/fmtcmd/fmtcmd.go new file mode 100644 index 000000000..afc1e4eb1 --- /dev/null +++ b/vendor/github.com/hashicorp/hcl/hcl/fmtcmd/fmtcmd.go @@ -0,0 +1,164 @@ +// Derivative work from: +// - https://golang.org/src/cmd/gofmt/gofmt.go +// - https://github.com/fatih/hclfmt + +package fmtcmd + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/hashicorp/hcl/hcl/printer" +) + +var ( + ErrWriteStdin = errors.New("cannot use write option with standard input") +) + +type Options struct { + List bool // list files whose formatting differs + Write bool // write result to (source) file instead of stdout + Diff bool // display diffs instead of rewriting files +} + +func isValidFile(f os.FileInfo, extensions []string) bool { + if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") { + for _, ext := range extensions { + if strings.HasSuffix(f.Name(), "."+ext) { + return true + } + } + } + + return false +} + +// If in == nil, the source is the contents of the file with the given filename. +func processFile(filename string, in io.Reader, out io.Writer, stdin bool, opts Options) error { + if in == nil { + f, err := os.Open(filename) + if err != nil { + return err + } + defer f.Close() + in = f + } + + src, err := ioutil.ReadAll(in) + if err != nil { + return err + } + + res, err := printer.Format(src) + if err != nil { + return err + } + // Files should end with newlines + res = append(res, []byte("\n")...) + + if !bytes.Equal(src, res) { + // formatting has changed + if opts.List { + fmt.Fprintln(out, filename) + } + if opts.Write { + err = ioutil.WriteFile(filename, res, 0644) + if err != nil { + return err + } + } + if opts.Diff { + data, err := diff(src, res) + if err != nil { + return fmt.Errorf("computing diff: %s", err) + } + fmt.Fprintf(out, "diff a/%s b/%s\n", filename, filename) + out.Write(data) + } + } + + if !opts.List && !opts.Write && !opts.Diff { + _, err = out.Write(res) + } + + return err +} + +func walkDir(path string, extensions []string, stdout io.Writer, opts Options) error { + visitFile := func(path string, f os.FileInfo, err error) error { + if err == nil && isValidFile(f, extensions) { + err = processFile(path, nil, stdout, false, opts) + } + return err + } + + return filepath.Walk(path, visitFile) +} + +func Run( + paths, extensions []string, + stdin io.Reader, + stdout io.Writer, + opts Options, +) error { + if len(paths) == 0 { + if opts.Write { + return ErrWriteStdin + } + if err := processFile("", stdin, stdout, true, opts); err != nil { + return err + } + return nil + } + + for _, path := range paths { + switch dir, err := os.Stat(path); { + case err != nil: + return err + case dir.IsDir(): + if err := walkDir(path, extensions, stdout, opts); err != nil { + return err + } + default: + if err := processFile(path, nil, stdout, false, opts); err != nil { + return err + } + } + } + + return nil +} + +func diff(b1, b2 []byte) (data []byte, err error) { + f1, err := ioutil.TempFile("", "") + if err != nil { + return + } + defer os.Remove(f1.Name()) + defer f1.Close() + + f2, err := ioutil.TempFile("", "") + if err != nil { + return + } + defer os.Remove(f2.Name()) + defer f2.Close() + + f1.Write(b1) + f2.Write(b2) + + data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput() + if len(data) > 0 { + // diff exits with a non-zero status when the files don't match. + // Ignore that failure as long as we get output. + err = nil + } + return +} diff --git a/vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go b/vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go new file mode 100644 index 000000000..a98495c76 --- /dev/null +++ b/vendor/github.com/hashicorp/hcl/hcl/printer/nodes.go @@ -0,0 +1,575 @@ +package printer + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/hcl/hcl/token" +) + +const ( + blank = byte(' ') + newline = byte('\n') + tab = byte('\t') + infinity = 1 << 30 // offset or line +) + +var ( + unindent = []byte("\uE123") // in the private use space +) + +type printer struct { + cfg Config + prev token.Pos + + comments []*ast.CommentGroup // may be nil, contains all comments + standaloneComments []*ast.CommentGroup // contains all standalone comments (not assigned to any node) + + enableTrace bool + indentTrace int +} + +type ByPosition []*ast.CommentGroup + +func (b ByPosition) Len() int { return len(b) } +func (b ByPosition) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b ByPosition) Less(i, j int) bool { return b[i].Pos().Before(b[j].Pos()) } + +// collectComments comments all standalone comments which are not lead or line +// comment +func (p *printer) collectComments(node ast.Node) { + // first collect all comments. This is already stored in + // ast.File.(comments) + ast.Walk(node, func(nn ast.Node) (ast.Node, bool) { + switch t := nn.(type) { + case *ast.File: + p.comments = t.Comments + return nn, false + } + return nn, true + }) + + standaloneComments := make(map[token.Pos]*ast.CommentGroup, 0) + for _, c := range p.comments { + standaloneComments[c.Pos()] = c + } + + // next remove all lead and line comments from the overall comment map. + // This will give us comments which are standalone, comments which are not + // assigned to any kind of node. + ast.Walk(node, func(nn ast.Node) (ast.Node, bool) { + switch t := nn.(type) { + case *ast.LiteralType: + if t.LineComment != nil { + for _, comment := range t.LineComment.List { + if _, ok := standaloneComments[comment.Pos()]; ok { + delete(standaloneComments, comment.Pos()) + } + } + } + case *ast.ObjectItem: + if t.LeadComment != nil { + for _, comment := range t.LeadComment.List { + if _, ok := standaloneComments[comment.Pos()]; ok { + delete(standaloneComments, comment.Pos()) + } + } + } + + if t.LineComment != nil { + for _, comment := range t.LineComment.List { + if _, ok := standaloneComments[comment.Pos()]; ok { + delete(standaloneComments, comment.Pos()) + } + } + } + } + + return nn, true + }) + + for _, c := range standaloneComments { + p.standaloneComments = append(p.standaloneComments, c) + } + + sort.Sort(ByPosition(p.standaloneComments)) + +} + +// output prints creates b printable HCL output and returns it. +func (p *printer) output(n interface{}) []byte { + var buf bytes.Buffer + + switch t := n.(type) { + case *ast.File: + return p.output(t.Node) + case *ast.ObjectList: + var index int + var nextItem token.Pos + var commented bool + for { + // TODO(arslan): refactor below comment printing, we have the same in objectType + for _, c := range p.standaloneComments { + for _, comment := range c.List { + if index != len(t.Items) { + nextItem = t.Items[index].Pos() + } else { + nextItem = token.Pos{Offset: infinity, Line: infinity} + } + + if comment.Pos().After(p.prev) && comment.Pos().Before(nextItem) { + // if we hit the end add newlines so we can print the comment + if index == len(t.Items) { + buf.Write([]byte{newline, newline}) + } + + buf.WriteString(comment.Text) + + buf.WriteByte(newline) + if index != len(t.Items) { + buf.WriteByte(newline) + } + } + } + } + + if index == len(t.Items) { + break + } + + buf.Write(p.output(t.Items[index])) + if !commented && index != len(t.Items)-1 { + buf.Write([]byte{newline, newline}) + } + index++ + } + case *ast.ObjectKey: + buf.WriteString(t.Token.Text) + case *ast.ObjectItem: + p.prev = t.Pos() + buf.Write(p.objectItem(t)) + case *ast.LiteralType: + buf.Write(p.literalType(t)) + case *ast.ListType: + buf.Write(p.list(t)) + case *ast.ObjectType: + buf.Write(p.objectType(t)) + default: + fmt.Printf(" unknown type: %T\n", n) + } + + return buf.Bytes() +} + +func (p *printer) literalType(lit *ast.LiteralType) []byte { + result := []byte(lit.Token.Text) + if lit.Token.Type == token.HEREDOC { + // Clear the trailing newline from heredocs + if result[len(result)-1] == '\n' { + result = result[:len(result)-1] + } + + // Poison lines 2+ so that we don't indent them + result = p.heredocIndent(result) + } + + return result +} + +// objectItem returns the printable HCL form of an object item. An object type +// starts with one/multiple keys and has a value. The value might be of any +// type. +func (p *printer) objectItem(o *ast.ObjectItem) []byte { + defer un(trace(p, fmt.Sprintf("ObjectItem: %s", o.Keys[0].Token.Text))) + var buf bytes.Buffer + + if o.LeadComment != nil { + for _, comment := range o.LeadComment.List { + buf.WriteString(comment.Text) + buf.WriteByte(newline) + } + } + + for i, k := range o.Keys { + buf.WriteString(k.Token.Text) + buf.WriteByte(blank) + + // reach end of key + if o.Assign.IsValid() && i == len(o.Keys)-1 && len(o.Keys) == 1 { + buf.WriteString("=") + buf.WriteByte(blank) + } + } + + buf.Write(p.output(o.Val)) + + if o.Val.Pos().Line == o.Keys[0].Pos().Line && o.LineComment != nil { + buf.WriteByte(blank) + for _, comment := range o.LineComment.List { + buf.WriteString(comment.Text) + } + } + + return buf.Bytes() +} + +// objectType returns the printable HCL form of an object type. An object type +// begins with a brace and ends with a brace. +func (p *printer) objectType(o *ast.ObjectType) []byte { + defer un(trace(p, "ObjectType")) + var buf bytes.Buffer + buf.WriteString("{") + buf.WriteByte(newline) + + var index int + var nextItem token.Pos + var commented bool + for { + // Print stand alone comments + for _, c := range p.standaloneComments { + for _, comment := range c.List { + // if we hit the end, last item should be the brace + if index != len(o.List.Items) { + nextItem = o.List.Items[index].Pos() + } else { + nextItem = o.Rbrace + } + + if comment.Pos().After(p.prev) && comment.Pos().Before(nextItem) { + // add newline if it's between other printed nodes + if index > 0 { + commented = true + buf.WriteByte(newline) + } + + buf.Write(p.indent([]byte(comment.Text))) + buf.WriteByte(newline) + if index != len(o.List.Items) { + buf.WriteByte(newline) // do not print on the end + } + } + } + } + + if index == len(o.List.Items) { + p.prev = o.Rbrace + break + } + + // check if we have adjacent one liner items. If yes we'll going to align + // the comments. + var aligned []*ast.ObjectItem + for _, item := range o.List.Items[index:] { + // we don't group one line lists + if len(o.List.Items) == 1 { + break + } + + // one means a oneliner with out any lead comment + // two means a oneliner with lead comment + // anything else might be something else + cur := lines(string(p.objectItem(item))) + if cur > 2 { + break + } + + curPos := item.Pos() + + nextPos := token.Pos{} + if index != len(o.List.Items)-1 { + nextPos = o.List.Items[index+1].Pos() + } + + prevPos := token.Pos{} + if index != 0 { + prevPos = o.List.Items[index-1].Pos() + } + + // fmt.Println("DEBUG ----------------") + // fmt.Printf("prev = %+v prevPos: %s\n", prev, prevPos) + // fmt.Printf("cur = %+v curPos: %s\n", cur, curPos) + // fmt.Printf("next = %+v nextPos: %s\n", next, nextPos) + + if curPos.Line+1 == nextPos.Line { + aligned = append(aligned, item) + index++ + continue + } + + if curPos.Line-1 == prevPos.Line { + aligned = append(aligned, item) + index++ + + // finish if we have a new line or comment next. This happens + // if the next item is not adjacent + if curPos.Line+1 != nextPos.Line { + break + } + continue + } + + break + } + + // put newlines if the items are between other non aligned items. + // newlines are also added if there is a standalone comment already, so + // check it too + if !commented && index != len(aligned) { + buf.WriteByte(newline) + } + + if len(aligned) >= 1 { + p.prev = aligned[len(aligned)-1].Pos() + + items := p.alignedItems(aligned) + buf.Write(p.indent(items)) + } else { + p.prev = o.List.Items[index].Pos() + + buf.Write(p.indent(p.objectItem(o.List.Items[index]))) + index++ + } + + buf.WriteByte(newline) + } + + buf.WriteString("}") + return buf.Bytes() +} + +func (p *printer) alignedItems(items []*ast.ObjectItem) []byte { + var buf bytes.Buffer + + // find the longest key and value length, needed for alignment + var longestKeyLen int // longest key length + var longestValLen int // longest value length + for _, item := range items { + key := len(item.Keys[0].Token.Text) + val := len(p.output(item.Val)) + + if key > longestKeyLen { + longestKeyLen = key + } + + if val > longestValLen { + longestValLen = val + } + } + + for i, item := range items { + if item.LeadComment != nil { + for _, comment := range item.LeadComment.List { + buf.WriteString(comment.Text) + buf.WriteByte(newline) + } + } + + for i, k := range item.Keys { + keyLen := len(k.Token.Text) + buf.WriteString(k.Token.Text) + for i := 0; i < longestKeyLen-keyLen+1; i++ { + buf.WriteByte(blank) + } + + // reach end of key + if i == len(item.Keys)-1 && len(item.Keys) == 1 { + buf.WriteString("=") + buf.WriteByte(blank) + } + } + + val := p.output(item.Val) + valLen := len(val) + buf.Write(val) + + if item.Val.Pos().Line == item.Keys[0].Pos().Line && item.LineComment != nil { + for i := 0; i < longestValLen-valLen+1; i++ { + buf.WriteByte(blank) + } + + for _, comment := range item.LineComment.List { + buf.WriteString(comment.Text) + } + } + + // do not print for the last item + if i != len(items)-1 { + buf.WriteByte(newline) + } + } + + return buf.Bytes() +} + +// list returns the printable HCL form of an list type. +func (p *printer) list(l *ast.ListType) []byte { + var buf bytes.Buffer + buf.WriteString("[") + + var longestLine int + for _, item := range l.List { + // for now we assume that the list only contains literal types + if lit, ok := item.(*ast.LiteralType); ok { + lineLen := len(lit.Token.Text) + if lineLen > longestLine { + longestLine = lineLen + } + } + } + + insertSpaceBeforeItem := false + for i, item := range l.List { + if item.Pos().Line != l.Lbrack.Line { + // multiline list, add newline before we add each item + buf.WriteByte(newline) + insertSpaceBeforeItem = false + // also indent each line + val := p.output(item) + curLen := len(val) + buf.Write(p.indent(val)) + buf.WriteString(",") + + if lit, ok := item.(*ast.LiteralType); ok && lit.LineComment != nil { + // if the next item doesn't have any comments, do not align + buf.WriteByte(blank) // align one space + for i := 0; i < longestLine-curLen; i++ { + buf.WriteByte(blank) + } + + for _, comment := range lit.LineComment.List { + buf.WriteString(comment.Text) + } + } + + if i == len(l.List)-1 { + buf.WriteByte(newline) + } + } else { + if insertSpaceBeforeItem { + buf.WriteByte(blank) + insertSpaceBeforeItem = false + } + buf.Write(p.output(item)) + if i != len(l.List)-1 { + buf.WriteString(",") + insertSpaceBeforeItem = true + } + } + + } + + buf.WriteString("]") + return buf.Bytes() +} + +// indent indents the lines of the given buffer for each non-empty line +func (p *printer) indent(buf []byte) []byte { + var prefix []byte + if p.cfg.SpacesWidth != 0 { + for i := 0; i < p.cfg.SpacesWidth; i++ { + prefix = append(prefix, blank) + } + } else { + prefix = []byte{tab} + } + + var res []byte + bol := true + for _, c := range buf { + if bol && c != '\n' { + res = append(res, prefix...) + } + + res = append(res, c) + bol = c == '\n' + } + return res +} + +// unindent removes all the indentation from the tombstoned lines +func (p *printer) unindent(buf []byte) []byte { + var res []byte + for i := 0; i < len(buf); i++ { + skip := len(buf)-i <= len(unindent) + if !skip { + skip = !bytes.Equal(unindent, buf[i:i+len(unindent)]) + } + if skip { + res = append(res, buf[i]) + continue + } + + // We have a marker. we have to backtrace here and clean out + // any whitespace ahead of our tombstone up to a \n + for j := len(res) - 1; j >= 0; j-- { + if res[j] == '\n' { + break + } + + res = res[:j] + } + + // Skip the entire unindent marker + i += len(unindent) - 1 + } + + return res +} + +// heredocIndent marks all the 2nd and further lines as unindentable +func (p *printer) heredocIndent(buf []byte) []byte { + var res []byte + bol := false + for _, c := range buf { + if bol && c != '\n' { + res = append(res, unindent...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +func lines(txt string) int { + endline := 1 + for i := 0; i < len(txt); i++ { + if txt[i] == '\n' { + endline++ + } + } + return endline +} + +// ---------------------------------------------------------------------------- +// Tracing support + +func (p *printer) printTrace(a ...interface{}) { + if !p.enableTrace { + return + } + + const dots = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . " + const n = len(dots) + i := 2 * p.indentTrace + for i > n { + fmt.Print(dots) + i -= n + } + // i <= n + fmt.Print(dots[0:i]) + fmt.Println(a...) +} + +func trace(p *printer, msg string) *printer { + p.printTrace(msg, "(") + p.indentTrace++ + return p +} + +// Usage pattern: defer un(trace(p, "...")) +func un(p *printer) { + p.indentTrace-- + p.printTrace(")") +} diff --git a/vendor/github.com/hashicorp/hcl/hcl/printer/printer.go b/vendor/github.com/hashicorp/hcl/hcl/printer/printer.go new file mode 100644 index 000000000..fb9df58d4 --- /dev/null +++ b/vendor/github.com/hashicorp/hcl/hcl/printer/printer.go @@ -0,0 +1,64 @@ +// Package printer implements printing of AST nodes to HCL format. +package printer + +import ( + "bytes" + "io" + "text/tabwriter" + + "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/hcl/hcl/parser" +) + +var DefaultConfig = Config{ + SpacesWidth: 2, +} + +// A Config node controls the output of Fprint. +type Config struct { + SpacesWidth int // if set, it will use spaces instead of tabs for alignment +} + +func (c *Config) Fprint(output io.Writer, node ast.Node) error { + p := &printer{ + cfg: *c, + comments: make([]*ast.CommentGroup, 0), + standaloneComments: make([]*ast.CommentGroup, 0), + // enableTrace: true, + } + + p.collectComments(node) + + if _, err := output.Write(p.unindent(p.output(node))); err != nil { + return err + } + + // flush tabwriter, if any + var err error + if tw, _ := output.(*tabwriter.Writer); tw != nil { + err = tw.Flush() + } + + return err +} + +// Fprint "pretty-prints" an HCL node to output +// It calls Config.Fprint with default settings. +func Fprint(output io.Writer, node ast.Node) error { + return DefaultConfig.Fprint(output, node) +} + +// Format formats src HCL and returns the result. +func Format(src []byte) ([]byte, error) { + node, err := parser.Parse(src) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + if err := DefaultConfig.Fprint(&buf, node); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/website/source/docs/commands/fmt.html.markdown b/website/source/docs/commands/fmt.html.markdown new file mode 100644 index 000000000..383f58d1d --- /dev/null +++ b/website/source/docs/commands/fmt.html.markdown @@ -0,0 +1,24 @@ +--- +layout: "docs" +page_title: "Command: fmt" +sidebar_current: "docs-commands-fmt" +description: |- + The `terraform fmt` command is used to rewrite Terraform configuration files to a canonical format and style. +--- + +# Command: fmt + +The `terraform fmt` command is used to rewrite Terraform configuration files +to a canonical format and style. + +## Usage + +Usage: `terraform fmt [options] [DIR]` + +`fmt` scans the current directory for configuration files. + +The command-line flags are all optional. The list of available flags are: + +* `-list=false` - List files whose formatting differs +* `-write=false` - Write result to source file instead of STDOUT +* `-diff=false` - Display diffs instead of rewriting files diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index a000ec6c8..e0576adcc 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -67,6 +67,10 @@ destroy + > + fmt + + > get