Skip to content

Commit 21e2a17

Browse files
committed
feat: add formatter.stdin-options to invoke formatters in stdin mode
1 parent affa216 commit 21e2a17

6 files changed

Lines changed: 124 additions & 7 deletions

File tree

cmd/root_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2254,6 +2254,31 @@ help:
22542254
as.Equal(`# print this message
22552255
help:
22562256
just --list --list-submodules --unsorted
2257+
`, string(out))
2258+
}),
2259+
)
2260+
2261+
// Format with embed-path. `embed-path` prefixes the file with its path. If
2262+
// this formatter didn't support the Stdin Specification, then we'd see a
2263+
// temp path get added instead of the true path to the file.
2264+
contents = `
2265+
The formatter should add the path above this line.
2266+
`
2267+
os.Stdin = test.TempFile(t, "", "stdin", &contents)
2268+
2269+
treefmt(t,
2270+
withArgs("-vvv", "--stdin", "path/to/foo.embed-path"),
2271+
withNoError(t),
2272+
withStats(t, map[stats.Type]int{
2273+
stats.Traversed: 1,
2274+
stats.Matched: 1,
2275+
stats.Formatted: 1,
2276+
stats.Changed: 1,
2277+
}),
2278+
withStdout(func(out []byte) {
2279+
as.Equal(`# path/to/foo.embed-path
2280+
2281+
The formatter should add the path above this line.
22572282
`, string(out))
22582283
}),
22592284
)

config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ type Formatter struct {
5757
Command string `mapstructure:"command" toml:"command"`
5858
// Options are an optional list of args to be passed to Command.
5959
Options []string `mapstructure:"options,omitempty" toml:"options,omitempty"`
60+
// StringOptions are an optional list of args used to invoke the formatter
61+
// in stdin mode (where it reads the buffer to format from stdin rather
62+
// than a file). Any occurrences of `$path` will be replaced with the "advisory path"
63+
// of the "file" being formatted (useful for formatters whose behavior
64+
// depend on the path of the file being formatted).
65+
StdinOptions []string `mapstructure:"stdin-options,omitempty" toml:"stdin-options,omitempty"`
6066
// Includes is a list of glob patterns used to determine whether this Formatter should be applied against a path.
6167
Includes []string `mapstructure:"includes,omitempty" toml:"includes,omitempty"`
6268
// Excludes is an optional list of glob patterns used to exclude certain files from this Formatter.

docs/site/getting-started/configure.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,15 @@ The command to invoke when applying the formatter.
460460

461461
An optional list of args to be passed to `command`.
462462

463+
### `stdin-options`
464+
465+
An optional list of args used to invoke the formatter in "stdin mode" (where it
466+
reads the buffer to format from stdin rather than a file). Any occurrences of
467+
`$path` will be replaced with the "advisory path" of the "file" being formatted.
468+
469+
This is useful for formatters whose behavior depend on the path of the file being
470+
formatted.
471+
463472
### `includes`
464473

465474
A list of [glob patterns](#glob-patterns-format) used to determine whether the formatter should be applied against a given path.

format/formatter.go

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,41 @@ func (f *Formatter) Apply(ctx context.Context, files []*walk.File) error {
107107
return nil
108108
}
109109

110-
// append paths to the args
111-
for _, file := range files {
112-
if file.TmpPath != "" {
113-
args = append(args, file.TmpPath)
114-
} else {
115-
args = append(args, file.RelPath)
110+
onlyFile := (*walk.File)(nil)
111+
if len(files) == 1 {
112+
onlyFile = files[0]
113+
}
114+
115+
// If we have a TmpPath, that means we're formatting something other than the file's RelPath
116+
// mode". If the underlying formatter has its own stdin mode support, then
117+
// we can invoke it in a way where we pass along the files's RelPath as an
118+
// "advisory path", which may affect the behavior of the formatter.
119+
useStdinMode := files[0].TmpPath != "" && f.config.StdinOptions != nil
120+
121+
stdin := (*os.File)(nil)
122+
123+
if useStdinMode {
124+
// Feed the file to the formatter via stdin.
125+
tmpFile, err := os.Open(onlyFile.TmpPath)
126+
if err != nil {
127+
panic(err)
128+
}
129+
defer tmpFile.Close()
130+
131+
stdin = tmpFile
132+
133+
replacer := strings.NewReplacer("$path", onlyFile.RelPath)
134+
for _, arg := range f.config.StdinOptions {
135+
args = append(args, replacer.Replace(arg))
136+
}
137+
} else {
138+
// append paths to the args
139+
for _, file := range files {
140+
if file.TmpPath != "" {
141+
args = append(args, file.TmpPath)
142+
} else {
143+
args = append(args, file.RelPath)
144+
}
116145
}
117146
}
118147

@@ -123,11 +152,13 @@ func (f *Formatter) Apply(ctx context.Context, files []*walk.File) error {
123152
return cmd.Process.Signal(os.Interrupt)
124153
}
125154
cmd.Dir = f.workingDir
155+
cmd.Stdin = stdin
126156

127157
// log out the command being executed
128158
f.log.Debugf("executing: %s", cmd.String())
129159

130-
if out, err := cmd.CombinedOutput(); err != nil {
160+
out, err := cmd.CombinedOutput()
161+
if err != nil {
131162
f.log.Errorf("failed to apply with options '%v': %s", f.config.Options, err)
132163

133164
if len(out) > 0 {
@@ -137,6 +168,16 @@ func (f *Formatter) Apply(ctx context.Context, files []*walk.File) error {
137168
return fmt.Errorf("formatter '%s' with options '%v' failed to apply: %w", f.config.Command, f.config.Options, err)
138169
}
139170

171+
// In stdin mode, the formatter won't write to the filesystem, it instead
172+
// prints the formatted file to stdout. Take that output and write it back
173+
// to the input file (`TmpPath`).
174+
if useStdinMode {
175+
path := onlyFile.TmpPath
176+
if err = os.WriteFile(path, out, 0o600); err != nil {
177+
return fmt.Errorf("couldn't write to '%s'", path)
178+
}
179+
}
180+
140181
f.log.Infof("%v file(s) processed in %v", len(files), time.Since(start))
141182

142183
return nil

nix/packages/treefmt/formatters.nix

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,35 @@ with pkgs; [
6969
test-fmt-append "suffix" "$1"
7070
'';
7171
})
72+
(pkgs.writeShellApplication {
73+
name = "test-fmt-embed-path";
74+
text = ''
75+
stdin_filepath=""
76+
while [[ $# -gt 0 ]]; do
77+
case $1 in
78+
--stdin-filepath)
79+
shift
80+
stdin_filepath=$1
81+
shift
82+
;;
83+
--* | -*)
84+
echo "Unknown option $1" >&2
85+
exit 1
86+
;;
87+
*)
88+
echo "Unknown positional argument $1" >&2
89+
exit 1
90+
;;
91+
esac
92+
done
93+
94+
if [ -z "$stdin_filepath" ]; then
95+
echo "I only support stdin mode (use --stdin-filepath [path/to/file])." >&2
96+
exit 1
97+
fi
98+
99+
echo "# $stdin_filepath"
100+
cat /dev/stdin
101+
'';
102+
})
72103
]

test/examples/treefmt.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,8 @@ command = "foo-fmt"
9494
command = "just"
9595
options = ["--fmt", "--unstable", "--justfile"]
9696
includes = ["**/justfile"]
97+
98+
[formatter.embed-path]
99+
command = "test-fmt-embed-path"
100+
includes = ["*.embed-path"]
101+
stdin-options = ["--stdin-filepath", "$path"]

0 commit comments

Comments
 (0)