diff --git a/.gitignore b/.gitignore index 481317c..12b096d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ .coverprofile .coverprofile-cmd codecov -cmd/treerack/treerack +.build diff --git a/Makefile b/Makefile index 13a2ce6..50c43a8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ -sources = $(shell find . -name '*.go') +sources = $(shell find . -name '*.go' | grep -v cmd/treerack/docreflect.gen.go) parsers = $(shell find . -name '*.treerack') +release_date = $(shell git show -s --format=%cs HEAD) +version = $(date)-$(shell git rev-parse --short HEAD) .PHONY: cpu.out @@ -13,9 +15,16 @@ imports: $(sources) @echo imports @goimports -w $(sources) -build: $(sources) +cmd/treerack/docreflect.gen.go: $(sources) + go run scripts/docreflect.go > .build/docreflect.gen.go && \ + mv .build/docreflect.gen.go cmd/treerack || \ + rm .build/docreflect.gen.go + +build: $(sources) cmd/treerack/readme.md cmd/treerack/docreflect.gen.go .build/treerack .build/treerack.1 + +.build/treerack: go build - go build -o cmd/treerack/treerack ./cmd/treerack + go build -o .build/treerack ./cmd/treerack install: $(sources) go install ./cmd/treerack @@ -91,6 +100,17 @@ check-generate: $(sources) $(parsers) @mv headexported.go.backup headexported.go @mv self/self.go.backup self/self.go +.build: + mkdir -p .build + +cmd/treerack/readme.md: $(sources) cmd/treerack/docreflect.gen.go + go run scripts/cmdreadme.go ./cmd/treerack > cmd/treerack/readme.md || \ + rm cmd/treerack/readme.md + +.build/treerack.1: $(sources) cmd/treerack/docreflect.gen.go + go run scripts/man.go $(version) $(release_date) > .build/treerack.1 || \ + rm .build/treerack.1 + check: build $(parsers) go test -test.short -run ^Test go test ./cmd/treerack -test.short -run ^Test @@ -144,6 +164,10 @@ clean: rm -f cpu.out rm -f .coverprofile go clean -i ./... + rm -f .build/treerack + rm -f .build/treerack.1 + rm -f cmd/treerack/docreflect.gen.go + rm -f cmd/treerack/readme.md ci-trigger: deps checkfmt build checkall ifeq ($(TRAVIS_BRANCH)_$(TRAVIS_PULL_REQUEST), master_false) diff --git a/cmd/treerack/check.go b/cmd/treerack/check.go index 6b9c712..22c5690 100644 --- a/cmd/treerack/check.go +++ b/cmd/treerack/check.go @@ -1,49 +1,50 @@ package main +import ( + "code.squareroundforest.org/arpio/treerack" + "io" +) + type checkOptions struct { - command *commandOptions - syntax *fileOptions - input *fileOptions + + // Syntax specifies the filename of the syntax definition file. + Syntax string + + // SyntaxString specifies the syntax as an inline string. + SyntaxString string + + // Input specifies the filename of the input content to be validated. + Input string + + // InputString specifies the input content as an inline string. + InputString string } -func check(args []string) int { - var o checkOptions - o.command = initOptions(checkUsage, checkExample, positionalInputUsage, args) - o.syntax = &fileOptions{typ: "syntax", flagSet: o.command.flagSet, positionalDoc: positionalInputUsage} - o.input = &fileOptions{typ: "input", flagSet: o.command.flagSet, positionalDoc: positionalInputUsage} - - o.command.stringFlag(&o.syntax.inline, "syntax-string", syntaxStringUsage) - o.command.stringFlag(&o.syntax.fileName, "syntax", syntaxFileUsage) - - o.command.stringFlag(&o.input.inline, "input-string", inputStringUsage) - o.command.stringFlag(&o.input.fileName, "input", inputFileUsage) - - if o.command.help() { - return 0 - } - - if code := o.command.parseArgs(); code != 0 { - return code - } - - s, code := o.syntax.openSyntax() - if code != 0 { - return code - } - - o.input.positional = o.command.flagSet.Args() - input, code := o.input.open() - if code != 0 { - return code - } - - defer input.Close() - - _, err := s.Parse(input) +// check parses input content against the provided syntax definition and fails if the input does not match. +// Syntax can be provided via a filename option or an inline string option. Input can be provided via a filename +// option, a positional argument filename, an inline string option, or piped from standard input. +func check(o checkOptions, stdin io.Reader, args ...string) error { + syntax, finalizeSyntax, err := initInput(o.Syntax, o.SyntaxString, nil, nil) if err != nil { - stderr(err) - return -1 + return err } - return 0 + defer finalizeSyntax() + input, finalizeInput, err := initInput(o.Input, o.InputString, stdin, args) + if err != nil { + return err + } + + defer finalizeInput() + s := &treerack.Syntax{} + if err := s.ReadSyntax(syntax); err != nil { + return err + } + + if err := s.Init(); err != nil { + return err + } + + _, err = s.Parse(input) + return err } diff --git a/cmd/treerack/check_test.go b/cmd/treerack/check_test.go index 220dc29..5095e59 100644 --- a/cmd/treerack/check_test.go +++ b/cmd/treerack/check_test.go @@ -1,235 +1,146 @@ package main -import "testing" - -var checkFailureTests = []mainTest{ - { - title: "invalid flag", - args: []string{ - "treerack", "check", "-foo", - }, - exit: -1, - stderr: []string{ - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - }, - }, - - { - title: "multiple syntaxes", - args: []string{ - "treerack", "check", "-syntax", "foo.treerack", "-syntax-string", `foo = "bar"`, "-input-string", "bar", - }, - exit: -1, - stderr: []string{ - "only one syntax", - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - }, - }, - - { - title: "multiple inputs", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "-input", "foo.txt", "-input-string", "bar", - }, - exit: -1, - stderr: []string{ - "only one input", - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - }, - }, - - { - title: "multiple inputs, positional", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "foo.txt", "bar.txt", - }, - exit: -1, - stderr: []string{ - "only one input", - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - }, - }, - - { - title: "multiple inputs, positional and explicit file", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "-input", "foo.txt", "bar.txt", - }, - exit: -1, - stderr: []string{ - "only one input", - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - }, - }, - - { - title: "no syntax", - args: []string{ - "treerack", "check", "-input-string", "foo", - }, - exit: -1, - stderr: []string{ - "missing syntax", - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - }, - }, - - { - title: "no input", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, - }, - exit: -1, - stderr: []string{ - "missing input", - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - }, - }, - - { - title: "invalid syntax", - args: []string{ - "treerack", "check", "-syntax-string", "foo", "-input-string", "foo", - }, - exit: -1, - stderr: []string{ - "parse failed", - }, - }, - - { - title: "syntax file open fails", - args: []string{ - "treerack", "check", "-syntax", "noexist.treerack", "-input-string", "foo", - }, - exit: -1, - stderr: []string{ - "file", - }, - }, - - { - title: "input file open fails", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "-input", "noexist.txt", - }, - exit: -1, - stderr: []string{ - "file", - }, - }, - - { - title: "invalid input", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "-input-string", "foo", - }, - exit: -1, - stderr: []string{ - "parse failed", - }, - }, -} - -var checkTests = []mainTest{ - { - title: "syntax as file", - args: []string{ - "treerack", "check", "-syntax", "foo_test.treerack", "-input-string", "bar", - }, - }, - - { - title: "syntax as string", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "-input-string", "bar", - }, - }, - - { - title: "input as stdin", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, - }, - stdin: "bar", - }, - - { - title: "input as file", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "-input", "bar_test.txt", - }, - }, - - { - title: "input as positional", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "bar_test.txt", - }, - }, - - { - title: "input as string", - args: []string{ - "treerack", "check", "-syntax-string", `foo = "bar"`, "-input-string", "bar", - }, - }, - - { - title: "explicit over stdin", - args: []string{ - "treerack", "check", "-syntax", "foo_test.treerack", "-input-string", "bar", - }, - stdin: "invalid", - }, -} +import ( + "bytes" + "code.squareroundforest.org/arpio/treerack" + "errors" + "os" + "testing" +) func TestCheck(t *testing.T) { - runMainTest(t, mainTest{ - title: "help", - args: []string{ - "treerack", "check", "-help", - }, - stdout: []string{ - wrapLines(checkUsage), - "-syntax", - "-syntax-string", - "-input", - "-input-string", - wrapLines(positionalInputUsage), - wrapLines(checkExample), - wrapLines(docRef), - }, + t.Run("no syntax", func(t *testing.T) { + o := checkOptions{Input: "bar_test.txt"} + if err := check(o, nil); !errors.Is(err, errNoInput) { + t.Fatal() + } }) - runMainTest(t, checkFailureTests...) - runMainTest(t, checkTests...) + t.Run("too many syntaxes", func(t *testing.T) { + o := checkOptions{ + Syntax: "foo_test.treerack", + SyntaxString: `foo = "baz"`, + Input: "bar_test.txt", + } + + if err := check(o, nil); !errors.Is(err, errMultipleInputs) { + t.Fatal() + } + }) + + t.Run("syntax file not found", func(t *testing.T) { + o := checkOptions{ + Syntax: "no-file.treerack", + Input: "bar_test.txt", + } + + if err := check(o, nil); !os.IsNotExist(err) { + t.Fatal() + } + }) + + t.Run("invalid syntax definition", func(t *testing.T) { + o := checkOptions{ + SyntaxString: `foo`, + Input: "bar_test.txt", + } + + var perr *treerack.ParseError + if err := check(o, nil); !errors.As(err, &perr) { + t.Fatal() + } + }) + + t.Run("invalid syntax init", func(t *testing.T) { + o := checkOptions{ + SyntaxString: `foo = "bar"; foo = "baz"`, + Input: "bar_test.txt", + } + + if err := check(o, nil); err == nil { + t.Fatal() + } + }) + + t.Run("no input", func(t *testing.T) { + o := checkOptions{Syntax: "foo_test.treerack"} + + if err := check(o, nil); !errors.Is(err, errNoInput) { + t.Fatal() + } + }) + + t.Run("too many inputs", func(t *testing.T) { + o := checkOptions{ + Syntax: "foo_test.treerack", + Input: "bar_test.txt", + } + + if err := check(o, nil, "baz_test.txt"); !errors.Is(err, errMultipleInputs) { + t.Fatal() + } + }) + + t.Run("empty filename for input", func(t *testing.T) { + o := checkOptions{Syntax: "foo_test.treerack"} + if err := check(o, nil, ""); !errors.Is(err, errInvalidFilename) { + t.Fatal() + } + }) + + t.Run("input file not found", func(t *testing.T) { + o := checkOptions{Syntax: "foo_test.treerack"} + if err := check(o, nil, "baz_test.txt"); !os.IsNotExist(err) { + t.Fatal() + } + }) + + t.Run("input parse fail", func(t *testing.T) { + o := checkOptions{ + Syntax: "foo_test.treerack", + InputString: "baz", + } + + var perr *treerack.ParseError + if err := check(o, nil); !errors.As(err, &perr) { + t.Fatal() + } + }) + + t.Run("input parse success", func(t *testing.T) { + o := checkOptions{ + Syntax: "foo_test.treerack", + Input: "bar_test.txt", + } + + if err := check(o, nil); err != nil { + t.Fatal(err) + } + }) + + t.Run("input from string success", func(t *testing.T) { + o := checkOptions{ + Syntax: "foo_test.treerack", + InputString: "bar", + } + + if err := check(o, nil); err != nil { + t.Fatal(err) + } + }) + + t.Run("input from file success", func(t *testing.T) { + o := checkOptions{Syntax: "foo_test.treerack"} + if err := check(o, nil, "bar_test.txt"); err != nil { + t.Fatal(err) + } + }) + + t.Run("input from stdin success", func(t *testing.T) { + o := checkOptions{Syntax: "foo_test.treerack"} + buf := bytes.NewBufferString("bar") + if err := check(o, buf); err != nil { + t.Fatal(err) + } + }) } diff --git a/cmd/treerack/checksyntax.go b/cmd/treerack/checksyntax.go index e5faf88..aa0d728 100644 --- a/cmd/treerack/checksyntax.go +++ b/cmd/treerack/checksyntax.go @@ -1,36 +1,32 @@ package main +import ( + "code.squareroundforest.org/arpio/treerack" + "io" +) + type checkSyntaxOptions struct { - command *commandOptions - syntax *fileOptions + + // Syntax specifies the filename of the syntax definition file. + Syntax string + + // SyntaxString specifies the syntax as an inline string. + SyntaxString string } -func checkSyntax(args []string) int { - var o checkSyntaxOptions - o.command = initOptions(checkSyntaxUsage, checkSyntaxExample, positionalSyntaxUsage, args) - o.syntax = &fileOptions{typ: "syntax", flagSet: o.command.flagSet, positionalDoc: positionalSyntaxUsage} - - o.command.stringFlag(&o.syntax.inline, "syntax-string", syntaxStringUsage) - o.command.stringFlag(&o.syntax.fileName, "syntax", syntaxFileUsage) - - if o.command.help() { - return 0 +// checkSyntax validates a syntax definition. The syntax may be provided via a file path (using an option or a +// positional argument), an inline string, or piped from standard input. +func checkSyntax(o checkSyntaxOptions, stdin io.Reader, args ...string) error { + syntax, finalize, err := initInput(o.Syntax, o.SyntaxString, stdin, args) + if err != nil { + return err } - if code := o.command.parseArgs(); code != 0 { - return code + defer finalize() + s := &treerack.Syntax{} + if err := s.ReadSyntax(syntax); err != nil { + return err } - o.syntax.positional = o.command.flagSet.Args() - s, code := o.syntax.openSyntax() - if code != 0 { - return code - } - - if err := s.Init(); err != nil { - stderr(err) - return -1 - } - - return 0 + return s.Init() } diff --git a/cmd/treerack/checksyntax_test.go b/cmd/treerack/checksyntax_test.go index 9ff8402..7314fe9 100644 --- a/cmd/treerack/checksyntax_test.go +++ b/cmd/treerack/checksyntax_test.go @@ -1,166 +1,76 @@ package main -import "testing" - -var checkSyntaxFailureTests = []mainTest{ - { - title: "invalid flag", - args: []string{ - "treerack", "check-syntax", "-foo", - }, - exit: -1, - stderr: []string{ - "-syntax", - "-syntax-string", - wrapLines(positionalSyntaxUsage), - }, - }, - - { - title: "multiple inputs", - args: []string{ - "treerack", "check-syntax", "-syntax", "foo.treerack", "-syntax-string", `foo = "bar"`, - }, - exit: -1, - stderr: []string{ - "only one syntax", - "-syntax", - "-syntax-string", - wrapLines(positionalSyntaxUsage), - }, - }, - - { - title: "multiple inputs, positional", - args: []string{ - "treerack", "check-syntax", "foo.treerack", "bar.treerack", - }, - exit: -1, - stderr: []string{ - "only one syntax", - "-syntax", - "-syntax-string", - wrapLines(positionalSyntaxUsage), - }, - }, - - { - title: "multiple inputs, positional and explicit file", - args: []string{ - "treerack", "check-syntax", "-syntax", "foo.treerack", "bar.treerack", - }, - exit: -1, - stderr: []string{ - "only one syntax", - "-syntax", - "-syntax-string", - wrapLines(positionalSyntaxUsage), - }, - }, - - { - title: "no input", - args: []string{ - "treerack", "check-syntax", - }, - exit: -1, - stderr: []string{ - "missing syntax", - "-syntax", - "-syntax-string", - wrapLines(positionalSyntaxUsage), - }, - }, - - { - title: "invalid input", - args: []string{ - "treerack", "check-syntax", "-syntax-string", "foo", - }, - exit: -1, - stderr: []string{ - "parse failed", - }, - }, - - { - title: "file open fails", - args: []string{ - "treerack", "check-syntax", "-syntax", "noexist.treerack", - }, - exit: -1, - stderr: []string{ - "file", - }, - }, -} - -var checkSyntaxTests = []mainTest{ - { - title: "syntax as stdin", - args: []string{ - "treerack", "check-syntax", - }, - stdin: `foo = "bar"`, - }, - - { - title: "syntax as file", - args: []string{ - "treerack", "check-syntax", "-syntax", "foo_test.treerack", - }, - }, - - { - title: "syntax as positional", - args: []string{ - "treerack", "check-syntax", "foo_test.treerack", - }, - }, - - { - title: "syntax as string", - args: []string{ - "treerack", "check-syntax", "-syntax-string", `foo = "bar"`, - }, - }, - - { - title: "explicit over stdin", - args: []string{ - "treerack", "check-syntax", "-syntax", "foo_test.treerack", - }, - stdin: "invalid", - }, - - { - title: "invalid syntax semantics", - args: []string{ - "treerack", "check-syntax", "-syntax-string", `foo:alias = "bar"`, - }, - exit: -1, - stderr: []string{ - "root", - }, - }, -} +import ( + "bytes" + "code.squareroundforest.org/arpio/treerack" + "errors" + "os" + "testing" +) func TestCheckSyntax(t *testing.T) { - runMainTest(t, mainTest{ - title: "help", - args: []string{ - "treerack", "check-syntax", "-help", - }, - stdout: []string{ - wrapLines(checkSyntaxUsage), - "-syntax", - "-syntax-string", - wrapLines(positionalSyntaxUsage), - wrapLines(checkSyntaxExample), - wrapLines(docRef), - }, + t.Run("too many inputs", func(t *testing.T) { + o := checkSyntaxOptions{Syntax: "foo_test.treerack", SyntaxString: `foo = "42"`} + if err := checkSyntax(o, nil); !errors.Is(err, errMultipleInputs) { + t.Fatal() + } }) - runMainTest(t, checkSyntaxFailureTests...) - runMainTest(t, checkSyntaxTests...) + t.Run("empty filename", func(t *testing.T) { + var o checkSyntaxOptions + if err := checkSyntax(o, nil, ""); !errors.Is(err, errInvalidFilename) { + t.Fatal() + } + }) + + t.Run("file not found", func(t *testing.T) { + var o checkSyntaxOptions + if err := checkSyntax(o, nil, "nofile.treerack"); !os.IsNotExist(err) { + t.Fatal() + } + }) + + t.Run("invalid syntax", func(t *testing.T) { + var perr *treerack.ParseError + o := checkSyntaxOptions{SyntaxString: "foo"} + if err := checkSyntax(o, nil); !errors.As(err, &perr) { + t.Fatal() + } + }) + + t.Run("invalid syntax init", func(t *testing.T) { + o := checkSyntaxOptions{SyntaxString: `foo = "42"; foo = "84"`} + if err := checkSyntax(o, nil); err == nil { + t.Fatal() + } + }) + + t.Run("success", func(t *testing.T) { + o := checkSyntaxOptions{Syntax: "foo_test.treerack"} + if err := checkSyntax(o, nil); err != nil { + t.Fatal(err) + } + }) + + t.Run("from string success", func(t *testing.T) { + o := checkSyntaxOptions{SyntaxString: `foo = "bar"`} + if err := checkSyntax(o, nil); err != nil { + t.Fatal(err) + } + }) + + t.Run("syntax from file success", func(t *testing.T) { + var o checkSyntaxOptions + if err := checkSyntax(o, nil, "foo_test.treerack"); err != nil { + t.Fatal(err) + } + }) + + t.Run("syntax from stdin success", func(t *testing.T) { + var o checkSyntaxOptions + buf := bytes.NewBufferString(`foo = "bar"`) + if err := checkSyntax(o, buf); err != nil { + t.Fatal(err) + } + }) } diff --git a/cmd/treerack/doc.go b/cmd/treerack/doc.go deleted file mode 100644 index 8170ca6..0000000 --- a/cmd/treerack/doc.go +++ /dev/null @@ -1,106 +0,0 @@ -package main - -import ( - "strings" - "unicode/utf8" -) - -const summary = `treerack - parser generator - https://code.squareroundforest.org/arpio/treerack` - -const commandsHelp = `Available commands: -check validates an arbitrary input against a syntax definition -show parses an arbitrary input with a syntax definition and prints the abstract syntax tree -check-syntax validates a syntax definition -generate generates a parser from a syntax definition -help prints the current help - -See more details about a particular command by calling: -treerack -help` - -const docRef = `See more documentation about the definition syntax and the parser output at -https://code.squareroundforest.org/arpio/treerack.` - -const positionalSyntaxUsage = "The path to the syntax file is accepted as a positional argument." - -const positionalInputUsage = "The path to the input file is accepted as a positional argument." - -const syntaxFileUsage = "path to the syntax file in treerack format" - -const syntaxStringUsage = "inline syntax in treerack format" - -const inputFileUsage = "path to the input to be parsed" - -const inputStringUsage = "inline input string to be parsed" - -const packageNameUsage = `package name of the generated Go code` - -const exportUsage = `when the export flag is set, the generated code will have exported symbols to allow using -it as a separate package.` - -const prettyUsage = `when the pretty flag is set, the AST will be pretty printed` - -const indentUsage = `string used for indentation of the printed AST` - -const checkUsage = `'treerack check' takes a syntax description from a file or inline string, an arbitrary piece -of text from the standard input, or a file, or inline string, and parses the input text with the defined syntax. -It returns non-zero exit code and prints the problem if the provided syntax is not valid or the input cannot be -parsed with it.` - -const checkExample = `Example: -treerack check -syntax example.treerack foo.example` - -const showUsage = `'treerack show' takes a syntax description from a file or inline string, an arbitrary piece -of text from the standard input, or a file, or inline string, and parses the input text with the defined syntax. -If it was successfully parsed, it prints the resulting abstract syntax tree (AST) in JSON format.` - -const showExample = `Example: -treerack show -syntax example.treerack foo.example` - -const checkSyntaxUsage = `'treerack check-syntax' takes a syntax description from the standard input, or a file, -or inline string, and validates it to check whether it represents a valid syntax. It returns with non-zero exit -code and prints the problem if the syntax is not valid.` - -const checkSyntaxExample = `Example: -treerack check-syntax example.treerack` - -const generateUsage = `'treerack generate' takes a syntax description from the standard input, or a file, or -inline string, and generates parser code implementing the described syntax. It prints the parser code to the -standard output.` - -const generateExample = `Example: -treerack generate example.treerack > parser.go` - -const wrap = 72 - -func wrapLines(s string) string { - s = strings.Replace(s, "\n", " ", -1) - w := strings.Split(s, " ") - - var l, ll []string - for i := 0; i < len(w); i++ { - ll = append(ll, w[i]) - lineLength := utf8.RuneCount([]byte(strings.Join(ll, " "))) - if lineLength < wrap { - continue - } - - if lineLength > wrap { - ll = ll[:len(ll)-1] - i-- - } - - if len(ll) == 0 { - l = append(l, w[i]) - i++ - } else { - l = append(l, strings.Join(ll, " ")) - ll = nil - } - } - - if len(ll) > 0 { - l = append(l, strings.Join(ll, " ")) - } - - return strings.Join(l, "\n") -} diff --git a/cmd/treerack/docreflect.gen.go b/cmd/treerack/docreflect.gen.go new file mode 100644 index 0000000..287e10c --- /dev/null +++ b/cmd/treerack/docreflect.gen.go @@ -0,0 +1,47 @@ +/* +Generated with https://code.squareroundforest.org/arpio/docreflect +*/ + + +package main +import "code.squareroundforest.org/arpio/docreflect" +func init() { +docreflect.Register("main", "") +docreflect.Register("main.check", "check parses input content against the provided syntax definition and fails if the input does not match.\nSyntax can be provided via a filename option or an inline string option. Input can be provided via a filename\noption, a positional argument filename, an inline string option, or piped from standard input.\n\nfunc(o, stdin, args)") +docreflect.Register("main.checkOptions", "") +docreflect.Register("main.checkOptions.Input", "Input specifies the filename of the input content to be validated.\n") +docreflect.Register("main.checkOptions.InputString", "InputString specifies the input content as an inline string.\n") +docreflect.Register("main.checkOptions.Syntax", "Syntax specifies the filename of the syntax definition file.\n") +docreflect.Register("main.checkOptions.SyntaxString", "SyntaxString specifies the syntax as an inline string.\n") +docreflect.Register("main.checkSyntax", "checkSyntax validates a syntax definition. The syntax may be provided via a file path (using an option or a\npositional argument), an inline string, or piped from standard input.\n\nfunc(o, stdin, args)") +docreflect.Register("main.checkSyntaxOptions", "") +docreflect.Register("main.checkSyntaxOptions.Syntax", "Syntax specifies the filename of the syntax definition file.\n") +docreflect.Register("main.checkSyntaxOptions.SyntaxString", "SyntaxString specifies the syntax as an inline string.\n") +docreflect.Register("main.errInvalidFilename", "") +docreflect.Register("main.errMultipleInputs", "") +docreflect.Register("main.errNoInput", "") +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") +docreflect.Register("main.generateOptions.PackageName", "PackageName specifies the package name for the generated code. Defaults to main.\n") +docreflect.Register("main.generateOptions.Syntax", "Syntax specifies the filename of the syntax definition file.\n") +docreflect.Register("main.generateOptions.SyntaxString", "SyntaxString specifies the syntax as an inline string.\n") +docreflect.Register("main.initInput", "\nfunc(filename, stringValue, stdin, args)") +docreflect.Register("main.main", "\nfunc()") +docreflect.Register("main.mapNode", "\nfunc(n)") +docreflect.Register("main.node", "") +docreflect.Register("main.node.From", "") +docreflect.Register("main.node.Name", "") +docreflect.Register("main.node.Nodes", "") +docreflect.Register("main.node.Text", "") +docreflect.Register("main.node.To", "") +docreflect.Register("main.noop", "\nfunc()") +docreflect.Register("main.show", "show input content against a provided syntax definition and outputs the resulting AST (Abstract Syntax Tree)\nin JSON format. Syntax can be provided via a filename option or an inline string option. Input can be\nprovided via a filename option, a positional argument filename, an inline string option, or piped from\nstandard input.\n\nfunc(o, stdin, stdout, args)") +docreflect.Register("main.showOptions", "") +docreflect.Register("main.showOptions.Indent", "Indent specifies a custom indentation string for the output.\n") +docreflect.Register("main.showOptions.Input", "Input specifies the filename of the input content to be validated.\n") +docreflect.Register("main.showOptions.InputString", "InputString specifies the input content as an inline string.\n") +docreflect.Register("main.showOptions.Pretty", "Pretty enables indented, human-readable output.\n") +docreflect.Register("main.showOptions.Syntax", "Syntax specifies the filename of the syntax definition file.\n") +docreflect.Register("main.showOptions.SyntaxString", "SyntaxString specifies the syntax as an inline string.\n") +} \ No newline at end of file diff --git a/cmd/treerack/generate.go b/cmd/treerack/generate.go index fd1f334..ab09a02 100644 --- a/cmd/treerack/generate.go +++ b/cmd/treerack/generate.go @@ -1,46 +1,52 @@ package main -import "code.squareroundforest.org/arpio/treerack" +import ( + "code.squareroundforest.org/arpio/treerack" + "io" +) type generateOptions struct { - command *commandOptions - syntax *fileOptions - packageName string - export bool + + // Syntax specifies the filename of the syntax definition file. + Syntax string + + // SyntaxString specifies the syntax as an inline string. + SyntaxString string + + // PackageName specifies the package name for the generated code. Defaults to main. + PackageName string + + // Export determines whether the generated parse function is exported (visible outside its package). + Export bool } -func generate(args []string) int { - var o generateOptions - o.command = initOptions(generateUsage, generateExample, positionalSyntaxUsage, args) - o.syntax = &fileOptions{typ: "syntax", flagSet: o.command.flagSet, positionalDoc: positionalSyntaxUsage} - - o.command.boolFlag(&o.export, "export", exportUsage) - o.command.stringFlag(&o.packageName, "package-name", packageNameUsage) - o.command.stringFlag(&o.syntax.inline, "syntax-string", syntaxStringUsage) - o.command.stringFlag(&o.syntax.fileName, "syntax", syntaxFileUsage) - - if o.command.help() { - return 0 +// generate generates Go code that can parse arbitrary input with the provided syntax, and can be used embedded +// in an application. +// +// The syntax may be provided via a file path (using an option or a positional argument), an +// inline string, or piped from standard input. +func generate(o generateOptions, stdin io.Reader, stdout io.Writer, args ...string) error { + syntax, finalizeSyntax, err := initInput(o.Syntax, o.SyntaxString, stdin, args) + if err != nil { + return err } - if code := o.command.parseArgs(); code != 0 { - return code + defer finalizeSyntax() + s := &treerack.Syntax{} + if err := s.ReadSyntax(syntax); err != nil { + return err } - o.syntax.positional = o.command.flagSet.Args() - s, code := o.syntax.openSyntax() - if code != 0 { - return code + if err := s.Init(); err != nil { + return err } - var g treerack.GeneratorOptions - g.PackageName = o.packageName - g.Export = o.export - - if err := s.Generate(g, wout); err != nil { - stderr(err) - return -1 + var genOpt treerack.GeneratorOptions + genOpt.PackageName = o.PackageName + genOpt.Export = o.Export + if err := s.Generate(genOpt, stdout); err != nil { + return err } - return 0 + return nil } diff --git a/cmd/treerack/generate_test.go b/cmd/treerack/generate_test.go index c1c21eb..55f0b05 100644 --- a/cmd/treerack/generate_test.go +++ b/cmd/treerack/generate_test.go @@ -1,117 +1,155 @@ package main -import "testing" - -var generateFailureTests = convertTests("generate", checkSyntaxFailureTests) - -var generateTests = []mainTest{ - { - title: "failing output", - args: []string{ - "treerack", "generate", "-syntax-string", `foo = "bar"`, - }, - failingOutput: true, - exit: -1, - }, - - { - title: "syntax as stdin", - args: []string{ - "treerack", "generate", "-export", "-package-name", "foo", - }, - stdin: `foo = "bar"`, - stdout: []string{ - "package foo", - "func Parse", - }, - }, - - { - title: "syntax as file", - args: []string{ - "treerack", "generate", "-export", "-package-name", "foo", "-syntax", "foo_test.treerack", - }, - stdout: []string{ - "package foo", - "func Parse", - }, - }, - - { - title: "syntax as positional", - args: []string{ - "treerack", "generate", "-export", "-package-name", "foo", "foo_test.treerack", - }, - stdout: []string{ - "package foo", - "func Parse", - }, - }, - - { - title: "syntax as string", - args: []string{ - "treerack", "generate", "-export", "-package-name", "foo", "-syntax-string", `foo = "bar"`, - }, - stdout: []string{ - "package foo", - "func Parse", - }, - }, - - { - title: "default package name", - args: []string{ - "treerack", "generate", "-export", "-syntax-string", `foo = "bar"`, - }, - stdout: []string{ - "package main", - "func Parse", - }, - }, - - { - title: "no export", - args: []string{ - "treerack", "generate", "-package-name", "foo", "-syntax-string", `foo = "bar"`, - }, - stdout: []string{ - "package foo", - "func parse", - }, - }, - - { - title: "explicit over stdin", - args: []string{ - "treerack", "generate", "-export", "-package-name", "foo", "-syntax", "foo_test.treerack", - }, - stdin: "invalid", - stdout: []string{ - "package foo", - "func Parse", - }, - }, -} +import ( + "bytes" + "code.squareroundforest.org/arpio/treerack" + "errors" + "os" + "strings" + "testing" +) func TestGenerate(t *testing.T) { - runMainTest(t, mainTest{ - title: "help", - args: []string{ - "treerack", "generate", "-help", - }, - stdout: []string{ - wrapLines(generateUsage), - "-syntax", - "-syntax-string", - "-export", - "-package-name", - wrapLines(positionalSyntaxUsage), - wrapLines(generateExample), - wrapLines(docRef), - }, + t.Run("too many inputs", func(t *testing.T) { + var out bytes.Buffer + o := generateOptions{Syntax: "foo_test.treerack", SyntaxString: `foo = "42"`} + if err := generate(o, nil, &out); !errors.Is(err, errMultipleInputs) { + t.Fatal() + } }) - runMainTest(t, generateFailureTests...) - runMainTest(t, generateTests...) + t.Run("empty filename", func(t *testing.T) { + var ( + o generateOptions + out bytes.Buffer + ) + + if err := generate(o, nil, &out, ""); !errors.Is(err, errInvalidFilename) { + t.Fatal() + } + }) + + t.Run("file not found", func(t *testing.T) { + var ( + o generateOptions + out bytes.Buffer + ) + + if err := generate(o, nil, &out, "nofile.treerack"); !os.IsNotExist(err) { + t.Fatal() + } + }) + + t.Run("invalid syntax", func(t *testing.T) { + var ( + out bytes.Buffer + perr *treerack.ParseError + ) + + o := generateOptions{SyntaxString: "foo"} + if err := generate(o, nil, &out); !errors.As(err, &perr) { + t.Fatal() + } + }) + + t.Run("invalid syntax init", func(t *testing.T) { + var out bytes.Buffer + o := generateOptions{SyntaxString: `foo = "42"; foo = "84"`} + if err := generate(o, nil, &out); err == nil { + t.Fatal() + } + }) + + t.Run("success", func(t *testing.T) { + var out bytes.Buffer + o := generateOptions{Syntax: "foo_test.treerack"} + if err := generate(o, nil, &out); err != nil { + t.Fatal(err) + } + + if !strings.Contains(out.String(), "package main") || + !strings.Contains(out.String(), "func parse") { + t.Fatal(out.String()) + } + }) + + t.Run("success string", func(t *testing.T) { + var out bytes.Buffer + o := generateOptions{SyntaxString: `foo = "bar"`} + if err := generate(o, nil, &out); err != nil { + t.Fatal(err) + } + + if !strings.Contains(out.String(), "package main") || + !strings.Contains(out.String(), "func parse") { + t.Fatal(out.String()) + } + }) + + t.Run("success file", func(t *testing.T) { + var ( + out bytes.Buffer + o generateOptions + ) + + if err := generate(o, nil, &out, "foo_test.treerack"); err != nil { + t.Fatal(err) + } + + if !strings.Contains(out.String(), "package main") || + !strings.Contains(out.String(), "func parse") { + t.Fatal(out.String()) + } + }) + + t.Run("success stdin", func(t *testing.T) { + var ( + out bytes.Buffer + o generateOptions + ) + + in := bytes.NewBufferString(`foo = "bar"`) + if err := generate(o, in, &out); err != nil { + t.Fatal(err) + } + + if !strings.Contains(out.String(), "package main") || + !strings.Contains(out.String(), "func parse") { + t.Fatal(out.String()) + } + }) + + t.Run("custom package name", func(t *testing.T) { + var out bytes.Buffer + o := generateOptions{ + Syntax: "foo_test.treerack", + PackageName: "foo", + } + + if err := generate(o, nil, &out); err != nil { + t.Fatal(err) + } + + if !strings.Contains(out.String(), "package foo") || + !strings.Contains(out.String(), "func parse") { + t.Fatal(out.String()) + } + }) + + t.Run("export", func(t *testing.T) { + var out bytes.Buffer + o := generateOptions{ + Syntax: "foo_test.treerack", + Export: true, + } + + if err := generate(o, nil, &out); err != nil { + t.Fatal(err) + } + + if !strings.Contains(out.String(), "package main") || + !strings.Contains(out.String(), "func Parse") { + t.Fatal(out.String()) + } + }) } diff --git a/cmd/treerack/input.go b/cmd/treerack/input.go new file mode 100644 index 0000000..8d43efb --- /dev/null +++ b/cmd/treerack/input.go @@ -0,0 +1,78 @@ +package main + +import ( + "bytes" + "errors" + "io" + "log" + "os" +) + +var ( + errNoInput = errors.New("input undefined") + errMultipleInputs = errors.New("multiple inputs defined") + errInvalidFilename = errors.New("invalid filename") +) + +func noop() {} + +func initInput( + filename, stringValue string, stdin io.Reader, args []string, +) (input io.Reader, finalize func(), err error) { + finalize = noop + + var inputCount int + if filename != "" { + inputCount++ + } + + if stringValue != "" { + inputCount++ + } + + if len(args) > 0 { + inputCount++ + } + + if inputCount > 1 { + err = errMultipleInputs + return + } + + if len(args) > 0 && args[0] == "" { + err = errInvalidFilename + return + } + + if len(args) > 0 { + filename = args[0] + } + + switch { + case filename != "": + var f io.ReadCloser + f, err = os.Open(filename) + if err != nil { + return + } + + finalize = func() { + if err := f.Close(); err != nil { + log.Fatalln(err) + } + } + + input = f + case stringValue != "": + input = bytes.NewBufferString(stringValue) + default: + if stdin == nil { + err = errNoInput + return + } + + input = stdin + } + + return +} diff --git a/cmd/treerack/io.go b/cmd/treerack/io.go deleted file mode 100644 index bd03ea0..0000000 --- a/cmd/treerack/io.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "fmt" - "io" - "os" -) - -type exitFunc func(int) - -var ( - isTest bool - rin io.Reader = os.Stdin - - wout io.Writer = os.Stdout - werr io.Writer = os.Stderr - - exit exitFunc = func(code int) { - os.Exit(code) - } -) - -func stdout(a ...interface{}) { - fmt.Fprintln(wout, a...) -} - -func stderr(a ...interface{}) { - fmt.Fprintln(werr, a...) -} diff --git a/cmd/treerack/main.go b/cmd/treerack/main.go index e8ee62b..db6ffbd 100644 --- a/cmd/treerack/main.go +++ b/cmd/treerack/main.go @@ -1,47 +1,11 @@ package main -import "os" - -func mainHelp() { - stdout(summary) - stdout() - stdout(commandsHelp) - stdout() - stdout(docRef) -} +import . "code.squareroundforest.org/arpio/wand" func main() { - if len(os.Args) == 1 { - stderr("missing command") - stderr() - stderr(commandsHelp) - stderr() - stderr(docRef) - exit(-1) - return - } - - var cmd func([]string) int - - switch os.Args[1] { - case "check-syntax": - cmd = checkSyntax - case "generate": - cmd = generate - case "check": - cmd = check - case "show": - cmd = show - case "help", "-help": - mainHelp() - return - default: - stderr("invalid command") - stderr() - stderr(commandsHelp) - exit(-1) - return - } - - exit(cmd(os.Args[2:])) + checkSyntax := Args(Command("check-syntax", checkSyntax), 0, 1) + check := Args(Command("check", check), 0, 1) + show := Args(Command("show", show), 0, 1) + generate := Args(Command("generate", generate), 0, 1) + Exec(Group("treerack", checkSyntax, check, show, generate)) } diff --git a/cmd/treerack/main_test.go b/cmd/treerack/main_test.go deleted file mode 100644 index 34068d3..0000000 --- a/cmd/treerack/main_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "fmt" - "io" - "os" - "strings" - "testing" -) - -type mainTest struct { - title string - args []string - failingOutput bool - exit int - stdin string - stdout []string - stderr []string -} - -type failingWriter struct{} - -var errWriteFailed = errors.New("write failed") - -func (w failingWriter) Write([]byte) (int, error) { - return 0, errWriteFailed -} - -func init() { - isTest = true -} - -func convertTest(cmd string, t mainTest) mainTest { - args := make([]string, len(t.args)) - copy(args, t.args) - args[1] = cmd - t.args = args - return t -} - -func convertTests(cmd string, t []mainTest) []mainTest { - tt := make([]mainTest, len(t)) - for i := range t { - tt[i] = convertTest(cmd, t[i]) - } - - return tt -} - -func mockArgs(args ...string) (reset func()) { - original := os.Args - os.Args = args - reset = func() { - os.Args = original - } - - return -} - -func mockStdin(in string) (reset func()) { - original := rin - - if in == "" { - rin = nil - } else { - rin = bytes.NewBufferString(in) - } - - reset = func() { - rin = original - } - - return -} - -func mockOutput(w *io.Writer, failing bool) (out fmt.Stringer, reset func()) { - original := *w - reset = func() { *w = original } - - if failing { - *w = failingWriter{} - return - } - - var buf bytes.Buffer - *w = &buf - out = &buf - return -} - -func mockStdout() (out fmt.Stringer, reset func()) { - return mockOutput(&wout, false) -} - -func mockStderr() (out fmt.Stringer, reset func()) { - return mockOutput(&werr, false) -} - -func mockFailingOutput() (reset func()) { - _, reset = mockOutput(&wout, true) - return -} - -func mockExit() (code *int, reset func()) { - var exitCode int - code = &exitCode - original := exit - exit = func(c int) { exitCode = c } - reset = func() { exit = original } - return -} - -func (mt mainTest) run(t *testing.T) { - test := func(t *testing.T) { - defer mockArgs(mt.args...)() - - defer mockStdin(mt.stdin)() - - var stdout fmt.Stringer - if mt.failingOutput { - defer mockFailingOutput()() - } else { - var reset func() - stdout, reset = mockStdout() - defer reset() - } - - stderr, resetStderr := mockStderr() - defer resetStderr() - - code, resetExit := mockExit() - defer resetExit() - - main() - - if *code != mt.exit { - t.Error("invalid exit code") - } - - if stdout != nil { - var failed bool - for i := range mt.stdout { - if !strings.Contains(stdout.String(), mt.stdout[i]) { - t.Error("invalid output") - failed = true - } - } - - if failed { - t.Log(stdout.String()) - } - } - - var failed bool - for i := range mt.stderr { - if !strings.Contains(stderr.String(), mt.stderr[i]) { - t.Error("invalid error output") - failed = true - } - } - - if failed { - t.Log(stderr.String()) - } - } - - if mt.title == "" { - test(t) - } else { - t.Run(mt.title, test) - } -} - -func runMainTest(t *testing.T, mt ...mainTest) { - for i := range mt { - mt[i].run(t) - } -} - -func TestMissingCommand(t *testing.T) { - runMainTest(t, - mainTest{ - args: []string{"treerack"}, - exit: -1, - stderr: []string{ - "missing command", - commandsHelp, - docRef, - }, - }, - ) -} - -func TestInvalidCommand(t *testing.T) { - runMainTest(t, - mainTest{ - args: []string{ - "treerack", "foo", - }, - exit: -1, - stderr: []string{ - "invalid command", - commandsHelp, - }, - }, - ) -} - -func TestHelp(t *testing.T) { - runMainTest(t, - mainTest{ - title: "without dash", - args: []string{ - "treerack", "help", - }, - stdout: []string{ - summary, commandsHelp, docRef, - }, - }, - mainTest{ - title: "with dash", - args: []string{ - "treerack", "-help", - }, - stdout: []string{ - summary, commandsHelp, docRef, - }, - }, - ) -} diff --git a/cmd/treerack/open.go b/cmd/treerack/open.go deleted file mode 100644 index a5638fd..0000000 --- a/cmd/treerack/open.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "bytes" - "code.squareroundforest.org/arpio/treerack" - "flag" - "golang.org/x/crypto/ssh/terminal" - "io" - "io/ioutil" - "os" -) - -type fileOptions struct { - typ string - inline string - fileName string - positional []string - flagSet *flag.FlagSet - positionalDoc string -} - -func (o *fileOptions) multipleInputsError() { - stderr("only one", o.typ, "is allowed") - stderr() - stderr("Options:") - o.flagSet.PrintDefaults() - stderr() - stderr(wrapLines(o.positionalDoc)) -} - -func (o *fileOptions) missingInputError() { - stderr("missing", o.typ) - stderr() - stderr("Options:") - o.flagSet.PrintDefaults() - stderr() - stderr(wrapLines(o.positionalDoc)) -} - -func (o *fileOptions) getSource() (hasInput bool, fileName string, inline string, code int) { - if len(o.positional) > 1 { - o.multipleInputsError() - code = -1 - return - } - - hasPositional := len(o.positional) == 1 - hasFile := o.fileName != "" - hasInline := o.inline != "" - - var has bool - for _, h := range []bool{hasPositional, hasFile, hasInline} { - if h && has { - o.multipleInputsError() - code = -1 - return - } - - has = h - } - - switch { - case hasPositional: - fileName = o.positional[0] - return - case hasFile: - fileName = o.fileName - return - case hasInline: - inline = o.inline - return - } - - // check input last to allow explicit input in non-TTY environments: - hasInput = isTest && rin != nil || !isTest && !terminal.IsTerminal(0) - if !hasInput { - o.missingInputError() - code = -1 - return - } - - return -} - -func (o *fileOptions) open() (io.ReadCloser, int) { - hasInput, fileName, inline, code := o.getSource() - if code != 0 { - return nil, code - } - - var r io.ReadCloser - if hasInput { - r = ioutil.NopCloser(rin) - } else if fileName != "" { - f, err := os.Open(fileName) - if err != nil { - stderr(err) - return nil, -1 - } - - r = f - } else { - r = ioutil.NopCloser(bytes.NewBufferString(inline)) - } - - return r, 0 -} - -func (o *fileOptions) openSyntax() (*treerack.Syntax, int) { - input, code := o.open() - if code != 0 { - return nil, code - } - - defer input.Close() - - s := &treerack.Syntax{} - if err := s.ReadSyntax(input); err != nil { - stderr(err) - return nil, -1 - } - - return s, 0 -} diff --git a/cmd/treerack/options.go b/cmd/treerack/options.go deleted file mode 100644 index 35abd4a..0000000 --- a/cmd/treerack/options.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import "flag" - -type commandOptions struct { - usage string - example string - args []string - flagSet *flag.FlagSet - positionalDoc string -} - -func initOptions(usage, example, positionalDoc string, args []string) *commandOptions { - var o commandOptions - - o.usage = wrapLines(usage) - o.example = wrapLines(example) - o.positionalDoc = wrapLines(positionalDoc) - o.args = args - - o.flagSet = flag.NewFlagSet("", flag.ContinueOnError) - o.flagSet.Usage = func() {} - o.flagSet.SetOutput(werr) - - return &o -} - -func (o *commandOptions) boolFlag(v *bool, name, usage string) { - usage = wrapLines(usage) - o.flagSet.BoolVar(v, name, *v, usage) -} - -func (o *commandOptions) stringFlag(v *string, name, usage string) { - usage = wrapLines(usage) - o.flagSet.StringVar(v, name, *v, usage) -} - -func (o *commandOptions) flagError() { - stderr() - stderr("Options:") - o.flagSet.PrintDefaults() - stderr() - stderr(o.positionalDoc) -} - -func (o *commandOptions) parseArgs() (exit int) { - if err := o.flagSet.Parse(o.args); err != nil { - o.flagError() - exit = -1 - } - - return -} - -func (o *commandOptions) printHelp() { - stdout(o.usage) - stdout() - - stdout("Options:") - o.flagSet.SetOutput(wout) - o.flagSet.PrintDefaults() - stdout() - stdout(o.positionalDoc) - - stdout() - stdout(o.example) - stdout() - stdout(wrapLines(docRef)) -} - -func (o *commandOptions) help() bool { - if len(o.args) == 0 || o.args[0] != "-help" { - return false - } - - o.printHelp() - return true -} diff --git a/cmd/treerack/readme.md b/cmd/treerack/readme.md new file mode 100644 index 0000000..394e230 --- /dev/null +++ b/cmd/treerack/readme.md @@ -0,0 +1,134 @@ +# treerack + +## Synopsis: + +``` +treerack +``` + +## Options: + +- --help: Show help. + +## Subcommands: + +Show help for each subcommand by calling \ help or \ --help. + +### treerack check-syntax + +#### Synopsis: + +``` +treerack check-syntax [options]... [--] [args string]... +treerack check-syntax +``` + +Expecting max 1 total number of arguments. + +#### Description: + +validates a syntax definition. The syntax may be provided via a file path (using an option or a positional +argument), an inline string, or piped from standard input. + +#### Options: + +- --syntax string: specifies the filename of the syntax definition file. +- --syntax-string string: specifies the syntax as an inline string. +- --help: Show help. + +### treerack check + +#### Synopsis: + +``` +treerack check [options]... [--] [args string]... +treerack check +``` + +Expecting max 1 total number of arguments. + +#### Description: + +parses input content against the provided syntax definition and fails if the input does not match. Syntax can be +provided via a filename option or an inline string option. Input can be provided via a filename option, a +positional argument filename, an inline string option, or piped from standard input. + +#### Options: + +- --input string: specifies the filename of the input content to be validated. +- --input-string string: specifies the input content as an inline string. +- --syntax string: specifies the filename of the syntax definition file. +- --syntax-string string: specifies the syntax as an inline string. +- --help: Show help. + +### treerack show + +#### Synopsis: + +``` +treerack show [options]... [--] [args string]... +treerack show +``` + +Expecting max 1 total number of arguments. + +#### Description: + +input content against a provided syntax definition and outputs the resulting AST (Abstract Syntax Tree) in JSON +format. Syntax can be provided via a filename option or an inline string option. Input can be provided via a +filename option, a positional argument filename, an inline string option, or piped from standard input. + +#### Options: + +- --indent string: specifies a custom indentation string for the output. +- --input string: specifies the filename of the input content to be validated. +- --input-string string: specifies the input content as an inline string. +- --pretty bool: enables indented, human-readable output. +- --syntax string: specifies the filename of the syntax definition file. +- --syntax-string string: specifies the syntax as an inline string. +- --help: Show help. + +### treerack generate + +#### Synopsis: + +``` +treerack generate [options]... [--] [args string]... +treerack generate +``` + +Expecting max 1 total number of arguments. + +#### Description: + +generates Go code that can parse arbitrary input with the provided syntax, and can be used embedded in an +application. + +The syntax may be provided via a file path (using an option or a positional argument), an inline string, or +piped from standard input. + +#### Options: + +- --export bool: determines whether the generated parse function is exported (visible outside its package). +- --package-name string: specifies the package name for the generated code. Defaults to main. +- --syntax string: specifies the filename of the syntax definition file. +- --syntax-string string: specifies the syntax as an inline string. +- --help: Show help. + +## Environment variables: + +Every command line option's value can also be provided as an environment variable. Environment variable names +need to use snake casing like myapp\_foo\_bar\_baz or MYAPP\_FOO\_BAR\_BAZ, or other casing that doesn't include the +'-' dash character, and they need to be prefixed with the name of the application, as in the base name of the +command. + +When both the environment variable and the command line option is defined, the command line option overrides the +environment variable. Multiple values for the same environment variable can be defined by concatenating the +values with the ':' separator character. When overriding multiple values with command line options, all the +environment values of the same field are dropped. + +### Example environment variable: + +``` +TREERACK_SYNTAX=42 +``` diff --git a/cmd/treerack/show.go b/cmd/treerack/show.go index de4df9a..c07f9bc 100644 --- a/cmd/treerack/show.go +++ b/cmd/treerack/show.go @@ -3,25 +3,39 @@ package main import ( "code.squareroundforest.org/arpio/treerack" "encoding/json" + "io" ) type showOptions struct { - command *commandOptions - syntax *fileOptions - input *fileOptions - pretty bool - indent string + + // Syntax specifies the filename of the syntax definition file. + Syntax string + + // SyntaxString specifies the syntax as an inline string. + SyntaxString string + + // Input specifies the filename of the input content to be validated. + Input string + + // InputString specifies the input content as an inline string. + InputString string + + // Pretty enables indented, human-readable output. + Pretty bool + + // Indent specifies a custom indentation string for the output. + Indent string } type node struct { - Name string `json:"name"` - From int `json:"from"` - To int `json:"to"` - Text string `json:"text,omitempty"` - Nodes []*node `json:"nodes,omitempty"` + Name string `json:"name"` + From int `json:"from"` + To int `json:"to"` + Text string `json:"text,omitempty"` + Nodes []node `json:"nodes,omitempty"` } -func mapNode(n *treerack.Node) *node { +func mapNode(n *treerack.Node) node { var nn node nn.Name = n.Name nn.From = n.From @@ -29,76 +43,67 @@ func mapNode(n *treerack.Node) *node { if len(n.Nodes) == 0 { nn.Text = n.Text() - return &nn + return nn } for i := range n.Nodes { nn.Nodes = append(nn.Nodes, mapNode(n.Nodes[i])) } - return &nn + return nn } -func show(args []string) int { - var o showOptions - o.command = initOptions(showUsage, showExample, positionalInputUsage, args) - o.syntax = &fileOptions{typ: "syntax", flagSet: o.command.flagSet, positionalDoc: positionalInputUsage} - o.input = &fileOptions{typ: "input", flagSet: o.command.flagSet, positionalDoc: positionalInputUsage} - - o.command.stringFlag(&o.syntax.inline, "syntax-string", syntaxStringUsage) - o.command.stringFlag(&o.syntax.fileName, "syntax", syntaxFileUsage) - - o.command.stringFlag(&o.input.inline, "input-string", inputStringUsage) - o.command.stringFlag(&o.input.fileName, "input", inputFileUsage) - - o.command.boolFlag(&o.pretty, "pretty", prettyUsage) - o.command.stringFlag(&o.indent, "indent", indentUsage) - - if o.command.help() { - return 0 +// show input content against a provided syntax definition and outputs the resulting AST (Abstract Syntax Tree) +// in JSON format. Syntax can be provided via a filename option or an inline string option. Input can be +// provided via a filename option, a positional argument filename, an inline string option, or piped from +// standard input. +func show(o showOptions, stdin io.Reader, stdout io.Writer, args ...string) error { + syntax, finalizeSyntax, err := initInput(o.Syntax, o.SyntaxString, nil, nil) + if err != nil { + return err } - if code := o.command.parseArgs(); code != 0 { - return code + defer finalizeSyntax() + input, finalizeInput, err := initInput(o.Input, o.InputString, stdin, args) + if err != nil { + return err } - s, code := o.syntax.openSyntax() - if code != 0 { - return code + defer finalizeInput() + s := &treerack.Syntax{} + if err := s.ReadSyntax(syntax); err != nil { + return err } - o.input.positional = o.command.flagSet.Args() - input, code := o.input.open() - if code != 0 { - return code + if err := s.Init(); err != nil { + return err } - defer input.Close() - n, err := s.Parse(input) if err != nil { - stderr(err) - return -1 + return err } nn := mapNode(n) - - marshal := json.Marshal - if o.pretty || o.indent != "" { - if o.indent == "" { - o.indent = " " + encode := json.Marshal + if o.Pretty || o.Indent != "" { + if o.Indent == "" { + o.Indent = " " } - marshal = func(n interface{}) ([]byte, error) { - return json.MarshalIndent(n, "", o.indent) + encode = func(a any) ([]byte, error) { + return json.MarshalIndent(a, "", o.Indent) } } - b, err := marshal(nn) + b, err := encode(nn) if err != nil { - stderr(err) + return err } - stdout(string(b)) - return 0 + if _, err := stdout.Write(b); err != nil { + return err + } + + return nil } diff --git a/cmd/treerack/show_test.go b/cmd/treerack/show_test.go index bf54bdd..1fb989a 100644 --- a/cmd/treerack/show_test.go +++ b/cmd/treerack/show_test.go @@ -1,144 +1,232 @@ package main -import "testing" - -var showFailureTests = convertTests("show", checkFailureTests) - -var showTests = []mainTest{ - { - title: "syntax as file", - args: []string{ - "treerack", "show", "-syntax", "foo_test.treerack", "-input-string", "bar", - }, - stdout: []string{ - `"name":"foo"`, - }, - }, - - { - title: "syntax as string", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, "-input-string", "bar", - }, - stdout: []string{ - `"name":"foo"`, - }, - }, - - { - title: "input as stdin", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, - }, - stdin: "bar", - stdout: []string{ - `"name":"foo"`, - }, - }, - - { - title: "input as file", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, "-input", "bar_test.txt", - }, - stdout: []string{ - `"name":"foo"`, - }, - }, - - { - title: "input as positional", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, "bar_test.txt", - }, - stdout: []string{ - `"name":"foo"`, - }, - }, - - { - title: "input as string", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, "-input-string", "bar", - }, - stdout: []string{ - `"name":"foo"`, - }, - }, - - { - title: "explicit over stdin", - args: []string{ - "treerack", "show", "-syntax", "foo_test.treerack", "-input-string", "bar", - }, - stdin: "invalid", - stdout: []string{ - `"name":"foo"`, - }, - }, - - { - title: "pretty", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, "-input-string", "bar", "-pretty", - }, - stdout: []string{ - ` "name": "foo"`, - }, - }, - - { - title: "pretty and indent", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, "-input-string", "bar", "-pretty", "-indent", "xx", - }, - stdout: []string{ - `xx"name": "foo"`, - }, - }, - - { - title: "indent without pretty", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"`, "-input-string", "bar", "-pretty", "-indent", "xx", - }, - stdout: []string{ - `xx"name": "foo"`, - }, - }, - - { - title: "with child nodes", - args: []string{ - "treerack", "show", "-syntax-string", `foo = "bar"; doc = foo`, "-input-string", "bar", - }, - stdout: []string{ - `"nodes":[`, - `"text":"bar"`, - }, - }, -} +import ( + "bytes" + "code.squareroundforest.org/arpio/treerack" + "errors" + "os" + "testing" +) func TestShow(t *testing.T) { - runMainTest(t, mainTest{ - title: "help", - args: []string{ - "treerack", "show", "-help", - }, - stdout: []string{ - wrapLines(showUsage), - "-syntax", - "-syntax-string", - "-input", - "-input-string", - "-pretty", - "-indent", - wrapLines(positionalInputUsage), - wrapLines(showExample), - wrapLines(docRef), - }, + t.Run("no syntax", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{Input: "bar_test.txt"} + if err := show(o, nil, &out); !errors.Is(err, errNoInput) { + t.Fatal() + } }) - runMainTest(t, showFailureTests...) - runMainTest(t, showTests...) + t.Run("too many syntaxes", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + SyntaxString: `foo = "baz"`, + Input: "bar_test.txt", + } + + if err := show(o, nil, &out); !errors.Is(err, errMultipleInputs) { + t.Fatal() + } + }) + + t.Run("syntax file not found", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "no-file.treerack", + Input: "bar_test.txt", + } + + if err := show(o, nil, &out); !os.IsNotExist(err) { + t.Fatal() + } + }) + + t.Run("invalid syntax definition", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + SyntaxString: `foo`, + Input: "bar_test.txt", + } + + var perr *treerack.ParseError + if err := show(o, nil, &out); !errors.As(err, &perr) { + t.Fatal() + } + }) + + t.Run("invalid syntax init", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + SyntaxString: `foo = "bar"; foo = "baz"`, + Input: "bar_test.txt", + } + + if err := show(o, nil, &out); err == nil { + t.Fatal() + } + }) + + t.Run("no input", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{Syntax: "foo_test.treerack"} + + if err := show(o, nil, &out); !errors.Is(err, errNoInput) { + t.Fatal() + } + }) + + t.Run("too many inputs", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + Input: "bar_test.txt", + } + + if err := show(o, nil, &out, "baz_test.txt"); !errors.Is(err, errMultipleInputs) { + t.Fatal() + } + }) + + t.Run("empty filename for input", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{Syntax: "foo_test.treerack"} + if err := show(o, nil, &out, ""); !errors.Is(err, errInvalidFilename) { + t.Fatal() + } + }) + + t.Run("input file not found", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{Syntax: "foo_test.treerack"} + if err := show(o, nil, &out, "baz_test.txt"); !os.IsNotExist(err) { + t.Fatal() + } + }) + + t.Run("input parse fail", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + InputString: "baz", + } + + var perr *treerack.ParseError + if err := show(o, nil, &out); !errors.As(err, &perr) { + t.Fatal() + } + }) + + t.Run("show", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + Input: "bar_test.txt", + } + + if err := show(o, nil, &out); err != nil { + t.Fatal(nil) + } + + if out.String() != `{"name":"foo","from":0,"to":3,"text":"bar"}` { + t.Fatal(out.String()) + } + }) + + t.Run("show string", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + InputString: "bar", + } + + if err := show(o, nil, &out); err != nil { + t.Fatal(nil) + } + + if out.String() != `{"name":"foo","from":0,"to":3,"text":"bar"}` { + t.Fatal(out.String()) + } + }) + + t.Run("show file", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + } + + if err := show(o, nil, &out, "bar_test.txt"); err != nil { + t.Fatal(nil) + } + + if out.String() != `{"name":"foo","from":0,"to":3,"text":"bar"}` { + t.Fatal(out.String()) + } + }) + + t.Run("show stdin", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{Syntax: "foo_test.treerack"} + in := bytes.NewBufferString("bar") + if err := show(o, in, &out); err != nil { + t.Fatal(nil) + } + + if out.String() != `{"name":"foo","from":0,"to":3,"text":"bar"}` { + t.Fatal(out.String()) + } + }) + + t.Run("indent", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + Input: "bar_test.txt", + Pretty: true, + } + + if err := show(o, nil, &out); err != nil { + t.Fatal(nil) + } + + const expect = "{\n \"name\": \"foo\",\n \"from\": 0,\n \"to\": 3,\n \"text\": \"bar\"\n}" + if out.String() != expect { + t.Fatal(out.String()) + } + }) + + t.Run("custom indent", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + Input: "bar_test.txt", + Indent: "xx", + } + + if err := show(o, nil, &out); err != nil { + t.Fatal(nil) + } + + if out.String() != "{\nxx\"name\": \"foo\",\nxx\"from\": 0,\nxx\"to\": 3,\nxx\"text\": \"bar\"\n}" { + t.Fatal(out.String()) + } + }) + + t.Run("redundant custom indent", func(t *testing.T) { + var out bytes.Buffer + o := showOptions{ + Syntax: "foo_test.treerack", + Input: "bar_test.txt", + Pretty: true, + Indent: "xx", + } + + if err := show(o, nil, &out); err != nil { + t.Fatal(nil) + } + + if out.String() != "{\nxx\"name\": \"foo\",\nxx\"from\": 0,\nxx\"to\": 3,\nxx\"text\": \"bar\"\n}" { + t.Fatal(out.String()) + } + }) } diff --git a/go.mod b/go.mod index 6b216d4..cbff61f 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,20 @@ module code.squareroundforest.org/arpio/treerack -go 1.24.6 - -require golang.org/x/crypto v0.41.0 +go 1.25.3 require ( - code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 // indirect - github.com/iancoleman/strcase v0.3.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect + code.squareroundforest.org/arpio/wand v0.0.0-20260113225451-514cd3375d96 + github.com/iancoleman/strcase v0.3.0 +) + +require ( + code.squareroundforest.org/arpio/bind v0.0.0-20251125135123-0de6ad6e67f2 // indirect + code.squareroundforest.org/arpio/docreflect v0.0.0-20260113222846-40bd1879753e // indirect + code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 // indirect + code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 // indirect + code.squareroundforest.org/arpio/textedit v0.0.0-20251209222254-5a3e22b886be // indirect + code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect ) diff --git a/go.sum b/go.sum index 681cbcb..a2e2e8e 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,34 @@ -code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 h1:DKMSagVY3uyRhJ4ohiwQzNnR6CWdVKLkg97A8eQGxQU= -code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= +code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 h1:SIgLIawD6Vv7rAvUobpVshLshdwFEJ0NOUrWpheS088= +code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= +code.squareroundforest.org/arpio/bind v0.0.0-20251125135123-0de6ad6e67f2 h1:zEztr5eSD/V3lzKPcRAxNprobhHMd3w6Dw3oIbjNrrk= +code.squareroundforest.org/arpio/bind v0.0.0-20251125135123-0de6ad6e67f2/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= +code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1 h1:bJi41U5yGQykg6jVlD2AdWiznvx3Jg7ZpzEU85syOXw= +code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA= +code.squareroundforest.org/arpio/docreflect v0.0.0-20260113222846-40bd1879753e h1:Z+TXQtCxNhHUgsBSYsatNGBCRtGibRcsEbZjk1LImCQ= +code.squareroundforest.org/arpio/docreflect v0.0.0-20260113222846-40bd1879753e/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA= +code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 h1:b7voJlwe0jKH568X+O7b/JTAUrHLTSKNSSL+hhV2Q/Q= +code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9/go.mod h1:hq+2CENEd4bVSZnOdq38FUFOJJnF3OTQRv78qMGkNlE= +code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 h1:JvLVMuvF2laxXkIZbHC1/0xtKyKndAwIHbIIWkHqTzc= +code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= +code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f h1:gomu8xTD953IkL3M528qVEuZ2z93C2I6Hr4vyIwE7kI= +code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs= +code.squareroundforest.org/arpio/textedit v0.0.0-20251209222254-5a3e22b886be h1:hy7tbsf8Fzl0UzBUNXRottKtCg3GvVI7Hmaf28Qoias= +code.squareroundforest.org/arpio/textedit v0.0.0-20251209222254-5a3e22b886be/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs= +code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18 h1:2aa62CYm9ld5SNoFxWzE2wUN0xjVWQ+xieoeFantdg4= +code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18/go.mod h1:+0G3gufMAP8SCEIrDT1D/DaVOSfjS8EwPTBs5vfxqQg= +code.squareroundforest.org/arpio/wand v0.0.0-20260108202216-ba493e77d610 h1:kgDcz4+PMq5iyd3r80vcZsNfphfaRIBf9B+D7B4vYfM= +code.squareroundforest.org/arpio/wand v0.0.0-20260108202216-ba493e77d610/go.mod h1:rYqrSmdkBlKjGwEPzzWAIRQKQJCpkdzG7vDiL6Fux9Y= +code.squareroundforest.org/arpio/wand v0.0.0-20260113225451-514cd3375d96 h1:RqFGMfQznU7ivTLS8/Qj0AantFbEHSAy6U/B4xoSO88= +code.squareroundforest.org/arpio/wand v0.0.0-20260113225451-514cd3375d96/go.mod h1:fPxs3LeGPxRMWUIXgBcdszk3a8d1TRqSHSVs5VL28Rc= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= diff --git a/scripts/cmdreadme.go b/scripts/cmdreadme.go new file mode 100644 index 0000000..8dbb27c --- /dev/null +++ b/scripts/cmdreadme.go @@ -0,0 +1,14 @@ +package main + +import ( + "code.squareroundforest.org/arpio/wand/tools" + "log" + "os" +) + +func main() { + var o tools.MarkdownOptions + if err := tools.Markdown(os.Stdout, o, os.Args[1]); err != nil { + log.Fatalln(err) + } +} diff --git a/scripts/docreflect.go b/scripts/docreflect.go new file mode 100644 index 0000000..2382885 --- /dev/null +++ b/scripts/docreflect.go @@ -0,0 +1,15 @@ +package main + +import ( + "code.squareroundforest.org/arpio/wand/tools" + "log" + "os" +) + +func main() { + const pkg = "code.squareroundforest.org/arpio/treerack/cmd/treerack" + o := tools.DocreflectOptions{Main: true} + if err := tools.Docreflect(o, os.Stdout, "main", pkg); err != nil { + log.Fatalln(err) + } +} diff --git a/scripts/man.go b/scripts/man.go new file mode 100644 index 0000000..1f7b045 --- /dev/null +++ b/scripts/man.go @@ -0,0 +1,18 @@ +package main + +import ( + "code.squareroundforest.org/arpio/wand/tools" + "log" + "os" +) + +func main() { + o := tools.ManOptions{ + Version: os.Args[1], + DateString: os.Args[2], + } + + if err := tools.Man(os.Stdout, o, "./cmd/treerack"); err != nil { + log.Fatalln(err) + } +} diff --git a/syntax.go b/syntax.go index 6d032b0..7a90928 100644 --- a/syntax.go +++ b/syntax.go @@ -103,10 +103,6 @@ func isValidSymbol(n string) bool { } -// func (pe *ParseError) Verbose() string { -// return "" -// } - func intsContain(is []int, i int) bool { for _, ii := range is { if ii == i { @@ -286,6 +282,18 @@ func (s *Syntax) ReadSyntax(r io.Reader) error { } sn, err := self.Parse(r) + + var sperr *self.ParseError + if errors.As(err, &sperr) { + var perr ParseError + perr.Input = sperr.Input + perr.Offset = sperr.Offset + perr.Line = sperr.Line + perr.Column = sperr.Column + perr.Definition = sperr.Definition + return &perr + } + if err != nil { return err }