mirror of
https://github.com/numtide/treefmt.git
synced 2024-10-03 20:18:11 +03:00
fix: --stdin flag
This was incorrectly ported from Rust to Go. When provided, `treefmt` will take the contents of stdin and place them into the file provided with the `--stdin` flag, then format it according to the configured formatters. If the file doesn't exist it is created. If it exists, it is first truncated and then populated with stdin. Signed-off-by: Brian McGee <brian@bmcgee.ie>
This commit is contained in:
parent
2454542a36
commit
9934a5764d
@ -25,8 +25,8 @@ type Format struct {
|
||||
|
||||
OnUnmatched log.Level `name:"on-unmatched" short:"u" default:"warn" help:"Log paths that did not match any formatters at the specified log level, with fatal exiting the process with an error. Possible values are <debug|info|warn|error|fatal>."`
|
||||
|
||||
Paths []string `name:"paths" arg:"" type:"path" optional:"" help:"Paths to format. Defaults to formatting the whole tree."`
|
||||
Stdin bool `help:"Format the context passed in via stdin."`
|
||||
Paths []string `name:"paths" arg:"" type:"path" optional:"" help:"Paths to format. Defaults to formatting the whole tree." xor:"paths"`
|
||||
Stdin string `type:"path" optional:"" help:"Format stdin, placing the output into the provided path. Formatters are matched based on the path's file extension." xor:"paths"`
|
||||
|
||||
CpuProfile string `optional:"" help:"The file into which a cpu profile will be written."`
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.numtide.com/numtide/treefmt/format"
|
||||
@ -221,66 +220,30 @@ func updateCache(ctx context.Context) func() error {
|
||||
|
||||
func walkFilesystem(ctx context.Context) func() error {
|
||||
return func() error {
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
pathsCh := make(chan string, BatchSize)
|
||||
// 1. Check if we have been provided with an explicit list of paths to process
|
||||
// 2. If not, check if we have been passed in some content to format via stdin
|
||||
// 3. If not, we process the tree root as normal.
|
||||
paths := Cli.Paths
|
||||
if Cli.Stdin != "" {
|
||||
|
||||
walkPaths := func() error {
|
||||
defer close(pathsCh)
|
||||
|
||||
var idx int
|
||||
for idx < len(Cli.Paths) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
pathsCh <- Cli.Paths[idx]
|
||||
idx += 1
|
||||
}
|
||||
// read from stdin and place the contents into the provided path before processing
|
||||
if err := os.MkdirAll(filepath.Dir(Cli.Stdin), 0o755); err != nil {
|
||||
return fmt.Errorf("failed to ensure the directory existed for stdin processing: %w", err)
|
||||
} else if file, err := os.Create(Cli.Stdin); err != nil {
|
||||
return fmt.Errorf("failed to open file for stdin processing: %w", err)
|
||||
} else if _, err = io.Copy(file, os.Stdin); err != nil {
|
||||
return fmt.Errorf("failed to read stdin: %w", err)
|
||||
} else if err = file.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close file for stdin processing: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
walkStdin := func() error {
|
||||
defer close(pathsCh)
|
||||
|
||||
// determine the current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine current working directory: %w", err)
|
||||
}
|
||||
|
||||
// read in all the paths
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
path := scanner.Text()
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
// append the cwd
|
||||
path = filepath.Join(cwd, path)
|
||||
}
|
||||
pathsCh <- path
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(Cli.Paths) > 0 {
|
||||
eg.Go(walkPaths)
|
||||
} else if Cli.Stdin {
|
||||
eg.Go(walkStdin)
|
||||
} else {
|
||||
// no explicit paths to process, so we only need to process root
|
||||
pathsCh <- Cli.TreeRoot
|
||||
close(pathsCh)
|
||||
paths = []string{Cli.Stdin}
|
||||
} else if len(paths) == 0 {
|
||||
paths = []string{Cli.TreeRoot}
|
||||
}
|
||||
|
||||
// create a filesystem walker
|
||||
walker, err := walk.New(Cli.Walk, Cli.TreeRoot, pathsCh)
|
||||
walker, err := walk.New(Cli.Walk, Cli.TreeRoot, paths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create walker: %w", err)
|
||||
}
|
||||
|
@ -573,54 +573,52 @@ func TestStdIn(t *testing.T) {
|
||||
// capture current cwd, so we can replace it after the test is finished
|
||||
cwd, err := os.Getwd()
|
||||
as.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
// return to the previous working directory
|
||||
as.NoError(os.Chdir(cwd))
|
||||
})
|
||||
|
||||
tempDir := test.TempExamples(t)
|
||||
configPath := filepath.Join(tempDir, "/treefmt.toml")
|
||||
|
||||
// change working directory to temp root
|
||||
as.NoError(os.Chdir(tempDir))
|
||||
|
||||
// basic config
|
||||
cfg := config.Config{
|
||||
Formatters: map[string]*config.Formatter{
|
||||
"echo": {
|
||||
Command: "echo",
|
||||
Includes: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
test.WriteConfig(t, configPath, cfg)
|
||||
|
||||
// swap out stdin
|
||||
// capture current stdin and replace it on test cleanup
|
||||
prevStdIn := os.Stdin
|
||||
stdin, err := os.CreateTemp("", "stdin")
|
||||
as.NoError(err)
|
||||
|
||||
os.Stdin = stdin
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.Stdin = prevStdIn
|
||||
_ = os.Remove(stdin.Name())
|
||||
})
|
||||
|
||||
go func() {
|
||||
_, err := stdin.WriteString(`treefmt.toml
|
||||
elm/elm.json
|
||||
go/main.go
|
||||
`)
|
||||
as.NoError(err, "failed to write to stdin")
|
||||
as.NoError(stdin.Sync())
|
||||
_, _ = stdin.Seek(0, 0)
|
||||
}()
|
||||
// write a new file
|
||||
contents := `{ foo, ... }: "hello"`
|
||||
os.Stdin = test.TempFile(t, "", "stdin", &contents)
|
||||
|
||||
_, err = cmd(t, "-C", tempDir, "--stdin")
|
||||
outPath := "foo/bar/test.nix"
|
||||
_, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", outPath)
|
||||
as.NoError(err)
|
||||
assertStats(t, as, 3, 3, 3, 0)
|
||||
assertStats(t, as, 1, 1, 1, 1)
|
||||
|
||||
// the nix formatters should have reduced the example to the following
|
||||
as.Equal(`{ ...}: "hello"
|
||||
`, string(test.ReadFile(t, outPath)))
|
||||
|
||||
// overwrite an existing file
|
||||
contents = `
|
||||
| col1 | col2 |
|
||||
| ---- | ---- |
|
||||
| nice | fits |
|
||||
| oh no! | it's ugly |
|
||||
`
|
||||
os.Stdin = test.TempFile(t, "", "stdin", &contents)
|
||||
|
||||
outPath = "haskell/CHANGELOG.md"
|
||||
out, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", outPath, "-vv")
|
||||
println(out)
|
||||
as.NoError(err)
|
||||
assertStats(t, as, 1, 1, 1, 1)
|
||||
|
||||
as.Equal(`| col1 | col2 |
|
||||
| ------ | --------- |
|
||||
| nice | fits |
|
||||
| oh no! | it's ugly |
|
||||
`, string(test.ReadFile(t, outPath)))
|
||||
}
|
||||
|
||||
func TestDeterministicOrderingInPipeline(t *testing.T) {
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
@ -42,7 +41,7 @@ func cmd(t *testing.T, args ...string) ([]byte, error) {
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
tempOut := test.TempFile(t, filepath.Join(tempDir, "combined_output"))
|
||||
tempOut := test.TempFile(t, tempDir, "combined_output", nil)
|
||||
|
||||
// capture standard outputs before swapping them
|
||||
stdout := os.Stdout
|
||||
|
@ -65,7 +65,7 @@ func TestReadConfigFile(t *testing.T) {
|
||||
deadnix, ok := cfg.Formatters["deadnix"]
|
||||
as.True(ok, "deadnix formatter not found")
|
||||
as.Equal("deadnix", deadnix.Command)
|
||||
as.Nil(deadnix.Options)
|
||||
as.Equal([]string{"-e"}, deadnix.Options)
|
||||
as.Equal([]string{"*.nix"}, deadnix.Includes)
|
||||
as.Nil(deadnix.Excludes)
|
||||
as.Equal(2, deadnix.Priority)
|
||||
|
@ -13,22 +13,23 @@ Arguments:
|
||||
[<paths> ...] Paths to format. Defaults to formatting the whole tree.
|
||||
|
||||
Flags:
|
||||
-h, --help Show context-sensitive help.
|
||||
--allow-missing-formatter Do not exit with error if a configured formatter is missing.
|
||||
-C, --working-directory="." Run as if treefmt was started in the specified working directory instead of the current working directory.
|
||||
--no-cache Ignore the evaluation cache entirely. Useful for CI.
|
||||
-c, --clear-cache Reset the evaluation cache. Use in case the cache is not precise enough.
|
||||
--config-file="./treefmt.toml" The config file to use.
|
||||
--fail-on-change Exit with error if any changes were made. Useful for CI.
|
||||
--formatters=FORMATTERS,... Specify formatters to apply. Defaults to all formatters.
|
||||
--tree-root="." The root directory from which treefmt will start walking the filesystem.
|
||||
--walk="auto" The method used to traverse the files within --tree-root. Currently supports 'auto', 'git' or 'filesystem'.
|
||||
-v, --verbose Set the verbosity of logs e.g. -vv ($LOG_LEVEL).
|
||||
-V, --version Print version.
|
||||
-i, --init Create a new treefmt.toml.
|
||||
-u, --on-unmatched=warn Log paths that did not match any formatters at the specified log level, with fatal exiting the process with an error. Possible values are <debug|info|warn|error|fatal>.
|
||||
--stdin Format the context passed in via stdin.
|
||||
--cpu-profile=STRING The file into which a cpu profile will be written.
|
||||
-h, --help Show context-sensitive help.
|
||||
--allow-missing-formatter Do not exit with error if a configured formatter is missing.
|
||||
-C, --working-directory="." Run as if treefmt was started in the specified working directory instead of the current working directory.
|
||||
--no-cache Ignore the evaluation cache entirely. Useful for CI.
|
||||
-c, --clear-cache Reset the evaluation cache. Use in case the cache is not precise enough.
|
||||
--config-file=STRING Load the config file from the given path (defaults to searching upwards for treefmt.toml).
|
||||
--fail-on-change Exit with error if any changes were made. Useful for CI.
|
||||
-f, --formatters=FORMATTERS,... Specify formatters to apply. Defaults to all formatters.
|
||||
--tree-root=STRING The root directory from which treefmt will start walking the filesystem (defaults to the directory containing the config file).
|
||||
--tree-root-file=STRING File to search for to find the project root (if --tree-root is not passed).
|
||||
--walk="auto" The method used to traverse the files within --tree-root. Currently supports 'auto', 'git' or 'filesystem'.
|
||||
-v, --verbose Set the verbosity of logs e.g. -vv ($LOG_LEVEL).
|
||||
-V, --version Print version.
|
||||
-i, --init Create a new treefmt.toml.
|
||||
-u, --on-unmatched=warn Log paths that did not match any formatters at the specified log level, with fatal exiting the process with an error. Possible values are <debug|info|warn|error|fatal>.
|
||||
--stdin=STRING Format stdin, placing the output into the provided path. Formatters are matched based on the path's file extension.
|
||||
--cpu-profile=STRING The file into which a cpu profile will be written.
|
||||
```
|
||||
|
||||
## Arguments
|
||||
@ -102,9 +103,9 @@ Log paths that did not match any formatters at the specified log level, with fat
|
||||
|
||||
[default: warn]
|
||||
|
||||
### `--stdin`
|
||||
### `--stdin=STRING`
|
||||
|
||||
Format the context passed in via stdin.
|
||||
Format stdin, placing the output into the provided path. Formatters are matched based on the path's file extension.
|
||||
|
||||
### `--cpu-profile`
|
||||
|
||||
|
1
foo/bar.md
Normal file
1
foo/bar.md
Normal file
@ -0,0 +1 @@
|
||||
# hello world
|
@ -35,6 +35,7 @@ priority = 1
|
||||
|
||||
[formatter.deadnix]
|
||||
command = "deadnix"
|
||||
options = ["-e"]
|
||||
includes = ["*.nix"]
|
||||
priority = 2
|
||||
|
||||
|
30
test/temp.go
30
test/temp.go
@ -1,6 +1,7 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@ -29,15 +30,36 @@ func TempExamples(t *testing.T) string {
|
||||
return tempDir
|
||||
}
|
||||
|
||||
func TempFile(t *testing.T, path string) *os.File {
|
||||
func TempFile(t *testing.T, dir string, pattern string, contents *string) *os.File {
|
||||
t.Helper()
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temporary file: %v", err)
|
||||
|
||||
file, err := os.CreateTemp(dir, pattern)
|
||||
require.NoError(t, err, "failed to create temp file")
|
||||
|
||||
if contents == nil {
|
||||
return file
|
||||
}
|
||||
|
||||
_, err = file.WriteString(*contents)
|
||||
require.NoError(t, err, "failed to write contents to temp file")
|
||||
require.NoError(t, file.Close(), "failed to close temp file")
|
||||
|
||||
file, err = os.Open(file.Name())
|
||||
require.NoError(t, err, "failed to open temp file")
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func ReadFile(t *testing.T, path string) []byte {
|
||||
f, err := os.Open(path)
|
||||
require.NoError(t, err, "failed to open file")
|
||||
defer f.Close()
|
||||
|
||||
bytes, err := io.ReadAll(f)
|
||||
require.NoError(t, err, "failed to read file")
|
||||
return bytes
|
||||
}
|
||||
|
||||
func RecreateSymlink(t *testing.T, path string) error {
|
||||
t.Helper()
|
||||
src, err := os.Readlink(path)
|
||||
|
@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
type filesystemWalker struct {
|
||||
root string
|
||||
pathsCh chan string
|
||||
root string
|
||||
paths []string
|
||||
}
|
||||
|
||||
func (f filesystemWalker) Root() string {
|
||||
@ -34,7 +34,7 @@ func (f filesystemWalker) Walk(_ context.Context, fn WalkFunc) error {
|
||||
return fn(&file, err)
|
||||
}
|
||||
|
||||
for path := range f.pathsCh {
|
||||
for _, path := range f.paths {
|
||||
if err := filepath.Walk(path, walkFn); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -43,6 +43,6 @@ func (f filesystemWalker) Walk(_ context.Context, fn WalkFunc) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFilesystem(root string, paths chan string) (Walker, error) {
|
||||
func NewFilesystem(root string, paths []string) (Walker, error) {
|
||||
return filesystemWalker{root, paths}, nil
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
|
||||
type gitWalker struct {
|
||||
root string
|
||||
paths chan string
|
||||
paths []string
|
||||
repo *git.Repository
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ func (g *gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
|
||||
// cache in-memory whether a path is present in the git index
|
||||
var cache map[string]bool
|
||||
|
||||
for path := range g.paths {
|
||||
for _, path := range g.paths {
|
||||
|
||||
if path == g.root {
|
||||
// we can just iterate the index entries
|
||||
@ -116,7 +116,7 @@ func (g *gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewGit(root string, paths chan string) (Walker, error) {
|
||||
func NewGit(root string, paths []string) (Walker, error) {
|
||||
repo, err := git.PlainOpen(root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open git repo: %w", err)
|
||||
|
@ -31,24 +31,24 @@ type Walker interface {
|
||||
Walk(ctx context.Context, fn WalkFunc) error
|
||||
}
|
||||
|
||||
func New(walkerType Type, root string, pathsCh chan string) (Walker, error) {
|
||||
func New(walkerType Type, root string, paths []string) (Walker, error) {
|
||||
switch walkerType {
|
||||
case Git:
|
||||
return NewGit(root, pathsCh)
|
||||
return NewGit(root, paths)
|
||||
case Auto:
|
||||
return Detect(root, pathsCh)
|
||||
return Detect(root, paths)
|
||||
case Filesystem:
|
||||
return NewFilesystem(root, pathsCh)
|
||||
return NewFilesystem(root, paths)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown walker type: %v", walkerType)
|
||||
}
|
||||
}
|
||||
|
||||
func Detect(root string, pathsCh chan string) (Walker, error) {
|
||||
func Detect(root string, paths []string) (Walker, error) {
|
||||
// for now, we keep it simple and try git first, filesystem second
|
||||
w, err := NewGit(root, pathsCh)
|
||||
w, err := NewGit(root, paths)
|
||||
if err == nil {
|
||||
return w, err
|
||||
}
|
||||
return NewFilesystem(root, pathsCh)
|
||||
return NewFilesystem(root, paths)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user