From 9d65878302d8a5a9081241db8ab65a00e2a10d2b Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Tue, 2 Jun 2026 00:04:03 +0200 Subject: [PATCH] format command line command --- .gitignore | 1 + Makefile | 2 +- cmd/treerack/baz_test.treerack | 2 + cmd/treerack/docreflect.gen.go | 10 ++ cmd/treerack/format.go | 116 +++++++++++++++++++++++ cmd/treerack/format_test.go | 162 +++++++++++++++++++++++++++++++++ cmd/treerack/main.go | 3 +- cmd/treerack/readme.md | 21 +++++ 8 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 cmd/treerack/baz_test.treerack create mode 100644 cmd/treerack/format.go create mode 100644 cmd/treerack/format_test.go diff --git a/.gitignore b/.gitignore index 92d172f..ddc1882 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .coverprofile .coverprofile-cmd .build +treerack.test diff --git a/Makefile b/Makefile index bb73590..0f25058 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ sources = $(shell find . -name '*.go' \ | grep -v .build/headexported.go \ | grep -v internal/self/self.go \ | grep -v .build/self.go) -parsers = $(shell find . -name '*.treerack') +parsers = $(shell find . -name '*.treerack' | grep -v baz_test[.]treerack) release_date = $(shell git show -s --format=%cs HEAD) version = $(release_date)-$(shell git rev-parse --short HEAD) PREFIX ?= /usr/local diff --git a/cmd/treerack/baz_test.treerack b/cmd/treerack/baz_test.treerack new file mode 100644 index 0000000..d977819 --- /dev/null +++ b/cmd/treerack/baz_test.treerack @@ -0,0 +1,2 @@ +# invalid comment +a = "42"; diff --git a/cmd/treerack/docreflect.gen.go b/cmd/treerack/docreflect.gen.go index d2f5c3d..525f0c2 100644 --- a/cmd/treerack/docreflect.gen.go +++ b/cmd/treerack/docreflect.gen.go @@ -21,6 +21,16 @@ func init() { docreflect.Register("main.errInvalidFilename", "") docreflect.Register("main.errMultipleInputs", "") docreflect.Register("main.errNoInput", "") + docreflect.Register("main.format", "format input syntax. Accepts syntax from one or more files, inline syntax string or stdin. Use the --in-place\noption, when formatting files in place, or print the formatted syntax to stdout.\n\nfunc(o, stdin, stdout, syntax)") + docreflect.Register("main.formatFile", "\nfunc(name, inPlace, out)") + docreflect.Register("main.formatFiles", "\nfunc(files, inPlace, out)") + docreflect.Register("main.formatInline", "\nfunc(syntax, out)") + docreflect.Register("main.formatOptions", "") + docreflect.Register("main.formatOptions.InPlace", "") + docreflect.Register("main.formatOptions.Syntax", "") + docreflect.Register("main.formatOptions.SyntaxString", "") + docreflect.Register("main.formatStdin", "\nfunc(in, out)") + docreflect.Register("main.formatSyntax", "\nfunc(in, out)") docreflect.Register("main.generate", "generate generates Go code that can parse arbitrary input with the provided syntax, and can be used embedded\nin an application.\n\nThe syntax may be provided via a file path (using an option or a positional argument), an\ninline string, or piped from standard input.\n\nfunc(o, stdin, stdout, args)") docreflect.Register("main.generateOptions", "") docreflect.Register("main.generateOptions.Export", "Export determines whether the generated parse function is exported (visible outside its package).\n") diff --git a/cmd/treerack/format.go b/cmd/treerack/format.go new file mode 100644 index 0000000..e8fa1e4 --- /dev/null +++ b/cmd/treerack/format.go @@ -0,0 +1,116 @@ +package main + +import ( + "bytes" + "code.squareroundforest.org/arpio/treerack" + "errors" + "fmt" + "io" + "os" +) + +type formatOptions struct { + InPlace bool + SyntaxString *string + Syntax []string +} + +func formatSyntax(in io.Reader, out io.Writer) error { + s := new(treerack.Syntax) + if err := s.ReadSyntax(in); err != nil { + return err + } + + if resetter, ok := out.(interface{ Reset() }); ok { + resetter.Reset() + } + + if err := s.Format(out); err != nil { + return err + } + + return nil +} + +func formatFile(name string, inPlace bool, out io.Writer) error { + var ( + inBytes []byte + buf *bytes.Buffer + err error + ) + + if inBytes, err = os.ReadFile(name); err != nil { + return err + } + + buf = bytes.NewBuffer(inBytes) + if err = formatSyntax(buf, buf); err != nil { + return err + } + + if !inPlace { + _, err = io.Copy(out, buf) + return err + } + + if bytes.Equal(buf.Bytes(), inBytes) { + return nil + } + + fmt.Fprintln(os.Stderr, name) + return os.WriteFile(name, buf.Bytes(), 0644) +} + +func formatFiles(files []string, inPlace bool, out io.Writer) error { + for _, f := range files { + if err := formatFile(f, inPlace, out); err != nil { + return err + } + } + + return nil +} + +func formatInline(syntax string, out io.Writer) error { + buf := bytes.NewBufferString(syntax) + return formatSyntax(buf, out) +} + +func formatStdin(in io.Reader, out io.Writer) error { + return formatSyntax(in, out) +} + +// format input syntax. Accepts syntax from one or more files, inline syntax string or stdin. Use the --in-place +// option, when formatting files in place, or print the formatted syntax to stdout. +func format(o formatOptions, stdin io.Reader, stdout io.Writer, syntax ...string) error { + files := make([]string, 0, len(o.Syntax)+len(syntax)) + files = append(files, o.Syntax...) + files = append(files, syntax...) + if o.SyntaxString != nil { + if len(files) > 0 { + return errors.New( + "accepted input: either inline syntax, or one or more syntax files, or stdin", + ) + } + } + + if o.InPlace { + if o.SyntaxString != nil { + return errors.New("cannot format inline syntax in place") + } + + if len(files) == 0 { + return errors.New("cannot format stdin in place") + } + } + + if len(files) > 0 { + return formatFiles(files, o.InPlace, stdout) + } + + if o.SyntaxString != nil { + return formatInline(*o.SyntaxString, stdout) + } + + return formatSyntax(stdin, stdout) +} diff --git a/cmd/treerack/format_test.go b/cmd/treerack/format_test.go new file mode 100644 index 0000000..436f265 --- /dev/null +++ b/cmd/treerack/format_test.go @@ -0,0 +1,162 @@ +package main + +import ( + "bytes" + "os" + "testing" +) + +func TestFormat(t *testing.T) { + t.Run("syntax string and file from option", func(t *testing.T) { + o := formatOptions{ + SyntaxString: ptrto(`a = "42"`), + Syntax: []string{"foo_test.treerack"}, + } + + if err := format(o, bytes.NewBuffer(nil), bytes.NewBuffer(nil)); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("syntax string and file from arg", func(t *testing.T) { + o := formatOptions{SyntaxString: ptrto(`a = "42"`)} + if err := format(o, bytes.NewBuffer(nil), bytes.NewBuffer(nil), "foo_test.treerack"); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("syntax string in place", func(t *testing.T) { + o := formatOptions{ + SyntaxString: ptrto(`a = "42"`), + InPlace: true, + } + + if err := format(o, bytes.NewBuffer(nil), bytes.NewBuffer(nil)); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("stdin in place", func(t *testing.T) { + o := formatOptions{InPlace: true} + if err := format(o, bytes.NewBuffer(nil), bytes.NewBuffer(nil)); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("files:", func(t *testing.T) { + t.Run("read error", func(t *testing.T) { + if err := format( + formatOptions{}, + bytes.NewBuffer(nil), + bytes.NewBuffer(nil), + "bar_test.treerack", + ); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("syntax error", func(t *testing.T) { + if err := format( + formatOptions{}, + bytes.NewBuffer(nil), + bytes.NewBuffer(nil), + "baz_test.treerack", + ); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("to stdout", func(t *testing.T) { + stdout := bytes.NewBuffer(nil) + if err := format( + formatOptions{}, + bytes.NewBuffer(nil), + stdout, + "foo_test.treerack", + ); err != nil { + t.Fatal("failed to fail") + } + + if stdout.Len() == 0 { + t.Fatal("failed to write out contents") + } + }) + + t.Run("in place no change", func(t *testing.T) { + c, err := os.ReadFile("foo_test.treerack") + if err != nil { + t.Fatal(err) + } + + o := formatOptions{InPlace: true} + if err := format( + o, + bytes.NewBuffer(nil), + bytes.NewBuffer(nil), + "foo_test.treerack", + ); err != nil { + t.Fatal("failed to fail") + } + + cc, err := os.ReadFile("foo_test.treerack") + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(cc, c) { + t.Fatal("failed to leave the file intact") + } + }) + + t.Run("in place", func(t *testing.T) { + c, err := os.ReadFile("foo_test.treerack") + if err != nil { + t.Fatal(err) + } + + cc := c[:len(c)-1] + if err := os.WriteFile("foo_test.treerack", cc, 0644); err != nil { + t.Fatal(err) + } + + o := formatOptions{InPlace: true} + if err := format( + o, + bytes.NewBuffer(nil), + bytes.NewBuffer(nil), + "foo_test.treerack", + ); err != nil { + t.Fatal("failed to fail") + } + + ccc, err := os.ReadFile("foo_test.treerack") + if !bytes.Equal(ccc, c) { + t.Fatal("failed to format file") + } + }) + }) + + t.Run("syntax string", func(t *testing.T) { + o := formatOptions{SyntaxString: ptrto(`a="42"`)} + stdout := bytes.NewBuffer(nil) + if err := format(o, bytes.NewBuffer(nil), stdout); err != nil { + t.Fatal(err) + } + + if stdout.String() != `a = "42";`+"\n" { + t.Fatal("failed to format syntax string") + } + }) + + t.Run("stdin", func(t *testing.T) { + stdin := bytes.NewBufferString(`a="42"`) + stdout := bytes.NewBuffer(nil) + if err := format(formatOptions{}, stdin, stdout); err != nil { + t.Fatal(err) + } + + if stdout.String() != `a = "42";`+"\n" { + t.Fatal("failed to format input") + } + }) +} diff --git a/cmd/treerack/main.go b/cmd/treerack/main.go index ec487a1..bf3dc25 100644 --- a/cmd/treerack/main.go +++ b/cmd/treerack/main.go @@ -9,5 +9,6 @@ func main() { check := Args(Command("check", check), 0, 1) show := Args(Command("show", show), 0, 1) generate := Args(Command("generate", generate), 0, 1) - Exec(Version(Group("treerack", checkSyntax, check, show, generate), version)) + format := Command("format", format) + Exec(Version(Group("treerack", checkSyntax, check, show, generate, format), version)) } diff --git a/cmd/treerack/readme.md b/cmd/treerack/readme.md index 281e84f..2760c9c 100644 --- a/cmd/treerack/readme.md +++ b/cmd/treerack/readme.md @@ -115,6 +115,27 @@ piped from standard input. - --syntax-string string: specifies the syntax as an inline string. - --help: Show help. +### treerack format + +#### Synopsis: + +``` +treerack format [options]... [--] [syntax string]... +treerack format +``` + +#### Description: + +input syntax. Accepts syntax from one or more files, inline syntax string or stdin. Use the --in-place option, +when formatting files in place, or print the formatted syntax to stdout. + +#### Options: + +- --in-place bool: +- --syntax string \[\*\]: +- --syntax-string string: +- --help: Show help. + ### treerack version Show version.