From cc41c7cfa03f05717374055e60c5761466e76e5e Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Tue, 2 Feb 2016 22:10:22 +0000 Subject: [PATCH 1/6] 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 From c753390399843953755ef63483a086fd3ab3708d Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Tue, 2 Feb 2016 22:10:22 +0000 Subject: [PATCH 2/6] command/fmt: Default write and list to true The most common usage usage will be enabling the `-write` and `-list` options so that files are updated in place and a list of any modified files is printed. This matches the default behaviour of `go fmt` (not `gofmt`). So enable these options by default. This does mean that you will have to explicitly disable these if you want to generate valid patches, e.g. `terraform fmt -diff -write=false -list=false` --- command/fmt.go | 4 ++-- command/fmt_test.go | 6 +++--- website/source/docs/commands/fmt.html.markdown | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/command/fmt.go b/command/fmt.go index e4e9aad2f..49e06c08e 100644 --- a/command/fmt.go +++ b/command/fmt.go @@ -24,8 +24,8 @@ 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.List, "list", true, "list") + cmdFlags.BoolVar(&c.opts.Write, "write", true, "write") cmdFlags.BoolVar(&c.opts.Diff, "diff", false, "diff") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } diff --git a/command/fmt_test.go b/command/fmt_test.go index 3d284e466..d38d54644 100644 --- a/command/fmt_test.go +++ b/command/fmt_test.go @@ -1,7 +1,7 @@ package command import ( - "bytes" + "fmt" "io/ioutil" "os" "path/filepath" @@ -88,8 +88,8 @@ func TestFmt_workingDirectory(t *testing.T) { t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String()) } - expected := fmtFixture.golden - if actual := ui.OutputWriter.Bytes(); !bytes.Equal(actual, expected) { + expected := fmt.Sprintf("%s\n", fmtFixture.filename) + if actual := ui.OutputWriter.String(); actual != expected { t.Fatalf("got: %q\nexpected: %q", actual, expected) } } diff --git a/website/source/docs/commands/fmt.html.markdown b/website/source/docs/commands/fmt.html.markdown index 383f58d1d..7e5d7647b 100644 --- a/website/source/docs/commands/fmt.html.markdown +++ b/website/source/docs/commands/fmt.html.markdown @@ -19,6 +19,6 @@ Usage: `terraform fmt [options] [DIR]` 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 +* `-list=true` - List files whose formatting differs +* `-write=true` - Write result to source file instead of STDOUT * `-diff=false` - Display diffs instead of rewriting files From 1b967e612f49393a699d528341b0d1c961910676 Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Tue, 2 Feb 2016 22:10:22 +0000 Subject: [PATCH 3/6] command/fmt: Accept optional directory argument So that you can operate on files in a directory other than your current working directory. --- command/fmt.go | 19 +++++++---- command/fmt_test.go | 33 +++++++++++++++++-- .../source/docs/commands/fmt.html.markdown | 4 ++- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/command/fmt.go b/command/fmt.go index 49e06c08e..9c9e6c22f 100644 --- a/command/fmt.go +++ b/command/fmt.go @@ -34,13 +34,19 @@ func (c *FmtCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) > 0 { - c.Ui.Error("The fmt command expects no arguments.") + if len(args) > 1 { + c.Ui.Error("The fmt command expects at most one argument.") cmdFlags.Usage() return 1 } - dir := "." + var dir string + if len(args) == 0 { + dir = "." + } else { + dir = args[0] + } + output := &cli.UiWriter{Ui: c.Ui} err := fmtcmd.Run([]string{dir}, []string{fileExtension}, nil, output, c.opts) if err != nil { @@ -53,10 +59,11 @@ func (c *FmtCommand) Run(args []string) int { func (c *FmtCommand) Help() string { helpText := ` -Usage: terraform fmt [options] +Usage: terraform fmt [options] [DIR] - Rewrites all Terraform configuration files in the current working - directory to a canonical format. + Rewrites all Terraform configuration files to a canonical format. + + If DIR is not specified then the current working directory will be used. Options: diff --git a/command/fmt_test.go b/command/fmt_test.go index d38d54644..b9d69fe66 100644 --- a/command/fmt_test.go +++ b/command/fmt_test.go @@ -47,12 +47,15 @@ func TestFmt_tooManyArgs(t *testing.T) { }, } - args := []string{"bad"} + args := []string{ + "one", + "two", + } 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." + expected := "The fmt command expects at most one argument." if actual := ui.ErrorWriter.String(); !strings.Contains(actual, expected) { t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) } @@ -94,6 +97,32 @@ func TestFmt_workingDirectory(t *testing.T) { } } +func TestFmt_directoryArg(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, + }, + } + + args := []string{tempDir} + if code := c.Run(args); code != 0 { + t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String()) + } + + expected := fmt.Sprintf("%s\n", filepath.Join(tempDir, fmtFixture.filename)) + if actual := ui.OutputWriter.String(); actual != expected { + t.Fatalf("got: %q\nexpected: %q", actual, expected) + } +} + var fmtFixture = struct { filename string input, golden []byte diff --git a/website/source/docs/commands/fmt.html.markdown b/website/source/docs/commands/fmt.html.markdown index 7e5d7647b..51802784b 100644 --- a/website/source/docs/commands/fmt.html.markdown +++ b/website/source/docs/commands/fmt.html.markdown @@ -15,7 +15,9 @@ to a canonical format and style. Usage: `terraform fmt [options] [DIR]` -`fmt` scans the current directory for configuration files. +By default, `fmt` scans the current directory for configuration files. If +the `dir` argument is provided then it will scan that given directory +instead. The command-line flags are all optional. The list of available flags are: From e9128769b5424f471af7378660baba8c7fc90693 Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Tue, 2 Feb 2016 22:10:22 +0000 Subject: [PATCH 4/6] command/fmt: Accept input from STDIN So that you can do automatic formatting from an editor. You probably want to disable the `-write` and `-list` options so that you just get the re-formatted content, e.g. cat main.tf | terraform fmt -write=false -list=false - I've added a non-exported field called `input` so that we can override this for the tests. If not specified, like in `commands.go`, then it will default to `os.Stdin` which works on the command line. --- command/fmt.go | 21 ++++++++++---- command/fmt_test.go | 29 +++++++++++++++++++ .../source/docs/commands/fmt.html.markdown | 3 +- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/command/fmt.go b/command/fmt.go index 9c9e6c22f..f0a7bfa79 100644 --- a/command/fmt.go +++ b/command/fmt.go @@ -3,6 +3,8 @@ package command import ( "flag" "fmt" + "io" + "os" "strings" "github.com/hashicorp/hcl/hcl/fmtcmd" @@ -10,6 +12,7 @@ import ( ) const ( + stdinArg = "-" fileExtension = "tf" ) @@ -17,10 +20,15 @@ const ( // files to a canonical format and style. type FmtCommand struct { Meta - opts fmtcmd.Options + opts fmtcmd.Options + input io.Reader // STDIN if nil } func (c *FmtCommand) Run(args []string) int { + if c.input == nil { + c.input = os.Stdin + } + args = c.Meta.process(args, false) cmdFlags := flag.NewFlagSet("fmt", flag.ContinueOnError) @@ -40,15 +48,15 @@ func (c *FmtCommand) Run(args []string) int { return 1 } - var dir string + var dirs []string if len(args) == 0 { - dir = "." - } else { - dir = args[0] + dirs = []string{"."} + } else if args[0] != stdinArg { + dirs = []string{args[0]} } output := &cli.UiWriter{Ui: c.Ui} - err := fmtcmd.Run([]string{dir}, []string{fileExtension}, nil, output, c.opts) + err := fmtcmd.Run(dirs, []string{fileExtension}, c.input, output, c.opts) if err != nil { c.Ui.Error(fmt.Sprintf("Error running fmt: %s", err)) return 2 @@ -64,6 +72,7 @@ Usage: terraform fmt [options] [DIR] Rewrites all Terraform configuration files to a canonical format. If DIR is not specified then the current working directory will be used. + If DIR is "-" then content will be read from STDIN. Options: diff --git a/command/fmt_test.go b/command/fmt_test.go index b9d69fe66..de117bb2c 100644 --- a/command/fmt_test.go +++ b/command/fmt_test.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "fmt" "io/ioutil" "os" @@ -123,6 +124,34 @@ func TestFmt_directoryArg(t *testing.T) { } } +func TestFmt_stdinArg(t *testing.T) { + input := new(bytes.Buffer) + input.Write(fmtFixture.input) + + ui := new(cli.MockUi) + c := &FmtCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + input: input, + } + + args := []string{ + "-write=false", + "-list=false", + "-", + } + 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 diff --git a/website/source/docs/commands/fmt.html.markdown b/website/source/docs/commands/fmt.html.markdown index 51802784b..9f33e98e3 100644 --- a/website/source/docs/commands/fmt.html.markdown +++ b/website/source/docs/commands/fmt.html.markdown @@ -17,7 +17,8 @@ Usage: `terraform fmt [options] [DIR]` By default, `fmt` scans the current directory for configuration files. If the `dir` argument is provided then it will scan that given directory -instead. +instead. If `dir` is a single dash (`-`) then `fmt` will read from standard +input (STDIN). The command-line flags are all optional. The list of available flags are: From 79e2753e4124602d4499716f133d1e87058f5b11 Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Tue, 2 Feb 2016 22:10:23 +0000 Subject: [PATCH 5/6] command/fmt: Disable list/write when using STDIN These options don't make sense when passing STDIN. `-write` will raise an error because there is no file to write to. `-list` will always say ``. So disable whenever using STDIN, making the command much simpler: cat main.tf | terraform fmt - --- command/fmt.go | 9 ++++++--- command/fmt_test.go | 6 +----- website/source/docs/commands/fmt.html.markdown | 5 +++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/command/fmt.go b/command/fmt.go index f0a7bfa79..d9ccc2643 100644 --- a/command/fmt.go +++ b/command/fmt.go @@ -51,7 +51,10 @@ func (c *FmtCommand) Run(args []string) int { var dirs []string if len(args) == 0 { dirs = []string{"."} - } else if args[0] != stdinArg { + } else if args[0] == stdinArg { + c.opts.List = false + c.opts.Write = false + } else { dirs = []string{args[0]} } @@ -76,9 +79,9 @@ Usage: terraform fmt [options] [DIR] Options: - -list List files whose formatting differs + -list List files whose formatting differs (disabled if using STDIN) - -write Write result to source file instead of STDOUT + -write Write result to source file instead of STDOUT (disabled if using STDIN) -diff Display diffs instead of rewriting files diff --git a/command/fmt_test.go b/command/fmt_test.go index de117bb2c..0a7f26fdb 100644 --- a/command/fmt_test.go +++ b/command/fmt_test.go @@ -137,11 +137,7 @@ func TestFmt_stdinArg(t *testing.T) { input: input, } - args := []string{ - "-write=false", - "-list=false", - "-", - } + args := []string{"-"} if code := c.Run(args); code != 0 { t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String()) } diff --git a/website/source/docs/commands/fmt.html.markdown b/website/source/docs/commands/fmt.html.markdown index 9f33e98e3..bb48ae957 100644 --- a/website/source/docs/commands/fmt.html.markdown +++ b/website/source/docs/commands/fmt.html.markdown @@ -22,6 +22,7 @@ input (STDIN). The command-line flags are all optional. The list of available flags are: -* `-list=true` - List files whose formatting differs -* `-write=true` - Write result to source file instead of STDOUT +* `-list=true` - List files whose formatting differs (disabled if using STDIN) +* `-write=true` - Write result to source file instead of STDOUT (disabled if + using STDIN) * `-diff=false` - Display diffs instead of rewriting files From d883b760704d2831c993e9c2c165202511555a93 Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Tue, 2 Feb 2016 22:10:23 +0000 Subject: [PATCH 6/6] command/fmt: Test non-default options To ensure that these are passed over to `fmtcmd` correctly. --- command/fmt_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/command/fmt_test.go b/command/fmt_test.go index 0a7f26fdb..191cd47ee 100644 --- a/command/fmt_test.go +++ b/command/fmt_test.go @@ -148,6 +148,37 @@ func TestFmt_stdinArg(t *testing.T) { } } +func TestFmt_nonDefaultOptions(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, + }, + } + + args := []string{ + "-list=false", + "-write=false", + "-diff", + tempDir, + } + if code := c.Run(args); code != 0 { + t.Fatalf("wrong exit code. errors: \n%s", ui.ErrorWriter.String()) + } + + expected := fmt.Sprintf("-%s+%s", fmtFixture.input, fmtFixture.golden) + if actual := ui.OutputWriter.String(); !strings.Contains(actual, expected) { + t.Fatalf("expected:\n%s\n\nto include: %q", actual, expected) + } +} + var fmtFixture = struct { filename string input, golden []byte