1
0

use wand for the command line tool

This commit is contained in:
Arpad Ryszka 2026-01-15 23:33:40 +01:00
parent b2a2974aa2
commit b4086951ee
25 changed files with 1139 additions and 1419 deletions

2
.gitignore vendored
View File

@ -3,4 +3,4 @@
.coverprofile
.coverprofile-cmd
codecov
cmd/treerack/treerack
.build

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
})
}

View File

@ -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()
}

View File

@ -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)
}
})
}

View File

@ -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 <command> -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")
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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())
}
})
}

78
cmd/treerack/input.go Normal file
View File

@ -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
}

View File

@ -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...)
}

View File

@ -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))
}

View File

@ -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,
},
},
)
}

View File

@ -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
}

View File

@ -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
}

134
cmd/treerack/readme.md Normal file
View File

@ -0,0 +1,134 @@
# treerack
## Synopsis:
```
treerack <subcommand>
```
## Options:
- --help: Show help.
## Subcommands:
Show help for each subcommand by calling \<command\> help or \<command\> --help.
### treerack check-syntax
#### Synopsis:
```
treerack check-syntax [options]... [--] [args string]...
treerack check-syntax <subcommand>
```
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 <subcommand>
```
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 <subcommand>
```
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 <subcommand>
```
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
```

View File

@ -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
}

View File

@ -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())
}
})
}

22
go.mod
View File

@ -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
)

40
go.sum
View File

@ -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=

14
scripts/cmdreadme.go Normal file
View File

@ -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)
}
}

15
scripts/docreflect.go Normal file
View File

@ -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)
}
}

18
scripts/man.go Normal file
View File

@ -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)
}
}

View File

@ -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
}