Repository: ariga/atlas
Pull request #491 (2022-01-25 14:29:27)

cmd/action: add schema fmt command


Patch:
diff --git a/cmd/action/fmt.go b/cmd/action/fmt.go
new file mode 100644
index 00000000..0ae3f2d6
--- /dev/null
+++ b/cmd/action/fmt.go
@@ -0,0 +1,107 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package action
+
+import (
+	"bytes"
+	"io/fs"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/hashicorp/hcl/v2/hclwrite"
+	"github.com/spf13/cobra"
+)
+
+var (
+	// FmtCmd represents the fmt command.
+	FmtCmd = &cobra.Command{
+		Use:   "fmt [path]",
+		Short: "Formats Atlas HCL files",
+		Long: "`atlas schema fmt`" + ` formats all ".hcl" files under the given path using
+cannonical HCL layout style as defined by the github.com/hashicorp/hcl/v2/hclwrite package. 
+Unless stated otherwise, the fmt command will use the current directory.
+
+After running, the command will print the names of the files it has formatted. If all
+files in the directory are formatted, no input will be printed out.
+`,
+		Run:  CmdFmtRun,
+		Args: cobra.MaximumNArgs(1),
+	}
+)
+
+func init() {
+	schemaCmd.AddCommand(FmtCmd)
+}
+
+// CmdFmtRun formats all HCL files in a given directory using canonical HCL formatting
+// rules.
+func CmdFmtRun(cmd *cobra.Command, args []string) {
+	path := "./"
+	if len(args) > 0 {
+		path = args[0]
+	}
+	tasks, err := tasks(path)
+	cobra.CheckErr(err)
+	for _, task := range tasks {
+		changed, err := fmtFile(task)
+		cobra.CheckErr(err)
+		if changed {
+			cmd.Println(task.path)
+		}
+	}
+}
+
+func tasks(path string) ([]fmttask, error) {
+	var tasks []fmttask
+	stat, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+	if !stat.IsDir() {
+		if strings.HasSuffix(path, ".hcl") {
+			tasks = append(tasks, fmttask{
+				path: path,
+				info: stat,
+			})
+		}
+		return tasks, nil
+	}
+	all, err := ioutil.ReadDir(path)
+	if err != nil {
+		return nil, err
+	}
+	for _, f := range all {
+		if f.IsDir() {
+			continue
+		}
+		if strings.HasSuffix(f.Name(), ".hcl") {
+			tasks = append(tasks, fmttask{
+				path: filepath.Join(path, f.Name()),
+				info: f,
+			})
+		}
+	}
+	return tasks, nil
+}
+
+type fmttask struct {
+	path string
+	info fs.FileInfo
+}
+
+// fmtFile tries to format a file and reports if formatting occurred.
+func fmtFile(task fmttask) (bool, error) {
+	orig, err := os.ReadFile(task.path)
+	if err != nil {
+		return false, err
+	}
+	formatted := hclwrite.Format(orig)
+	if !bytes.Equal(formatted, orig) {
+		return true, os.WriteFile(task.path, formatted, task.info.Mode())
+	}
+	return false, nil
+}
diff --git a/doc/md/cli.md b/doc/md/cli.md
index 0e2483ff..2abe17a6 100644
--- a/doc/md/cli.md
+++ b/doc/md/cli.md
@@ -95,6 +95,47 @@ atlas schema apply -d "sqlite://file:ex1.db?_fk=1" -f atlas.hcl
 ```
 
 
+### atlas schema diff
+
+Calculate and print the diff between two schemas.
+
+#### Usage
+```
+atlas schema diff [flags]
+```
+
+#### Details
+`atlas schema diff` connects to two given databases, inspects
+them, calculates the difference in their schemas, and prints a plan of
+SQL queries to bring the "from" database to the schema of the "to" database.
+
+#### Flags
+```
+      --from string   [driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format
+      --to string     [driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format
+
+```
+
+
+### atlas schema fmt
+
+Formats Atlas HCL files
+
+#### Usage
+```
+atlas schema fmt [path]
+```
+
+#### Details
+`atlas schema fmt` formats all ".hcl" files under the given path using
+cannonical HCL layout style as defined by the github.com/hashicorp/hcl/v2/hclwrite package. 
+Unless stated otherwise, the fmt command will use the current directory.
+
+After running, the command will print the names of the files it has formatted. If all
+files in the directory are formatted, no input will be printed out.
+
+
+
 ### atlas schema inspect
 
 Inspect an a database's and print its schema in Atlas DDL syntax.


Test patch:
diff --git a/cmd/action/fmt_test.go b/cmd/action/fmt_test.go
new file mode 100644
index 00000000..90a5d6de
--- /dev/null
+++ b/cmd/action/fmt_test.go
@@ -0,0 +1,78 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package action
+
+import (
+	"bytes"
+	"os"
+	"path"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	unformatted = `block  "x"  {
+ x = 1
+    y     = 2
+}
+`
+	formatted = `block "x" {
+  x = 1
+  y = 2
+}
+`
+)
+
+func TestFmt(t *testing.T) {
+	for _, tt := range []struct {
+		name          string
+		input         string
+		expectedFile  string
+		expectedPrint bool
+	}{
+		{
+			name:          "unformatted",
+			input:         unformatted,
+			expectedFile:  formatted,
+			expectedPrint: true,
+		},
+		{
+			name:          "formatted",
+			input:         formatted,
+			expectedFile:  formatted,
+			expectedPrint: false,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			var out bytes.Buffer
+			filename := "test.hcl"
+			dir := setupFmtTest(t, filename, tt.input)
+			FmtCmd.ResetCommands() // Detach from sub-commands and parents, needed to skip input validation done by them.
+			FmtCmd.SetOut(&out)
+			FmtCmd.SetArgs([]string{dir})
+			err := FmtCmd.Execute()
+			require.NoError(t, err)
+			require.Equal(t, tt.expectedPrint, out.Len() > 0)
+			rf, err := os.ReadFile(filepath.Join(dir, filename))
+			require.NoError(t, err)
+			require.Equal(t, tt.expectedFile, string(rf))
+		})
+	}
+}
+
+func setupFmtTest(t *testing.T, filename, contents string) string {
+	dir, err := os.MkdirTemp(os.TempDir(), "fmt-test-")
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		os.RemoveAll(dir)
+	})
+
+	file := path.Join(dir, filename)
+	err = os.WriteFile(file, []byte(contents), 0644)
+	require.NoError(t, err)
+	return dir
+}
diff --git a/test.hcl b/test.hcl
new file mode 100644
index 00000000..28848beb
--- /dev/null
+++ b/test.hcl
@@ -0,0 +1,4 @@
+schema {
+  x = 1
+  y = 2
+}