Skip to content

Commit 336a991

Browse files
NoamTDmvdan
authored andcommitted
cmd/cue: add cue fmt --files mode
Currently `cue fmt` only supports discovering files based on a "package mode". This means that the arguments passed to `cue fmt` are interpreted as "import paths" as described in `cue help inputs`. This means that the following commands are allowed: - cue fmt ./... - cue fmt ./dir:pkg This change adds a --files flag to cue fmt. When set, it will recursively walk the directory tree rooted at each argument and format all cue files it finds, regardless of the package/module/instance they belong to. Note that the following are excluded, unless explicitly specified as an argument: - files under the `cue.mod` directory. The module.cue file and pkg/gen directories are generated by the cue command so shouldn't need formatting. - files/directories beginning with `.` or `_` (as described in `cue help inputs`) - files with a non-cue extension In addition, we explicitly disallow arguments containing "...", since they only make sense as import paths. Fixes #3065. Updates #728. Signed-off-by: Noam Dolovich <[email protected]> Change-Id: Iba2843a6f36748548241e30ca0341f140d9aba10 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1193962 Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Daniel Martí <[email protected]> TryBot-Result: CUEcueckoo <[email protected]>
1 parent 07d485c commit 336a991

File tree

3 files changed

+263
-65
lines changed

3 files changed

+263
-65
lines changed

cmd/cue/cmd/fmt.go

Lines changed: 155 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,115 +17,148 @@ package cmd
1717
import (
1818
"bytes"
1919
"fmt"
20+
"io"
21+
"io/fs"
2022
"os"
2123
"path/filepath"
24+
"slices"
25+
"strings"
2226

27+
"github.com/rogpeppe/go-internal/diff"
28+
"github.com/spf13/cobra"
29+
30+
"cuelang.org/go/cue/build"
2331
"cuelang.org/go/cue/errors"
2432
"cuelang.org/go/cue/format"
2533
"cuelang.org/go/cue/load"
2634
"cuelang.org/go/cue/parser"
2735
"cuelang.org/go/cue/token"
36+
"cuelang.org/go/internal/filetypes"
2837
"cuelang.org/go/internal/source"
29-
30-
"github.com/rogpeppe/go-internal/diff"
31-
"github.com/spf13/cobra"
3238
)
3339

3440
func newFmtCmd(c *Command) *cobra.Command {
3541
cmd := &cobra.Command{
3642
Use: "fmt [-s] [inputs]",
3743
Short: "formats CUE configuration files",
3844
Long: `Fmt formats the given files or the files for the given packages in place
45+
46+
Arguments are interpreted as import paths (see 'cue help inputs') unless --files is set,
47+
in which case the arguments are file paths to descend into and format all CUE files.
48+
Directories named "cue.mod" and those beginning with "." and "_" are skipped unless
49+
given as explicit arguments.
3950
`,
4051
RunE: mkRunE(c, func(cmd *Command, args []string) error {
41-
builds := loadFromArgs(args, &load.Config{
42-
Tests: true,
43-
Tools: true,
44-
AllCUEFiles: true,
45-
Package: "*",
46-
})
47-
if builds == nil {
48-
return errors.Newf(token.NoPos, "invalid args")
49-
}
52+
cwd, _ := os.Getwd()
53+
check := flagCheck.Bool(cmd)
54+
doDiff := flagDiff.Bool(cmd)
5055

51-
opts := []format.Option{}
56+
formatOpts := []format.Option{}
5257
if flagSimplify.Bool(cmd) {
53-
opts = append(opts, format.Simplify())
58+
formatOpts = append(formatOpts, format.Simplify())
5459
}
5560

5661
var foundBadlyFormatted bool
57-
check := flagCheck.Bool(cmd)
58-
doDiff := flagDiff.Bool(cmd)
59-
cwd, _ := os.Getwd()
60-
stdout := cmd.OutOrStdout()
61-
62-
for _, inst := range builds {
63-
if err := inst.Err; err != nil {
64-
switch {
65-
case errors.As(err, new(*load.PackageError)) && len(inst.BuildFiles) != 0:
66-
// Ignore package errors if there are files to format.
67-
case errors.As(err, new(*load.NoFilesError)):
68-
default:
69-
return err
70-
}
62+
if !flagFiles.Bool(cmd) { // format packages
63+
builds := loadFromArgs(args, &load.Config{
64+
Tests: true,
65+
Tools: true,
66+
AllCUEFiles: true,
67+
Package: "*",
68+
})
69+
if len(builds) == 0 {
70+
return errors.Newf(token.NoPos, "invalid args")
7171
}
72-
for _, file := range inst.BuildFiles {
73-
shouldFormat := inst.User || file.Filename == "-" || filepath.Dir(file.Filename) == inst.Dir
74-
if !shouldFormat {
75-
continue
72+
73+
for _, inst := range builds {
74+
if inst.Err != nil {
75+
switch {
76+
case errors.As(inst.Err, new(*load.PackageError)) && len(inst.BuildFiles) != 0:
77+
// Ignore package errors if there are files to format.
78+
case errors.As(inst.Err, new(*load.NoFilesError)):
79+
default:
80+
exitOnErr(cmd, inst.Err, false)
81+
continue
82+
}
7683
}
84+
for _, file := range inst.BuildFiles {
85+
shouldFormat := inst.User || file.Filename == "-" || filepath.Dir(file.Filename) == inst.Dir
86+
if !shouldFormat {
87+
continue
88+
}
7789

78-
// We buffer the input and output bytes to compare them.
79-
// This allows us to determine whether a file is already
80-
// formatted, without modifying the file.
81-
src, ok := file.Source.([]byte)
82-
if !ok {
83-
var err error
84-
src, err = source.ReadAll(file.Filename, file.Source)
90+
wasModified, err := formatFile(file, formatOpts, cwd, doDiff, check, cmd)
8591
if err != nil {
8692
return err
8793
}
94+
if wasModified {
95+
foundBadlyFormatted = true
96+
}
8897
}
98+
}
99+
} else { // format individual files
100+
hasDots := slices.ContainsFunc(args, func(arg string) bool {
101+
return strings.Contains(arg, "...")
102+
})
103+
if hasDots {
104+
return errors.New(`cannot use "..." in --files mode`)
105+
}
106+
107+
if len(args) == 0 {
108+
args = []string{"."}
109+
}
89110

90-
file, err := parser.ParseFile(file.Filename, src, parser.ParseComments)
111+
processFile := func(path string) error {
112+
file, err := filetypes.ParseFile(path, filetypes.Input)
91113
if err != nil {
92114
return err
93115
}
94116

95-
formatted, err := format.Node(file, opts...)
117+
if path == "-" {
118+
contents, err := io.ReadAll(cmd.InOrStdin())
119+
exitOnErr(cmd, err, false)
120+
file.Source = contents
121+
}
122+
123+
wasModified, err := formatFile(file, formatOpts, cwd, doDiff, check, cmd)
96124
if err != nil {
97125
return err
98126
}
99-
100-
// Always write to stdout if the file is read from stdin.
101-
if file.Filename == "-" && !doDiff && !check {
102-
stdout.Write(formatted)
127+
if wasModified {
128+
foundBadlyFormatted = true
103129
}
104-
105-
// File is already well formatted; we can stop here.
106-
if bytes.Equal(formatted, src) {
130+
return nil
131+
}
132+
for _, arg := range args {
133+
if arg == "-" {
134+
err := processFile(arg)
135+
exitOnErr(cmd, err, false)
107136
continue
108137
}
109138

110-
foundBadlyFormatted = true
111-
path, err := filepath.Rel(cwd, file.Filename)
112-
if err != nil {
113-
path = file.Filename
114-
}
115-
116-
switch {
117-
case doDiff:
118-
d := diff.Diff(path+".orig", src, path, formatted)
119-
fmt.Fprintln(stdout, string(d))
120-
case check:
121-
fmt.Fprintln(stdout, path)
122-
case file.Filename == "-":
123-
// already wrote the formatted source to stdout above
124-
default:
125-
if err := os.WriteFile(file.Filename, formatted, 0644); err != nil {
139+
arg = filepath.Clean(arg)
140+
err := filepath.WalkDir(arg, func(path string, d fs.DirEntry, err error) error {
141+
if err != nil {
126142
return err
127143
}
128-
}
144+
145+
if d.IsDir() {
146+
name := d.Name()
147+
isMod := name == "cue.mod"
148+
isDot := strings.HasPrefix(name, ".") && name != "." && name != ".."
149+
if path != arg && (isMod || isDot || strings.HasPrefix(name, "_")) {
150+
return filepath.SkipDir
151+
}
152+
return nil
153+
}
154+
155+
if !strings.HasSuffix(path, ".cue") {
156+
return nil
157+
}
158+
159+
return processFile(path)
160+
})
161+
exitOnErr(cmd, err, false)
129162
}
130163
}
131164

@@ -139,6 +172,63 @@ func newFmtCmd(c *Command) *cobra.Command {
139172

140173
cmd.Flags().Bool(string(flagCheck), false, "exits with non-zero status if any files are not formatted")
141174
cmd.Flags().BoolP(string(flagDiff), "d", false, "display diffs instead of rewriting files")
175+
cmd.Flags().Bool(string(flagFiles), false, "treat arguments as file paths to descend into rather than import paths")
142176

143177
return cmd
144178
}
179+
180+
// formatFile formats a single file.
181+
// It returns true if the file was not well formatted.
182+
func formatFile(file *build.File, opts []format.Option, cwd string, doDiff, check bool, cmd *Command) (bool, error) {
183+
// We buffer the input and output bytes to compare them.
184+
// This allows us to determine whether a file is already
185+
// formatted, without modifying the file.
186+
src, ok := file.Source.([]byte)
187+
if !ok {
188+
var err error
189+
src, err = source.ReadAll(file.Filename, file.Source)
190+
if err != nil {
191+
return false, err
192+
}
193+
}
194+
195+
syntax, err := parser.ParseFile(file.Filename, src, parser.ParseComments)
196+
if err != nil {
197+
return false, err
198+
}
199+
200+
formatted, err := format.Node(syntax, opts...)
201+
if err != nil {
202+
return false, err
203+
}
204+
205+
stdout := cmd.OutOrStdout()
206+
// Always write to stdout if the file is read from stdin.
207+
if file.Filename == "-" && !doDiff && !check {
208+
stdout.Write(formatted)
209+
}
210+
211+
// File is already well formatted; we can stop here.
212+
if bytes.Equal(formatted, src) {
213+
return false, nil
214+
}
215+
216+
path, err := filepath.Rel(cwd, file.Filename)
217+
if err != nil {
218+
path = file.Filename
219+
}
220+
221+
switch {
222+
case doDiff:
223+
d := diff.Diff(path+".orig", src, path, formatted)
224+
fmt.Fprintln(stdout, string(d))
225+
case check:
226+
fmt.Fprintln(stdout, path)
227+
case file.Filename == "-":
228+
// already wrote the formatted source to stdout above
229+
default:
230+
err := os.WriteFile(file.Filename, formatted, 0644)
231+
exitOnErr(cmd, err, false)
232+
}
233+
return true, nil
234+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# succeeds when all files are well formatted
2+
exec cue fmt --files --check standalone/a/
3+
4+
# prints files that are not well formatted, recursively
5+
! exec cue fmt --files --check standalone/b
6+
cmpenv stdout standalone/b/stdout-golden
7+
8+
# regular files can be passed as an argument with or without files
9+
! exec cue fmt --files --check standalone/b/c/not-formatted.cue standalone/b/not-formatted.cue
10+
cmpenv stdout standalone/b/stdout-golden
11+
12+
# file can be passed via stdin
13+
stdin standalone/stdin/not-formatted.cue
14+
! exec cue fmt --files --check -
15+
cmpenv stdout standalone/stdin/stdout-check-golden
16+
17+
stdin standalone/stdin/not-formatted.cue
18+
exec cue fmt --files -
19+
cmpenv stdout standalone/stdin/stdout-golden
20+
21+
# current directory is used when no args provided
22+
cd noargs
23+
! exec cue fmt --files --check
24+
cmpenv stdout stdout-golden
25+
cd ..
26+
27+
# directories beginning with _ or . are ignored
28+
exec cue fmt --files --check standalone/hidden/
29+
30+
# directories beginning with _ or . are processed when passed explicitly
31+
! exec cue fmt --files --check standalone/hidden/_temp/ standalone/hidden/.temp/
32+
cmpenv stdout standalone/hidden/stdin-golden
33+
34+
# files under cue.mod are ignored
35+
exec cue fmt --files --check mod
36+
! stdout .
37+
38+
# files under cue.mod are processed when passed explicitly
39+
! exec cue fmt --files --check mod/cue.mod/
40+
cmpenv stdout mod/stdout-golden
41+
42+
# exit early if user attempts to use ... patterns
43+
! exec cue fmt --files --check standalone/a/...
44+
stderr 'cannot use "..." in --files mode'
45+
! stdout .
46+
! exec cue fmt --files --check h...
47+
stderr 'cannot use "..." in --files mode'
48+
! stdout .
49+
50+
-- standalone/a/formatted.cue --
51+
foo: "bar"
52+
-- standalone/b/not-formatted.cue --
53+
x: 1
54+
-- standalone/b/c/not-formatted.cue --
55+
foo: "bar"
56+
-- standalone/hidden/_temp/not-formatted.cue --
57+
foo: "bar"
58+
-- standalone/hidden/.temp/not-formatted.cue --
59+
foo: "bar"
60+
-- standalone/hidden/stdin-golden --
61+
standalone${/}hidden${/}_temp${/}not-formatted.cue
62+
standalone${/}hidden${/}.temp${/}not-formatted.cue
63+
-- standalone/b/stdout-golden --
64+
standalone${/}b${/}c${/}not-formatted.cue
65+
standalone${/}b${/}not-formatted.cue
66+
-- standalone/stdin/not-formatted.cue --
67+
foo: "bar"
68+
-- standalone/stdin/stdout-golden --
69+
foo: "bar"
70+
-- standalone/stdin/stdout-check-golden --
71+
-
72+
-- mod/cue.mod/gen/not-formatted.cue --
73+
foo: "bar"
74+
-- mod/cue.mod/usr/not-formatted.cue --
75+
foo: "bar"
76+
-- mod/cue.mod/pkg/not-formatted.cue --
77+
foo: "bar"
78+
-- mod/cue.mod/module.cue --
79+
module: "example.com"
80+
language: {
81+
version: "v0.9.3"
82+
}
83+
-- mod/stdout-golden --
84+
mod${/}cue.mod${/}gen${/}not-formatted.cue
85+
mod${/}cue.mod${/}module.cue
86+
mod${/}cue.mod${/}pkg${/}not-formatted.cue
87+
mod${/}cue.mod${/}usr${/}not-formatted.cue
88+
-- noargs/file.cue --
89+
foo: "bar"
90+
-- noargs/dir/another-file.cue --
91+
foo: "bar"
92+
-- noargs/stdout-golden --
93+
dir${/}another-file.cue
94+
file.cue
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# simplify works in package mode
2+
exec cue fmt --simplify ./pkg/...
3+
cmp pkg/file.cue file.golden
4+
5+
# simplify works in --files mode
6+
exec cue fmt --simplify --files ./files/
7+
cmp files/file.cue file.golden
8+
9+
-- files/file.cue --
10+
"quoted": b
11+
-- pkg/file.cue --
12+
"quoted": b
13+
-- file.golden --
14+
quoted: b

0 commit comments

Comments
 (0)