From b2e61cd0d21caa32e2c0842ad9a1ca7f87990dd5 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 14 May 2022 20:58:09 -0400 Subject: [PATCH] More fun with parser and parse tree tests --- argh/README.md | 4 + argh/argh.go | 31 ++++--- argh/parse_tree.go | 49 ++++++---- argh/parser.go | 26 +++--- argh/parser_test.go | 212 +++++++++++++++++++++++++++++++------------- 5 files changed, 220 insertions(+), 102 deletions(-) create mode 100644 argh/README.md diff --git a/argh/README.md b/argh/README.md new file mode 100644 index 0000000..2c701db --- /dev/null +++ b/argh/README.md @@ -0,0 +1,4 @@ +# argh command line parser + +> NOTE: much of this is lifted from +> https://blog.gopheracademy.com/advent-2014/parsers-lexers/ diff --git a/argh/argh.go b/argh/argh.go index 4077aa6..87a6476 100644 --- a/argh/argh.go +++ b/argh/argh.go @@ -1,35 +1,46 @@ package argh import ( + "fmt" "log" "os" + "path/filepath" + "runtime" ) -// NOTE: much of this is lifted from -// https://blog.gopheracademy.com/advent-2014/parsers-lexers/ - var ( tracingEnabled = os.Getenv("ARGH_TRACING") == "enabled" + traceLogger *log.Logger ) +func init() { + if !tracingEnabled { + return + } + + traceLogger = log.New(os.Stderr, "ARGH TRACING: ", 0) +} + type Argh struct { ParseTree *ParseTree `json:"parse_tree"` } -func (a *Argh) AST() []TypedNode { - return a.ParseTree.toAST() +func (a *Argh) TypedAST() []TypedNode { + return a.ParseTree.typedAST() } -/* -func (a *Argh) String() string { - return a.ParseTree.String() +func (a *Argh) AST() []Node { + return a.ParseTree.ast() } -*/ func tracef(format string, v ...any) { if !tracingEnabled { return } - log.Printf(format, v...) + if _, file, line, ok := runtime.Caller(2); ok { + format = fmt.Sprintf("%v:%v ", filepath.Base(file), line) + format + } + + traceLogger.Printf(format, v...) } diff --git a/argh/parse_tree.go b/argh/parse_tree.go index 7ffb182..7274cff 100644 --- a/argh/parse_tree.go +++ b/argh/parse_tree.go @@ -6,7 +6,7 @@ type ParseTree struct { Nodes []Node `json:"nodes"` } -func (pt *ParseTree) toAST() []TypedNode { +func (pt *ParseTree) typedAST() []TypedNode { ret := []TypedNode{} for _, node := range pt.Nodes { @@ -30,6 +30,32 @@ func (pt *ParseTree) toAST() []TypedNode { return ret } +func (pt *ParseTree) ast() []Node { + ret := []Node{} + + for _, node := range pt.Nodes { + if _, ok := node.(ArgDelimiter); ok { + continue + } + + if _, ok := node.(StopFlag); ok { + continue + } + + if v, ok := node.(Statement); ok { + for _, subNode := range v.Nodes { + ret = append(ret, subNode) + } + + continue + } + + ret = append(ret, node) + } + + return ret +} + type Node interface{} type TypedNode struct { @@ -38,45 +64,32 @@ type TypedNode struct { } type Args struct { - Pos int `json:"pos"` Nodes []Node `json:"nodes"` } type Statement struct { - Pos int `json:"pos"` Nodes []Node `json:"nodes"` } type Program struct { - Pos int `json:"pos"` Name string `json:"name"` } type Ident struct { - Pos int `json:"pos"` Literal string `json:"literal"` } type Command struct { - Pos int `json:"pos"` - Name string `json:"name"` - Nodes []Node `json:"nodes"` + Name string `json:"name"` } type Flag struct { - Pos int `json:"pos"` Name string `json:"name"` Value *string `json:"value,omitempty"` } -type StdinFlag struct { - Pos int `json:"pos"` -} +type StdinFlag struct{} -type StopFlag struct { - Pos int `json:"pos"` -} +type StopFlag struct{} -type ArgDelimiter struct { - Pos int `json:"pos"` -} +type ArgDelimiter struct{} diff --git a/argh/parser.go b/argh/parser.go index 211ce9c..6afda59 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -131,43 +131,47 @@ func (p *Parser) nodify() (Node, error) { switch tok { case IDENT: if len(p.nodes) == 0 { - return Program{Name: lit, Pos: pos - len(lit)}, nil + return Program{Name: lit}, nil } - return Ident{Literal: lit, Pos: pos - len(lit)}, nil + + if _, ok := p.commands[lit]; ok { + return Command{Name: lit}, nil + } + + return Ident{Literal: lit}, nil case ARG_DELIMITER: - return ArgDelimiter{Pos: pos - 1}, nil + return ArgDelimiter{}, nil case COMPOUND_SHORT_FLAG: flagNodes := []Node{} - for i, r := range lit[1:] { + for _, r := range lit[1:] { flagNodes = append( flagNodes, Flag{ - Pos: pos + i + 1, Name: string(r), }, ) } - return Statement{Pos: pos, Nodes: flagNodes}, nil + return Statement{Nodes: flagNodes}, nil case SHORT_FLAG: flagName := string(lit[1:]) if _, ok := p.valueFlags[flagName]; ok { return p.scanValueFlag(flagName, pos) } - return Flag{Name: flagName, Pos: pos - len(flagName) - 1}, nil + return Flag{Name: flagName}, nil case LONG_FLAG: flagName := string(lit[2:]) if _, ok := p.valueFlags[flagName]; ok { return p.scanValueFlag(flagName, pos) } - return Flag{Name: flagName, Pos: pos - len(flagName) - 2}, nil + return Flag{Name: flagName}, nil default: } - return Ident{Literal: lit, Pos: pos - len(lit)}, nil + return Ident{Literal: lit}, nil } func (p *Parser) scanValueFlag(flagName string, pos int) (Node, error) { @@ -178,9 +182,7 @@ func (p *Parser) scanValueFlag(flagName string, pos int) (Node, error) { return nil, err } - flagSepLen := len("--") + 1 - - return Flag{Name: flagName, Pos: pos - len(lit) - flagSepLen, Value: ptr(lit)}, nil + return Flag{Name: flagName, Value: ptr(lit)}, nil } func (p *Parser) scanIdent() (string, error) { diff --git a/argh/parser_test.go b/argh/parser_test.go index 0f95be1..c753698 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -13,39 +13,41 @@ func ptr[T any](v T) *T { func TestParser(t *testing.T) { for _, tc := range []struct { - name string - args []string - cfg *argh.ParserConfig - expected *argh.Argh - expectedErr error - skip bool + name string + args []string + cfg *argh.ParserConfig + expPT []argh.Node + expAST []argh.Node + expErr error + skip bool }{ { name: "bare", args: []string{"pizzas"}, - expected: &argh.Argh{ - ParseTree: &argh.ParseTree{ - Nodes: []argh.Node{ - argh.Program{Name: "pizzas"}, - }, - }, + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas"}, }, }, { name: "long value-less flags", args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, - expected: &argh.Argh{ - ParseTree: &argh.ParseTree{ - Nodes: []argh.Node{ - argh.Program{Name: "pizzas", Pos: 0}, - argh.ArgDelimiter{Pos: 6}, - argh.Flag{Name: "tasty", Pos: 7}, - argh.ArgDelimiter{Pos: 14}, - argh.Flag{Name: "fresh", Pos: 15}, - argh.ArgDelimiter{Pos: 22}, - argh.Flag{Name: "super-hot-right-now", Pos: 23}, - }, - }, + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "tasty"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "fresh"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "super-hot-right-now"}, + }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.Flag{Name: "tasty"}, + argh.Flag{Name: "fresh"}, + argh.Flag{Name: "super-hot-right-now"}, }, }, { @@ -55,65 +57,151 @@ func TestParser(t *testing.T) { Commands: []string{}, ValueFlags: []string{"fresh"}, }, - expected: &argh.Argh{ - ParseTree: &argh.ParseTree{ + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "tasty"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "fresh", Value: ptr("soon")}, + argh.ArgDelimiter{}, + argh.Flag{Name: "super-hot-right-now"}, + }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.Flag{Name: "tasty"}, + argh.Flag{Name: "fresh", Value: ptr("soon")}, + argh.Flag{Name: "super-hot-right-now"}, + }, + }, + { + name: "short value-less flags", + args: []string{"pizzas", "-t", "-f", "-s"}, + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "t"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "f"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "s"}, + }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.Flag{Name: "t"}, + argh.Flag{Name: "f"}, + argh.Flag{Name: "s"}, + }, + }, + { + name: "compound short flags", + args: []string{"pizzas", "-aca", "-blol"}, + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Statement{ Nodes: []argh.Node{ - argh.Program{Name: "pizzas", Pos: 0}, - argh.ArgDelimiter{Pos: 6}, - argh.Flag{Name: "tasty", Pos: 7}, - argh.ArgDelimiter{Pos: 14}, - argh.Flag{Name: "fresh", Pos: 15, Value: ptr("soon")}, - argh.ArgDelimiter{Pos: 27}, - argh.Flag{Name: "super-hot-right-now", Pos: 28}, + argh.Flag{Name: "a"}, + argh.Flag{Name: "c"}, + argh.Flag{Name: "a"}, }, }, + argh.ArgDelimiter{}, + argh.Statement{ + Nodes: []argh.Node{ + argh.Flag{Name: "b"}, + argh.Flag{Name: "l"}, + argh.Flag{Name: "o"}, + argh.Flag{Name: "l"}, + }, + }, + }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.Flag{Name: "a"}, + argh.Flag{Name: "c"}, + argh.Flag{Name: "a"}, + argh.Flag{Name: "b"}, + argh.Flag{Name: "l"}, + argh.Flag{Name: "o"}, + argh.Flag{Name: "l"}, }, }, { - skip: true, - - name: "typical", + name: "mixed long short value flags", args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, cfg: &argh.ParserConfig{ Commands: []string{}, ValueFlags: []string{"b"}, }, - expected: &argh.Argh{ - ParseTree: &argh.ParseTree{ + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "a"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "ca"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "b", Value: ptr("1312")}, + argh.ArgDelimiter{}, + argh.Statement{ Nodes: []argh.Node{ - argh.Program{Name: "pizzas", Pos: 0}, - argh.ArgDelimiter{Pos: 6}, - argh.Flag{Name: "a", Pos: 7}, - argh.ArgDelimiter{Pos: 9}, - argh.Flag{Name: "ca", Pos: 10}, - argh.ArgDelimiter{Pos: 14}, - argh.Flag{Name: "b", Pos: 15, Value: ptr("1312")}, - argh.ArgDelimiter{Pos: 22}, - argh.Statement{ - Pos: 23, - Nodes: []argh.Node{ - argh.Flag{Name: "l", Pos: 29}, - argh.Flag{Name: "o", Pos: 30}, - argh.Flag{Name: "l", Pos: 31}, - }, - }, + argh.Flag{Name: "l"}, + argh.Flag{Name: "o"}, + argh.Flag{Name: "l"}, }, }, }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.Flag{Name: "a"}, + argh.Flag{Name: "ca"}, + argh.Flag{Name: "b", Value: ptr("1312")}, + argh.Flag{Name: "l"}, + argh.Flag{Name: "o"}, + argh.Flag{Name: "l"}, + }, + }, + { + name: "commands", + args: []string{"pizzas", "fly", "fry"}, + cfg: &argh.ParserConfig{ + Commands: []string{"fly", "fry"}, + ValueFlags: []string{}, + }, + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fly"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fry"}, + }, }, } { if tc.skip { continue } - t.Run(tc.name, func(ct *testing.T) { - actual, err := argh.ParseArgs(tc.args, tc.cfg) - if err != nil { - assert.ErrorIs(ct, err, tc.expectedErr) - return - } + if tc.expPT != nil { + t.Run(tc.name+" parse tree", func(ct *testing.T) { + actual, err := argh.ParseArgs(tc.args, tc.cfg) + if err != nil { + assert.ErrorIs(ct, err, tc.expErr) + return + } - assert.Equal(ct, tc.expected, actual) - }) + assert.Equal(ct, tc.expPT, actual.ParseTree.Nodes) + }) + } + + if tc.expAST != nil { + t.Run(tc.name+" ast", func(ct *testing.T) { + actual, err := argh.ParseArgs(tc.args, tc.cfg) + if err != nil { + assert.ErrorIs(ct, err, tc.expErr) + return + } + + assert.Equal(ct, tc.expAST, actual.AST()) + }) + } } }