Skip to content

Commit

Permalink
feat: fmt of individual files and stdin. (#1316)
Browse files Browse the repository at this point in the history
## What this PR does / why we need it:

Introduces the ability to format individual files or `stdin` with the
`terramate fmt` command.

Example:

Individual files:
```
$ terramate fmt terramate.tm stacks/my-stack/stack.tm
```

Stdin:
```
$ terramate fmt -
format this input ^D
$ echo $?
1
```

Format current directory (old behavior):
```
$ terramate fmt
```

## Which issue(s) this PR fixes:

Fixes #1226 

## Special notes for your reviewer:

## Does this PR introduce a user-facing change?
```
yes, new arguments list for `fmt`
```
  • Loading branch information
i4ki committed Feb 9, 2024
2 parents 273151e + 924fce3 commit b41d3d9
Show file tree
Hide file tree
Showing 6 changed files with 386 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Given a version number `MAJOR.MINOR.PATCH`, we increment the:
### Added

- Add `terramate.config.generate.hcl_magic_header_comment_style` option for setting the generated comment style.
- Add support for formatting specific files and stdin (`terramate fmt [file...]` or `terramate fmt -`).

## 0.4.5

Expand Down
49 changes: 44 additions & 5 deletions cmd/terramate/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ type cliSpec struct {
} `cmd:"" help:"Creates a stack on the project"`

Fmt struct {
Check bool `hidden:"" help:"Lists unformatted files, exit with 0 if all is formatted, 1 otherwise"`
DetailedExitCode bool `help:"Return an appropriate exit code (0 = ok, 1 = error, 2 = no error but changes were made)"`
Files []string `arg:"" optional:"true" predictor:"file" help:"files to be formatted"`
Check bool `hidden:"" help:"Lists unformatted files, exit with 0 if all is formatted, 1 otherwise"`
DetailedExitCode bool `help:"Return an appropriate exit code (0 = ok, 1 = error, 2 = no error but changes were made)"`
} `cmd:"" help:"Format all files inside dir recursively"`

List struct {
Expand Down Expand Up @@ -595,6 +596,8 @@ func (c *cli) run() {
switch c.ctx.Command() {
case "fmt":
c.format()
case "fmt <files>":
c.format()
case "create <path>":
c.createStack()
case "create":
Expand Down Expand Up @@ -1403,9 +1406,45 @@ func (c *cli) format() {
fatal("Invalid args", errors.E("--check conflicts with --detailed-exit-code"))
}

results, err := fmt.FormatTree(c.wd())
if err != nil {
fatal("formatting files", err)
var results []fmt.FormatResult
switch len(c.parsedArgs.Fmt.Files) {
case 0:
var err error
results, err = fmt.FormatTree(c.wd())
if err != nil {
fatal(sprintf("formatting directory %s", c.wd()), err)
}
case 1:
if c.parsedArgs.Fmt.Files[0] == "-" {
content, err := io.ReadAll(os.Stdin)
if err != nil {
fatal("reading stdin", err)
}
original := string(content)
formatted, err := fmt.Format(original, "<stdin>")
if err != nil {
fatal("formatting stdin", err)
}

if c.parsedArgs.Fmt.Check {
var status int
if formatted != original {
status = 1
}
os.Exit(status)
}

stdfmt.Print(formatted)
return
}

fallthrough
default:
var err error
results, err = fmt.FormatFiles(c.wd(), c.parsedArgs.Fmt.Files)
if err != nil {
fatal("formatting files", err)
}
}

for _, res := range results {
Expand Down
213 changes: 213 additions & 0 deletions cmd/terramate/e2etests/core/fmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,216 @@ name = "name"
assertWantedFilesContents(t, formattedHCL)
})
}

func TestFmtFiles(t *testing.T) {
type want struct {
layout []string
res RunExpected
}
type testcase struct {
name string
layout []string
files []string
check bool
stdin string
absPaths bool
want want
}

for _, tc := range []testcase{
{
name: "non-existent file",
files: []string{"non-existent.tm"},
want: want{
res: RunExpected{
StderrRegex: string(fmt.ErrReadFile),
Status: 1,
},
},
},
{
name: "single file",
layout: []string{
`f:example.tm:terramate {
config{}
}`,
},
files: []string{"example.tm"},
want: want{
res: RunExpected{
Stdout: nljoin("example.tm"),
},
layout: []string{
`f:example.tm:terramate {
config {}
}`,
},
},
},
{
name: "multiple files",
layout: []string{
`f:example1.tm:terramate {
config{}
}`,
`f:example2.tm:terramate {
config{}
}`,
},
files: []string{"example1.tm", "example2.tm"},
want: want{
res: RunExpected{
Stdout: nljoin("example1.tm", "example2.tm"),
},
layout: []string{
`f:example1.tm:terramate {
config {}
}`,
`f:example2.tm:terramate {
config {}
}`,
},
},
},
{
name: "multiple files with --check",
layout: []string{
`f:example1.tm:terramate {
config{}
}`,
`f:example2.tm:terramate {
config{}
}`,
},
files: []string{"example1.tm", "example2.tm"},
check: true,
want: want{
res: RunExpected{
Stdout: nljoin("example1.tm", "example2.tm"),
Status: 1,
},
},
},
{
name: "multiple files with absolute path",
layout: []string{
`f:example1.tm:terramate {
config{}
}`,
`f:example2.tm:terramate {
config{}
}`,
},
absPaths: true,
files: []string{"example1.tm", "example2.tm"},
want: want{
res: RunExpected{
Stdout: nljoin("example1.tm", "example2.tm"),
},
layout: []string{
`f:example1.tm:terramate {
config {}
}`,
`f:example2.tm:terramate {
config {}
}`,
},
},
},
{
name: "format stdin",
files: []string{"-"},
stdin: `stack {
name="name"
description = "desc"
}`,
want: want{
res: RunExpected{
Stdout: `stack {
name = "name"
description = "desc"
}`,
},
},
},
{
name: "format stdin with multiple blocks",
files: []string{"-"},
stdin: `stack {
name="name"
description = "desc"
}
generate_file "a.txt" {
content = "a"
}
`,
want: want{
res: RunExpected{
Stdout: `stack {
name = "name"
description = "desc"
}
generate_file "a.txt" {
content = "a"
}
`,
},
},
},
{
name: "format stdin without content",
files: []string{"-"},
},
{
name: "format stdin with --check",
files: []string{"-"},
stdin: `stack {
name="name"
description = "desc"
}`,
check: true,
want: want{
res: RunExpected{
Status: 1,
},
},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s := sandbox.NoGit(t, true)
s.BuildTree(tc.layout)
cli := NewCLI(t, s.RootDir())
files := tc.files
if tc.absPaths {
for i, f := range files {
files[i] = filepath.Join(s.RootDir(), f)
}
}
args := []string{"fmt"}
if tc.check {
args = append(args, "--check")
}
args = append(args, files...)
var result RunResult
if len(files) == 1 && files[0] == "-" {
result = cli.RunWithStdin(tc.stdin, args...)
} else {
result = cli.Run(args...)
}
AssertRunResult(t, result, tc.want.res)
s.AssertTree(tc.want.layout)
})
}
}
17 changes: 17 additions & 0 deletions cmd/terramate/e2etests/internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,23 @@ func (tm CLI) Run(args ...string) RunResult {
}
}

// RunWithStdin runs the CLI but uses the provided string as stdin.
func (tm CLI) RunWithStdin(stdin string, args ...string) RunResult {
t := tm.t
t.Helper()

cmd := tm.NewCmd(args...)
cmd.Stdin.b.WriteString(stdin)
_ = cmd.Run()

return RunResult{
Cmd: strings.Join(args, " "),
Stdout: cmd.Stdout.String(),
Stderr: cmd.Stderr.String(),
Status: cmd.ExitCode(),
}
}

// RunScript is a helper for executing `terramate run-script`.
func (tm CLI) RunScript(args ...string) RunResult {
return tm.Run(append([]string{"script", "run"}, args...)...)
Expand Down
Loading

0 comments on commit b41d3d9

Please sign in to comment.