From af7d5c6e14b8f21c7bcb75bd68eb9ea8f0d3e05f Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 11 May 2022 22:11:05 -0400 Subject: [PATCH 01/23] Making a mess with command line parsing --- argh/argh.go | 5 ++ argh/ast.go | 10 +++ argh/cmd/argh/main.go | 24 ++++++ argh/go.mod | 5 ++ argh/go.sum | 2 + argh/parser.go | 82 ++++++++++++++++++ argh/scanner.go | 190 ++++++++++++++++++++++++++++++++++++++++++ argh/token.go | 22 +++++ argh/token_string.go | 36 ++++++++ 9 files changed, 376 insertions(+) create mode 100644 argh/argh.go create mode 100644 argh/ast.go create mode 100644 argh/cmd/argh/main.go create mode 100644 argh/go.mod create mode 100644 argh/go.sum create mode 100644 argh/parser.go create mode 100644 argh/scanner.go create mode 100644 argh/token.go create mode 100644 argh/token_string.go diff --git a/argh/argh.go b/argh/argh.go new file mode 100644 index 0000000..085cc76 --- /dev/null +++ b/argh/argh.go @@ -0,0 +1,5 @@ +package argh + +type Argh struct { + AST *AST +} diff --git a/argh/ast.go b/argh/ast.go new file mode 100644 index 0000000..30834f8 --- /dev/null +++ b/argh/ast.go @@ -0,0 +1,10 @@ +package argh + +type AST struct { + Nodes []*Node `json:"nodes"` +} + +type Node struct { + Token string `json:"token"` + Literal string `json:"literal"` +} diff --git a/argh/cmd/argh/main.go b/argh/cmd/argh/main.go new file mode 100644 index 0000000..2b77e52 --- /dev/null +++ b/argh/cmd/argh/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "git.meatballhat.com/x/box-o-sand/argh" +) + +func main() { + ast, err := argh.ParseArgs(os.Args) + if err != nil { + log.Fatal(err) + } + + b, err := json.MarshalIndent(ast, "", " ") + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(b)) +} diff --git a/argh/go.mod b/argh/go.mod new file mode 100644 index 0000000..f70b654 --- /dev/null +++ b/argh/go.mod @@ -0,0 +1,5 @@ +module git.meatballhat.com/x/box-o-sand/argh + +go 1.18 + +require github.com/pkg/errors v0.9.1 diff --git a/argh/go.sum b/argh/go.sum new file mode 100644 index 0000000..7c401c3 --- /dev/null +++ b/argh/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/argh/parser.go b/argh/parser.go new file mode 100644 index 0000000..cf74b87 --- /dev/null +++ b/argh/parser.go @@ -0,0 +1,82 @@ +package argh + +import ( + "io" + "strings" + + "github.com/pkg/errors" +) + +// NOTE: much of this is lifted from +// https://blog.gopheracademy.com/advent-2014/parsers-lexers/ + +var ( + errSyntax = errors.New("syntax error") +) + +func ParseArgs(args []string) (*Argh, error) { + reEncoded := strings.Join(args, string(nul)) + + return NewParser( + strings.NewReader(reEncoded), + nil, + ).Parse() +} + +type Parser struct { + s *Scanner + buf ParserBuffer +} + +type ParserBuffer struct { + tok Token + lit string + n int +} + +func NewParser(r io.Reader, cfg *ScannerConfig) *Parser { + return &Parser{s: NewScanner(r, cfg)} +} + +func (p *Parser) Parse() (*Argh, error) { + arghOut := &Argh{ + AST: &AST{ + Nodes: []*Node{}, + }, + } + + for { + tok, lit := p.scan() + if tok == ILLEGAL { + return nil, errors.Wrapf(errSyntax, "illegal value %q", lit) + } + + if tok == EOL { + break + } + + arghOut.AST.Nodes = append( + arghOut.AST.Nodes, + &Node{Token: tok.String(), Literal: lit}, + ) + } + + return arghOut, nil +} + +func (p *Parser) scan() (Token, string) { + if p.buf.n != 0 { + p.buf.n = 0 + return p.buf.tok, p.buf.lit + } + + tok, lit := p.s.Scan() + + p.buf.tok, p.buf.lit = tok, lit + + return tok, lit +} + +func (p *Parser) unscan() { + p.buf.n = 1 +} diff --git a/argh/scanner.go b/argh/scanner.go new file mode 100644 index 0000000..02e1267 --- /dev/null +++ b/argh/scanner.go @@ -0,0 +1,190 @@ +package argh + +// NOTE: much of this is lifted from +// https://blog.gopheracademy.com/advent-2014/parsers-lexers/ + +import ( + "bufio" + "bytes" + "errors" + "io" + "log" + "unicode" +) + +const ( + nul = rune(0) + eol = rune(-1) +) + +var ( + DefaultScannerConfig = &ScannerConfig{ + AssignmentOperator: '=', + FlagPrefix: '-', + MultiValueDelim: ',', + } +) + +type Scanner struct { + r *bufio.Reader + cfg *ScannerConfig +} + +type ScannerConfig struct { + AssignmentOperator rune + FlagPrefix rune + MultiValueDelim rune + + Commands []string +} + +func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { + if cfg == nil { + cfg = DefaultScannerConfig + } + + return &Scanner{ + r: bufio.NewReader(r), + cfg: cfg, + } +} + +func (s *Scanner) Scan() (Token, string) { + ch := s.read() + + if s.isBlankspace(ch) { + s.unread() + return s.scanBlankspace() + } + + if s.isAssignmentOperator(ch) { + return ASSIGN, string(ch) + } + + if s.isMultiValueDelim(ch) { + return MULTI_VALUE_DELIMITER, string(ch) + } + + if ch == eol { + return EOL, "" + } + + if ch == nul { + return ARG_DELIMITER, string(ch) + } + + if unicode.IsGraphic(ch) { + s.unread() + return s.scanArg() + } + + return ILLEGAL, string(ch) +} + +func (s *Scanner) read() rune { + ch, _, err := s.r.ReadRune() + if errors.Is(err, io.EOF) { + return eol + } else if err != nil { + log.Printf("unknown scanner error=%+v", err) + return eol + } + + return ch +} + +func (s *Scanner) unread() { + _ = s.r.UnreadRune() +} + +func (s *Scanner) isBlankspace(ch rune) bool { + return ch == ' ' || ch == '\t' || ch == '\n' +} + +func (s *Scanner) isUnderscore(ch rune) bool { + return ch == '_' +} + +func (s *Scanner) isFlagPrefix(ch rune) bool { + return ch == s.cfg.FlagPrefix +} + +func (s *Scanner) isMultiValueDelim(ch rune) bool { + return ch == s.cfg.MultiValueDelim +} + +func (s *Scanner) isAssignmentOperator(ch rune) bool { + return ch == s.cfg.AssignmentOperator +} + +func (s *Scanner) scanBlankspace() (Token, string) { + buf := &bytes.Buffer{} + buf.WriteRune(s.read()) + + for { + if ch := s.read(); ch == eol { + break + } else if !s.isBlankspace(ch) { + s.unread() + break + } else { + _, _ = buf.WriteRune(ch) + } + } + + return BS, buf.String() +} + +func (s *Scanner) scanArg() (Token, string) { + buf := &bytes.Buffer{} + buf.WriteRune(s.read()) + + for { + ch := s.read() + + if ch == eol || ch == nul || s.isAssignmentOperator(ch) || s.isMultiValueDelim(ch) { + s.unread() + break + } + + _, _ = buf.WriteRune(ch) + } + + str := buf.String() + + if len(str) == 0 { + return EMPTY, str + } + + ch0 := rune(str[0]) + + if len(str) == 1 { + if s.isFlagPrefix(ch0) { + return STDIN_FLAG, str + } + + return IDENT, str + } + + ch1 := rune(str[1]) + + if len(str) == 2 { + if str == string(s.cfg.FlagPrefix)+string(s.cfg.FlagPrefix) { + return STOP_FLAG, str + } + + if s.isFlagPrefix(ch0) { + return SHORT_FLAG, str + } + } + + if s.isFlagPrefix(ch0) { + if s.isFlagPrefix(ch1) { + return LONG_FLAG, str + } + + return COMPOUND_SHORT_FLAG, str + } + + return IDENT, str +} diff --git a/argh/token.go b/argh/token.go new file mode 100644 index 0000000..ec3f758 --- /dev/null +++ b/argh/token.go @@ -0,0 +1,22 @@ +//go:generate stringer -type Token + +package argh + +const ( + ILLEGAL Token = iota + EOL + EMPTY + BS + IDENT + ARG_DELIMITER + COMMAND + ASSIGN + MULTI_VALUE_DELIMITER + LONG_FLAG + SHORT_FLAG + COMPOUND_SHORT_FLAG + STDIN_FLAG + STOP_FLAG +) + +type Token int diff --git a/argh/token_string.go b/argh/token_string.go new file mode 100644 index 0000000..8c1b585 --- /dev/null +++ b/argh/token_string.go @@ -0,0 +1,36 @@ +// Code generated by "stringer -type Token"; DO NOT EDIT. + +package argh + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ILLEGAL-0] + _ = x[EOL-1] + _ = x[EMPTY-2] + _ = x[BS-3] + _ = x[IDENT-4] + _ = x[ARG_DELIMITER-5] + _ = x[COMMAND-6] + _ = x[ASSIGN-7] + _ = x[MULTI_VALUE_DELIMITER-8] + _ = x[LONG_FLAG-9] + _ = x[SHORT_FLAG-10] + _ = x[COMPOUND_SHORT_FLAG-11] + _ = x[STDIN_FLAG-12] + _ = x[STOP_FLAG-13] +} + +const _Token_name = "ILLEGALEOLEMPTYBSIDENTARG_DELIMITERCOMMANDASSIGNMULTI_VALUE_DELIMITERLONG_FLAGSHORT_FLAGCOMPOUND_SHORT_FLAGSTDIN_FLAGSTOP_FLAG" + +var _Token_index = [...]uint8{0, 7, 10, 15, 17, 22, 35, 42, 48, 69, 78, 88, 107, 117, 126} + +func (i Token) String() string { + if i < 0 || i >= Token(len(_Token_index)-1) { + return "Token(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Token_name[_Token_index[i]:_Token_index[i+1]] +} From c15bafe55d47f7de1274daf90935a69f069eb32b Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 13 May 2022 20:58:55 -0400 Subject: [PATCH 02/23] Yet more argh implementation fun --- .gitignore | 1 + argh/argh.go | 32 ++++++- argh/ast.go | 10 --- argh/cmd/argh/main.go | 4 +- argh/go.mod | 7 ++ argh/go.sum | 10 +++ argh/parse_tree.go | 82 ++++++++++++++++++ argh/parser.go | 196 ++++++++++++++++++++++++++++++++++++------ argh/parser_test.go | 119 +++++++++++++++++++++++++ argh/scanner.go | 76 ++++++++-------- 10 files changed, 463 insertions(+), 74 deletions(-) delete mode 100644 argh/ast.go create mode 100644 argh/parse_tree.go create mode 100644 argh/parser_test.go diff --git a/.gitignore b/.gitignore index ca46c84..954bdcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.hex *.log +*.out *env .dep **/target/ diff --git a/argh/argh.go b/argh/argh.go index 085cc76..4077aa6 100644 --- a/argh/argh.go +++ b/argh/argh.go @@ -1,5 +1,35 @@ package argh +import ( + "log" + "os" +) + +// NOTE: much of this is lifted from +// https://blog.gopheracademy.com/advent-2014/parsers-lexers/ + +var ( + tracingEnabled = os.Getenv("ARGH_TRACING") == "enabled" +) + type Argh struct { - AST *AST + ParseTree *ParseTree `json:"parse_tree"` +} + +func (a *Argh) AST() []TypedNode { + return a.ParseTree.toAST() +} + +/* +func (a *Argh) String() string { + return a.ParseTree.String() +} +*/ + +func tracef(format string, v ...any) { + if !tracingEnabled { + return + } + + log.Printf(format, v...) } diff --git a/argh/ast.go b/argh/ast.go deleted file mode 100644 index 30834f8..0000000 --- a/argh/ast.go +++ /dev/null @@ -1,10 +0,0 @@ -package argh - -type AST struct { - Nodes []*Node `json:"nodes"` -} - -type Node struct { - Token string `json:"token"` - Literal string `json:"literal"` -} diff --git a/argh/cmd/argh/main.go b/argh/cmd/argh/main.go index 2b77e52..bd9b32c 100644 --- a/argh/cmd/argh/main.go +++ b/argh/cmd/argh/main.go @@ -10,7 +10,9 @@ import ( ) func main() { - ast, err := argh.ParseArgs(os.Args) + log.SetFlags(0) + + ast, err := argh.ParseArgs(os.Args, nil) if err != nil { log.Fatal(err) } diff --git a/argh/go.mod b/argh/go.mod index f70b654..7091c9a 100644 --- a/argh/go.mod +++ b/argh/go.mod @@ -3,3 +3,10 @@ module git.meatballhat.com/x/box-o-sand/argh go 1.18 require github.com/pkg/errors v0.9.1 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.7.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/argh/go.sum b/argh/go.sum index 7c401c3..842edf5 100644 --- a/argh/go.sum +++ b/argh/go.sum @@ -1,2 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/argh/parse_tree.go b/argh/parse_tree.go new file mode 100644 index 0000000..7ffb182 --- /dev/null +++ b/argh/parse_tree.go @@ -0,0 +1,82 @@ +package argh + +import "fmt" + +type ParseTree struct { + Nodes []Node `json:"nodes"` +} + +func (pt *ParseTree) toAST() []TypedNode { + ret := []TypedNode{} + + for _, node := range pt.Nodes { + if _, ok := node.(ArgDelimiter); ok { + continue + } + + if _, ok := node.(StopFlag); ok { + continue + } + + ret = append( + ret, + TypedNode{ + Type: fmt.Sprintf("%T", node), + Node: node, + }, + ) + } + + return ret +} + +type Node interface{} + +type TypedNode struct { + Type string `json:"type"` + Node Node `json:"node"` +} + +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"` +} + +type Flag struct { + Pos int `json:"pos"` + Name string `json:"name"` + Value *string `json:"value,omitempty"` +} + +type StdinFlag struct { + Pos int `json:"pos"` +} + +type StopFlag struct { + Pos int `json:"pos"` +} + +type ArgDelimiter struct { + Pos int `json:"pos"` +} diff --git a/argh/parser.go b/argh/parser.go index cf74b87..211ce9c 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -7,76 +7,220 @@ import ( "github.com/pkg/errors" ) -// NOTE: much of this is lifted from -// https://blog.gopheracademy.com/advent-2014/parsers-lexers/ - var ( errSyntax = errors.New("syntax error") + + DefaultParserConfig = &ParserConfig{ + Commands: []string{}, + ValueFlags: []string{}, + ScannerConfig: DefaultScannerConfig, + } ) -func ParseArgs(args []string) (*Argh, error) { +func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) { reEncoded := strings.Join(args, string(nul)) return NewParser( strings.NewReader(reEncoded), - nil, + pCfg, ).Parse() } type Parser struct { s *Scanner buf ParserBuffer + + commands map[string]struct{} + valueFlags map[string]struct{} + + nodes []Node + stopSeen bool } type ParserBuffer struct { tok Token lit string + pos int n int } -func NewParser(r io.Reader, cfg *ScannerConfig) *Parser { - return &Parser{s: NewScanner(r, cfg)} +type ParserConfig struct { + Commands []string + ValueFlags []string + ScannerConfig *ScannerConfig } -func (p *Parser) Parse() (*Argh, error) { - arghOut := &Argh{ - AST: &AST{ - Nodes: []*Node{}, - }, +type parseDirective struct { + Break bool +} + +func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { + if pCfg == nil { + pCfg = DefaultParserConfig } + parser := &Parser{ + s: NewScanner(r, pCfg.ScannerConfig), + commands: map[string]struct{}{}, + valueFlags: map[string]struct{}{}, + } + + for _, command := range pCfg.Commands { + parser.commands[command] = struct{}{} + } + + for _, valueFlag := range pCfg.ValueFlags { + parser.valueFlags[valueFlag] = struct{}{} + } + + tracef("NewParser parser=%+#v", parser) + tracef("NewParser pCfg=%+#v", pCfg) + + return parser +} + +func (p *Parser) Parse() (*Argh, error) { + p.nodes = []Node{} + for { - tok, lit := p.scan() - if tok == ILLEGAL { - return nil, errors.Wrapf(errSyntax, "illegal value %q", lit) + pd, err := p.parseArg() + if err != nil { + return nil, err } - if tok == EOL { + if pd != nil && pd.Break { break } + } + + return &Argh{ParseTree: &ParseTree{Nodes: p.nodes}}, nil +} + +func (p *Parser) parseArg() (*parseDirective, error) { + tok, lit, pos := p.scan() + if tok == ILLEGAL { + return nil, errors.Wrapf(errSyntax, "illegal value %q at pos=%v", lit, pos) + } + + if tok == EOL { + return &parseDirective{Break: true}, nil + } + + p.unscan() + + node, err := p.nodify() + + tracef("parseArg node=%+#v err=%+#v", node, err) + + if err != nil { + return nil, errors.Wrapf(err, "value %q at pos=%v", lit, pos) + } + + if node != nil { + p.nodes = append(p.nodes, node) + } + + return nil, nil +} + +func (p *Parser) nodify() (Node, error) { + tok, lit, pos := p.scan() + + tracef("nodify tok=%s lit=%q pos=%v", tok, lit, pos) + + switch tok { + case IDENT: + if len(p.nodes) == 0 { + return Program{Name: lit, Pos: pos - len(lit)}, nil + } + return Ident{Literal: lit, Pos: pos - len(lit)}, nil + case ARG_DELIMITER: + return ArgDelimiter{Pos: pos - 1}, nil + case COMPOUND_SHORT_FLAG: + flagNodes := []Node{} + + for i, r := range lit[1:] { + flagNodes = append( + flagNodes, + Flag{ + Pos: pos + i + 1, + Name: string(r), + }, + ) + } + + return Statement{Pos: pos, 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 + case LONG_FLAG: + flagName := string(lit[2:]) + if _, ok := p.valueFlags[flagName]; ok { + return p.scanValueFlag(flagName, pos) + } - arghOut.AST.Nodes = append( - arghOut.AST.Nodes, - &Node{Token: tok.String(), Literal: lit}, - ) + return Flag{Name: flagName, Pos: pos - len(flagName) - 2}, nil + default: } - return arghOut, nil + return Ident{Literal: lit, Pos: pos - len(lit)}, nil } -func (p *Parser) scan() (Token, string) { +func (p *Parser) scanValueFlag(flagName string, pos int) (Node, error) { + tracef("scanValueFlag flagName=%q pos=%v", flagName, pos) + + lit, err := p.scanIdent() + if err != nil { + return nil, err + } + + flagSepLen := len("--") + 1 + + return Flag{Name: flagName, Pos: pos - len(lit) - flagSepLen, Value: ptr(lit)}, nil +} + +func (p *Parser) scanIdent() (string, error) { + tok, lit, pos := p.scan() + + nUnscan := 0 + + if tok == ASSIGN || tok == ARG_DELIMITER { + nUnscan++ + tok, lit, pos = p.scan() + } + + if tok == IDENT { + return lit, nil + } + + for i := 0; i < nUnscan; i++ { + p.unscan() + } + + return "", errors.Wrapf(errSyntax, "expected ident at pos=%v but got %s (%q)", pos, tok, lit) +} + +func (p *Parser) scan() (Token, string, int) { if p.buf.n != 0 { p.buf.n = 0 - return p.buf.tok, p.buf.lit + return p.buf.tok, p.buf.lit, p.buf.pos } - tok, lit := p.s.Scan() + tok, lit, pos := p.s.Scan() - p.buf.tok, p.buf.lit = tok, lit + p.buf.tok, p.buf.lit, p.buf.pos = tok, lit, pos - return tok, lit + return tok, lit, pos } func (p *Parser) unscan() { p.buf.n = 1 } + +func ptr[T any](v T) *T { + return &v +} diff --git a/argh/parser_test.go b/argh/parser_test.go new file mode 100644 index 0000000..0f95be1 --- /dev/null +++ b/argh/parser_test.go @@ -0,0 +1,119 @@ +package argh_test + +import ( + "testing" + + "git.meatballhat.com/x/box-o-sand/argh" + "github.com/stretchr/testify/assert" +) + +func ptr[T any](v T) *T { + return &v +} + +func TestParser(t *testing.T) { + for _, tc := range []struct { + name string + args []string + cfg *argh.ParserConfig + expected *argh.Argh + expectedErr error + skip bool + }{ + { + name: "bare", + args: []string{"pizzas"}, + expected: &argh.Argh{ + ParseTree: &argh.ParseTree{ + Nodes: []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}, + }, + }, + }, + }, + { + name: "long flags mixed", + args: []string{"pizzas", "--tasty", "--fresh", "soon", "--super-hot-right-now"}, + cfg: &argh.ParserConfig{ + Commands: []string{}, + ValueFlags: []string{"fresh"}, + }, + 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, Value: ptr("soon")}, + argh.ArgDelimiter{Pos: 27}, + argh.Flag{Name: "super-hot-right-now", Pos: 28}, + }, + }, + }, + }, + { + skip: true, + + name: "typical", + args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, + cfg: &argh.ParserConfig{ + Commands: []string{}, + ValueFlags: []string{"b"}, + }, + expected: &argh.Argh{ + ParseTree: &argh.ParseTree{ + 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}, + }, + }, + }, + }, + }, + }, + } { + 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 + } + + assert.Equal(ct, tc.expected, actual) + }) + } +} diff --git a/argh/scanner.go b/argh/scanner.go index 02e1267..cc24842 100644 --- a/argh/scanner.go +++ b/argh/scanner.go @@ -1,8 +1,5 @@ package argh -// NOTE: much of this is lifted from -// https://blog.gopheracademy.com/advent-2014/parsers-lexers/ - import ( "bufio" "bytes" @@ -27,6 +24,7 @@ var ( type Scanner struct { r *bufio.Reader + i int cfg *ScannerConfig } @@ -34,8 +32,6 @@ type ScannerConfig struct { AssignmentOperator rune FlagPrefix rune MultiValueDelim rune - - Commands []string } func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { @@ -49,52 +45,56 @@ func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { } } -func (s *Scanner) Scan() (Token, string) { - ch := s.read() +func (s *Scanner) Scan() (Token, string, int) { + ch, pos := s.read() if s.isBlankspace(ch) { - s.unread() + _ = s.unread() return s.scanBlankspace() } if s.isAssignmentOperator(ch) { - return ASSIGN, string(ch) + return ASSIGN, string(ch), pos } if s.isMultiValueDelim(ch) { - return MULTI_VALUE_DELIMITER, string(ch) + return MULTI_VALUE_DELIMITER, string(ch), pos } if ch == eol { - return EOL, "" + return EOL, "", pos } if ch == nul { - return ARG_DELIMITER, string(ch) + return ARG_DELIMITER, string(ch), pos } if unicode.IsGraphic(ch) { - s.unread() + _ = s.unread() return s.scanArg() } - return ILLEGAL, string(ch) + return ILLEGAL, string(ch), pos } -func (s *Scanner) read() rune { +func (s *Scanner) read() (rune, int) { ch, _, err := s.r.ReadRune() + s.i++ + if errors.Is(err, io.EOF) { - return eol + return eol, s.i } else if err != nil { log.Printf("unknown scanner error=%+v", err) - return eol + return eol, s.i } - return ch + return ch, s.i } -func (s *Scanner) unread() { +func (s *Scanner) unread() int { _ = s.r.UnreadRune() + s.i-- + return s.i } func (s *Scanner) isBlankspace(ch rune) bool { @@ -117,33 +117,37 @@ func (s *Scanner) isAssignmentOperator(ch rune) bool { return ch == s.cfg.AssignmentOperator } -func (s *Scanner) scanBlankspace() (Token, string) { +func (s *Scanner) scanBlankspace() (Token, string, int) { buf := &bytes.Buffer{} - buf.WriteRune(s.read()) + ch, pos := s.read() + buf.WriteRune(ch) for { - if ch := s.read(); ch == eol { + ch, pos = s.read() + + if ch == eol { break } else if !s.isBlankspace(ch) { - s.unread() + pos = s.unread() break } else { _, _ = buf.WriteRune(ch) } } - return BS, buf.String() + return BS, buf.String(), pos } -func (s *Scanner) scanArg() (Token, string) { +func (s *Scanner) scanArg() (Token, string, int) { buf := &bytes.Buffer{} - buf.WriteRune(s.read()) + ch, pos := s.read() + buf.WriteRune(ch) for { - ch := s.read() + ch, pos = s.read() if ch == eol || ch == nul || s.isAssignmentOperator(ch) || s.isMultiValueDelim(ch) { - s.unread() + pos = s.unread() break } @@ -153,38 +157,38 @@ func (s *Scanner) scanArg() (Token, string) { str := buf.String() if len(str) == 0 { - return EMPTY, str + return EMPTY, str, pos } ch0 := rune(str[0]) if len(str) == 1 { if s.isFlagPrefix(ch0) { - return STDIN_FLAG, str + return STDIN_FLAG, str, pos } - return IDENT, str + return IDENT, str, pos } ch1 := rune(str[1]) if len(str) == 2 { if str == string(s.cfg.FlagPrefix)+string(s.cfg.FlagPrefix) { - return STOP_FLAG, str + return STOP_FLAG, str, pos } if s.isFlagPrefix(ch0) { - return SHORT_FLAG, str + return SHORT_FLAG, str, pos } } if s.isFlagPrefix(ch0) { if s.isFlagPrefix(ch1) { - return LONG_FLAG, str + return LONG_FLAG, str, pos } - return COMPOUND_SHORT_FLAG, str + return COMPOUND_SHORT_FLAG, str, pos } - return IDENT, str + return IDENT, str, pos } From b2e61cd0d21caa32e2c0842ad9a1ca7f87990dd5 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 14 May 2022 20:58:09 -0400 Subject: [PATCH 03/23] 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()) + }) + } } } From 8c280c303e4cc0d61c0e2c421edd2ce6da23d532 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 15 May 2022 14:22:56 -0400 Subject: [PATCH 04/23] Handle variable count flag values --- argh/node.go | 40 +++++++++++++ argh/nvalue_string.go | 25 ++++++++ argh/parse_tree.go | 40 +------------ argh/parser.go | 131 +++++++++++++++++++++++++++--------------- argh/parser_test.go | 44 +++++++++----- 5 files changed, 182 insertions(+), 98 deletions(-) create mode 100644 argh/node.go create mode 100644 argh/nvalue_string.go diff --git a/argh/node.go b/argh/node.go new file mode 100644 index 0000000..01e3c5a --- /dev/null +++ b/argh/node.go @@ -0,0 +1,40 @@ +package argh + +type Node interface{} + +type TypedNode struct { + Type string `json:"type"` + Node Node `json:"node"` +} + +type Args struct { + Nodes []Node `json:"nodes"` +} + +type CompoundShortFlag struct { + Nodes []Node `json:"nodes"` +} + +type Program struct { + Name string `json:"name"` +} + +type Ident struct { + Literal string `json:"literal"` +} + +type Command struct { + Name string `json:"name"` + // Values []string `json:"values" +} + +type Flag struct { + Name string `json:"name"` + Values []string `json:"values"` +} + +type StdinFlag struct{} + +type StopFlag struct{} + +type ArgDelimiter struct{} diff --git a/argh/nvalue_string.go b/argh/nvalue_string.go new file mode 100644 index 0000000..f159a23 --- /dev/null +++ b/argh/nvalue_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type NValue"; DO NOT EDIT. + +package argh + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ZeroValue-0] + _ = x[OneValue-1] + _ = x[OneOrMoreValue-2] +} + +const _NValue_name = "ZeroValueOneValueOneOrMoreValue" + +var _NValue_index = [...]uint8{0, 9, 17, 31} + +func (i NValue) String() string { + if i < 0 || i >= NValue(len(_NValue_index)-1) { + return "NValue(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _NValue_name[_NValue_index[i]:_NValue_index[i+1]] +} diff --git a/argh/parse_tree.go b/argh/parse_tree.go index 7274cff..7478abe 100644 --- a/argh/parse_tree.go +++ b/argh/parse_tree.go @@ -42,7 +42,7 @@ func (pt *ParseTree) ast() []Node { continue } - if v, ok := node.(Statement); ok { + if v, ok := node.(CompoundShortFlag); ok { for _, subNode := range v.Nodes { ret = append(ret, subNode) } @@ -55,41 +55,3 @@ func (pt *ParseTree) ast() []Node { return ret } - -type Node interface{} - -type TypedNode struct { - Type string `json:"type"` - Node Node `json:"node"` -} - -type Args struct { - Nodes []Node `json:"nodes"` -} - -type Statement struct { - Nodes []Node `json:"nodes"` -} - -type Program struct { - Name string `json:"name"` -} - -type Ident struct { - Literal string `json:"literal"` -} - -type Command struct { - Name string `json:"name"` -} - -type Flag struct { - Name string `json:"name"` - Value *string `json:"value,omitempty"` -} - -type StdinFlag struct{} - -type StopFlag struct{} - -type ArgDelimiter struct{} diff --git a/argh/parser.go b/argh/parser.go index 6afda59..cc42a92 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -1,3 +1,5 @@ +//go:generate stringer -type NValue + package argh import ( @@ -7,16 +9,24 @@ import ( "github.com/pkg/errors" ) +const ( + ZeroValue NValue = iota + OneValue + OneOrMoreValue +) + var ( errSyntax = errors.New("syntax error") DefaultParserConfig = &ParserConfig{ - Commands: []string{}, - ValueFlags: []string{}, + Commands: map[string]NValue{}, + Flags: map[string]NValue{}, ScannerConfig: DefaultScannerConfig, } ) +type NValue int + func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) { reEncoded := strings.Join(args, string(nul)) @@ -27,26 +37,26 @@ func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) { } type Parser struct { - s *Scanner - buf ParserBuffer + s *Scanner - commands map[string]struct{} - valueFlags map[string]struct{} + buf []ScanEntry + + commands map[string]NValue + valueFlags map[string]NValue nodes []Node stopSeen bool } -type ParserBuffer struct { +type ScanEntry struct { tok Token lit string pos int - n int } type ParserConfig struct { - Commands []string - ValueFlags []string + Commands map[string]NValue + Flags map[string]NValue ScannerConfig *ScannerConfig } @@ -60,17 +70,10 @@ func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { } parser := &Parser{ + buf: []ScanEntry{}, s: NewScanner(r, pCfg.ScannerConfig), - commands: map[string]struct{}{}, - valueFlags: map[string]struct{}{}, - } - - for _, command := range pCfg.Commands { - parser.commands[command] = struct{}{} - } - - for _, valueFlag := range pCfg.ValueFlags { - parser.valueFlags[valueFlag] = struct{}{} + commands: pCfg.Commands, + valueFlags: pCfg.Flags, } tracef("NewParser parser=%+#v", parser) @@ -106,7 +109,7 @@ func (p *Parser) parseArg() (*parseDirective, error) { return &parseDirective{Break: true}, nil } - p.unscan() + p.unscan(tok, lit, pos) node, err := p.nodify() @@ -134,8 +137,8 @@ func (p *Parser) nodify() (Node, error) { return Program{Name: lit}, nil } - if _, ok := p.commands[lit]; ok { - return Command{Name: lit}, nil + if n, ok := p.commands[lit]; ok { + return p.scanValueCommand(lit, pos, n) } return Ident{Literal: lit}, nil @@ -153,18 +156,18 @@ func (p *Parser) nodify() (Node, error) { ) } - return Statement{Nodes: flagNodes}, nil + return CompoundShortFlag{Nodes: flagNodes}, nil case SHORT_FLAG: flagName := string(lit[1:]) - if _, ok := p.valueFlags[flagName]; ok { - return p.scanValueFlag(flagName, pos) + if n, ok := p.valueFlags[flagName]; ok { + return p.scanValueFlag(flagName, pos, n) } return Flag{Name: flagName}, nil case LONG_FLAG: flagName := string(lit[2:]) - if _, ok := p.valueFlags[flagName]; ok { - return p.scanValueFlag(flagName, pos) + if n, ok := p.valueFlags[flagName]; ok { + return p.scanValueFlag(flagName, pos, n) } return Flag{Name: flagName}, nil @@ -174,24 +177,57 @@ func (p *Parser) nodify() (Node, error) { return Ident{Literal: lit}, nil } -func (p *Parser) scanValueFlag(flagName string, pos int) (Node, error) { - tracef("scanValueFlag flagName=%q pos=%v", flagName, pos) +func (p *Parser) scanValueFlag(flagName string, pos int, n NValue) (Node, error) { + tracef("scanValueFlag flagName=%q pos=%v n=%v", flagName, pos, n) + + values, err := func() ([]string, error) { + if n == ZeroValue { + return []string{}, nil + } + + ret := []string{} + + for { + lit, err := p.scanIdent() + if err != nil { + if n == OneValue { + return nil, err + } + + if n == OneOrMoreValue { + break + } + } + + ret = append(ret, lit) + + if n == OneValue && len(ret) == 1 { + break + } + } + + return ret, nil + }() - lit, err := p.scanIdent() if err != nil { return nil, err } - return Flag{Name: flagName, Value: ptr(lit)}, nil + return Flag{Name: flagName, Values: values}, nil +} + +func (p *Parser) scanValueCommand(lit string, pos int, n NValue) (Node, error) { + return Command{Name: lit}, nil } func (p *Parser) scanIdent() (string, error) { tok, lit, pos := p.scan() - nUnscan := 0 + unscanBuf := []ScanEntry{} if tok == ASSIGN || tok == ARG_DELIMITER { - nUnscan++ + unscanBuf = append([]ScanEntry{{tok: tok, lit: lit, pos: pos}}, unscanBuf...) + tok, lit, pos = p.scan() } @@ -199,30 +235,35 @@ func (p *Parser) scanIdent() (string, error) { return lit, nil } - for i := 0; i < nUnscan; i++ { - p.unscan() + unscanBuf = append([]ScanEntry{{tok: tok, lit: lit, pos: pos}}, unscanBuf...) + + for _, entry := range unscanBuf { + p.unscan(entry.tok, entry.lit, entry.pos) } return "", errors.Wrapf(errSyntax, "expected ident at pos=%v but got %s (%q)", pos, tok, lit) } func (p *Parser) scan() (Token, string, int) { - if p.buf.n != 0 { - p.buf.n = 0 - return p.buf.tok, p.buf.lit, p.buf.pos + if len(p.buf) != 0 { + entry, buf := p.buf[len(p.buf)-1], p.buf[:len(p.buf)-1] + p.buf = buf + + tracef("scan returning buffer entry=%s %+#v", entry.tok, entry) + return entry.tok, entry.lit, entry.pos } tok, lit, pos := p.s.Scan() - p.buf.tok, p.buf.lit, p.buf.pos = tok, lit, pos + tracef("scan returning next=%s %+#v", tok, ScanEntry{tok: tok, lit: lit, pos: pos}) return tok, lit, pos } -func (p *Parser) unscan() { - p.buf.n = 1 -} +func (p *Parser) unscan(tok Token, lit string, pos int) { + entry := ScanEntry{tok: tok, lit: lit, pos: pos} + + tracef("unscan entry=%s %+#v", tok, entry) -func ptr[T any](v T) *T { - return &v + p.buf = append(p.buf, entry) } diff --git a/argh/parser_test.go b/argh/parser_test.go index c753698..68315cc 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -52,25 +52,41 @@ func TestParser(t *testing.T) { }, { name: "long flags mixed", - args: []string{"pizzas", "--tasty", "--fresh", "soon", "--super-hot-right-now"}, + args: []string{ + "pizzas", + "--tasty", + "--fresh", "soon", + "--super-hot-right-now", + "--box", "square", "shaped", "hot", + "--please", + }, cfg: &argh.ParserConfig{ - Commands: []string{}, - ValueFlags: []string{"fresh"}, + Commands: map[string]argh.NValue{}, + Flags: map[string]argh.NValue{ + "fresh": argh.OneValue, + "box": argh.OneOrMoreValue, + }, }, expPT: []argh.Node{ argh.Program{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Flag{Name: "tasty"}, argh.ArgDelimiter{}, - argh.Flag{Name: "fresh", Value: ptr("soon")}, + argh.Flag{Name: "fresh", Values: []string{"soon"}}, argh.ArgDelimiter{}, argh.Flag{Name: "super-hot-right-now"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "box", Values: []string{"square", "shaped", "hot"}}, + argh.ArgDelimiter{}, + argh.Flag{Name: "please"}, }, expAST: []argh.Node{ argh.Program{Name: "pizzas"}, argh.Flag{Name: "tasty"}, - argh.Flag{Name: "fresh", Value: ptr("soon")}, + argh.Flag{Name: "fresh", Values: []string{"soon"}}, argh.Flag{Name: "super-hot-right-now"}, + argh.Flag{Name: "box", Values: []string{"square", "shaped", "hot"}}, + argh.Flag{Name: "please"}, }, }, { @@ -98,7 +114,7 @@ func TestParser(t *testing.T) { expPT: []argh.Node{ argh.Program{Name: "pizzas"}, argh.ArgDelimiter{}, - argh.Statement{ + argh.CompoundShortFlag{ Nodes: []argh.Node{ argh.Flag{Name: "a"}, argh.Flag{Name: "c"}, @@ -106,7 +122,7 @@ func TestParser(t *testing.T) { }, }, argh.ArgDelimiter{}, - argh.Statement{ + argh.CompoundShortFlag{ Nodes: []argh.Node{ argh.Flag{Name: "b"}, argh.Flag{Name: "l"}, @@ -130,8 +146,8 @@ func TestParser(t *testing.T) { name: "mixed long short value flags", args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, cfg: &argh.ParserConfig{ - Commands: []string{}, - ValueFlags: []string{"b"}, + Commands: map[string]argh.NValue{}, + Flags: map[string]argh.NValue{"b": argh.OneValue}, }, expPT: []argh.Node{ argh.Program{Name: "pizzas"}, @@ -140,9 +156,9 @@ func TestParser(t *testing.T) { argh.ArgDelimiter{}, argh.Flag{Name: "ca"}, argh.ArgDelimiter{}, - argh.Flag{Name: "b", Value: ptr("1312")}, + argh.Flag{Name: "b", Values: []string{"1312"}}, argh.ArgDelimiter{}, - argh.Statement{ + argh.CompoundShortFlag{ Nodes: []argh.Node{ argh.Flag{Name: "l"}, argh.Flag{Name: "o"}, @@ -154,7 +170,7 @@ func TestParser(t *testing.T) { argh.Program{Name: "pizzas"}, argh.Flag{Name: "a"}, argh.Flag{Name: "ca"}, - argh.Flag{Name: "b", Value: ptr("1312")}, + argh.Flag{Name: "b", Values: []string{"1312"}}, argh.Flag{Name: "l"}, argh.Flag{Name: "o"}, argh.Flag{Name: "l"}, @@ -164,8 +180,8 @@ func TestParser(t *testing.T) { name: "commands", args: []string{"pizzas", "fly", "fry"}, cfg: &argh.ParserConfig{ - Commands: []string{"fly", "fry"}, - ValueFlags: []string{}, + Commands: map[string]argh.NValue{"fly": argh.ZeroValue, "fry": argh.ZeroValue}, + Flags: map[string]argh.NValue{}, }, expPT: []argh.Node{ argh.Program{Name: "pizzas"}, From 9989801e620643bdbd70edaff5c80b8913856e9e Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 15 May 2022 20:55:54 -0400 Subject: [PATCH 05/23] Ensure program and commands can also receive positional arg values --- argh/node.go | 7 ++-- argh/parser.go | 94 ++++++++++++++++++++++++++++++--------------- argh/parser_test.go | 62 ++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 34 deletions(-) diff --git a/argh/node.go b/argh/node.go index 01e3c5a..f51f48c 100644 --- a/argh/node.go +++ b/argh/node.go @@ -16,7 +16,8 @@ type CompoundShortFlag struct { } type Program struct { - Name string `json:"name"` + Name string `json:"name"` + Values []string `json:"values"` } type Ident struct { @@ -24,8 +25,8 @@ type Ident struct { } type Command struct { - Name string `json:"name"` - // Values []string `json:"values" + Name string `json:"name"` + Values []string `json:"values"` } type Flag struct { diff --git a/argh/parser.go b/argh/parser.go index cc42a92..87707b1 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -41,8 +41,7 @@ type Parser struct { buf []ScanEntry - commands map[string]NValue - valueFlags map[string]NValue + cfg *ParserConfig nodes []Node stopSeen bool @@ -55,25 +54,21 @@ type ScanEntry struct { } type ParserConfig struct { + ProgValues NValue Commands map[string]NValue Flags map[string]NValue ScannerConfig *ScannerConfig } -type parseDirective struct { - Break bool -} - func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { if pCfg == nil { pCfg = DefaultParserConfig } parser := &Parser{ - buf: []ScanEntry{}, - s: NewScanner(r, pCfg.ScannerConfig), - commands: pCfg.Commands, - valueFlags: pCfg.Flags, + buf: []ScanEntry{}, + s: NewScanner(r, pCfg.ScannerConfig), + cfg: pCfg, } tracef("NewParser parser=%+#v", parser) @@ -86,12 +81,12 @@ func (p *Parser) Parse() (*Argh, error) { p.nodes = []Node{} for { - pd, err := p.parseArg() + br, err := p.parseArg() if err != nil { return nil, err } - if pd != nil && pd.Break { + if br { break } } @@ -99,14 +94,14 @@ func (p *Parser) Parse() (*Argh, error) { return &Argh{ParseTree: &ParseTree{Nodes: p.nodes}}, nil } -func (p *Parser) parseArg() (*parseDirective, error) { +func (p *Parser) parseArg() (bool, error) { tok, lit, pos := p.scan() if tok == ILLEGAL { - return nil, errors.Wrapf(errSyntax, "illegal value %q at pos=%v", lit, pos) + return false, errors.Wrapf(errSyntax, "illegal value %q at pos=%v", lit, pos) } if tok == EOL { - return &parseDirective{Break: true}, nil + return true, nil } p.unscan(tok, lit, pos) @@ -116,14 +111,14 @@ func (p *Parser) parseArg() (*parseDirective, error) { tracef("parseArg node=%+#v err=%+#v", node, err) if err != nil { - return nil, errors.Wrapf(err, "value %q at pos=%v", lit, pos) + return false, errors.Wrapf(err, "value %q at pos=%v", lit, pos) } if node != nil { p.nodes = append(p.nodes, node) } - return nil, nil + return false, nil } func (p *Parser) nodify() (Node, error) { @@ -134,11 +129,21 @@ func (p *Parser) nodify() (Node, error) { switch tok { case IDENT: if len(p.nodes) == 0 { - return Program{Name: lit}, nil + values, err := p.scanValues(lit, pos, p.cfg.ProgValues) + if err != nil { + return nil, err + } + + return Program{Name: lit, Values: values}, nil } - if n, ok := p.commands[lit]; ok { - return p.scanValueCommand(lit, pos, n) + if n, ok := p.cfg.Commands[lit]; ok { + values, err := p.scanValues(lit, pos, n) + if err != nil { + return nil, err + } + + return Command{Name: lit, Values: values}, nil } return Ident{Literal: lit}, nil @@ -147,7 +152,24 @@ func (p *Parser) nodify() (Node, error) { case COMPOUND_SHORT_FLAG: flagNodes := []Node{} - for _, r := range lit[1:] { + withoutFlagPrefix := lit[1:] + + for i, r := range withoutFlagPrefix { + if i == len(withoutFlagPrefix)-1 { + flagName := string(r) + + if n, ok := p.cfg.Flags[flagName]; ok { + values, err := p.scanValues(flagName, pos, n) + if err != nil { + return nil, err + } + + flagNodes = append(flagNodes, Flag{Name: flagName, Values: values}) + + continue + } + } + flagNodes = append( flagNodes, Flag{ @@ -159,15 +181,25 @@ func (p *Parser) nodify() (Node, error) { return CompoundShortFlag{Nodes: flagNodes}, nil case SHORT_FLAG: flagName := string(lit[1:]) - if n, ok := p.valueFlags[flagName]; ok { - return p.scanValueFlag(flagName, pos, n) + if n, ok := p.cfg.Flags[flagName]; ok { + values, err := p.scanValues(flagName, pos, n) + if err != nil { + return nil, err + } + + return Flag{Name: flagName, Values: values}, nil } return Flag{Name: flagName}, nil case LONG_FLAG: flagName := string(lit[2:]) - if n, ok := p.valueFlags[flagName]; ok { - return p.scanValueFlag(flagName, pos, n) + if n, ok := p.cfg.Flags[flagName]; ok { + values, err := p.scanValues(flagName, pos, n) + if err != nil { + return nil, err + } + + return Flag{Name: flagName, Values: values}, nil } return Flag{Name: flagName}, nil @@ -177,8 +209,8 @@ func (p *Parser) nodify() (Node, error) { return Ident{Literal: lit}, nil } -func (p *Parser) scanValueFlag(flagName string, pos int, n NValue) (Node, error) { - tracef("scanValueFlag flagName=%q pos=%v n=%v", flagName, pos, n) +func (p *Parser) scanValues(lit string, pos int, n NValue) ([]string, error) { + tracef("scanValues lit=%q pos=%v n=%v", lit, pos, n) values, err := func() ([]string, error) { if n == ZeroValue { @@ -213,11 +245,11 @@ func (p *Parser) scanValueFlag(flagName string, pos int, n NValue) (Node, error) return nil, err } - return Flag{Name: flagName, Values: values}, nil -} + if len(values) == 0 { + return nil, nil + } -func (p *Parser) scanValueCommand(lit string, pos int, n NValue) (Node, error) { - return Command{Name: lit}, nil + return values, nil } func (p *Parser) scanIdent() (string, error) { diff --git a/argh/parser_test.go b/argh/parser_test.go index 68315cc..6432105 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -31,6 +31,32 @@ func TestParser(t *testing.T) { argh.Program{Name: "pizzas"}, }, }, + { + name: "one positional arg", + args: []string{"pizzas", "excel"}, + cfg: &argh.ParserConfig{ + ProgValues: argh.OneValue, + }, + expPT: []argh.Node{ + argh.Program{Name: "pizzas", Values: []string{"excel"}}, + }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas", Values: []string{"excel"}}, + }, + }, + { + name: "many positional args", + args: []string{"pizzas", "excel", "wildly", "when", "feral"}, + cfg: &argh.ParserConfig{ + ProgValues: argh.OneOrMoreValue, + }, + expPT: []argh.Node{ + argh.Program{Name: "pizzas", Values: []string{"excel", "wildly", "when", "feral"}}, + }, + expAST: []argh.Node{ + argh.Program{Name: "pizzas", Values: []string{"excel", "wildly", "when", "feral"}}, + }, + }, { name: "long value-less flags", args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, @@ -191,6 +217,42 @@ func TestParser(t *testing.T) { argh.Command{Name: "fry"}, }, }, + { + name: "total weirdo", + args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, + cfg: &argh.ParserConfig{ + Commands: map[string]argh.NValue{"goose": argh.OneValue}, + Flags: map[string]argh.NValue{ + "w": argh.ZeroValue, + "A": argh.ZeroValue, + "T": argh.OneValue, + "hecking": argh.ZeroValue, + "FIERCENESS": argh.OneValue, + }, + ScannerConfig: &argh.ScannerConfig{ + AssignmentOperator: '@', + FlagPrefix: '^', + MultiValueDelim: ',', + }, + }, + expPT: []argh.Node{ + argh.Program{Name: "PIZZAs"}, + argh.ArgDelimiter{}, + argh.CompoundShortFlag{ + Nodes: []argh.Node{ + argh.Flag{Name: "w"}, + argh.Flag{Name: "A"}, + argh.Flag{Name: "T", Values: []string{"golf"}}, + }, + }, + argh.ArgDelimiter{}, + argh.Flag{Name: "hecKing"}, + argh.ArgDelimiter{}, + argh.Command{Name: "goose", Values: []string{"bonk"}}, + argh.ArgDelimiter{}, + argh.Flag{Name: "FIERCENESS", Values: []string{"-2"}}, + }, + }, } { if tc.skip { continue From de6e907c60150dbae9abe43637f67e3db905fb98 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 16 May 2022 08:24:24 -0400 Subject: [PATCH 06/23] Handle bare assignments as syntax error + NValue rework --- argh/nvalue_string.go | 11 ++++++----- argh/parser.go | 46 +++++++++++++++++++++++++++++-------------- argh/parser_test.go | 27 ++++++++++++++++--------- argh/scanner.go | 4 ++++ 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/argh/nvalue_string.go b/argh/nvalue_string.go index f159a23..d9d1f06 100644 --- a/argh/nvalue_string.go +++ b/argh/nvalue_string.go @@ -8,18 +8,19 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} + _ = x[OneOrMoreValue - -2] + _ = x[ZeroOrMoreValue - -1] _ = x[ZeroValue-0] - _ = x[OneValue-1] - _ = x[OneOrMoreValue-2] } -const _NValue_name = "ZeroValueOneValueOneOrMoreValue" +const _NValue_name = "OneOrMoreValueZeroOrMoreValueZeroValue" -var _NValue_index = [...]uint8{0, 9, 17, 31} +var _NValue_index = [...]uint8{0, 14, 29, 38} func (i NValue) String() string { + i -= -2 if i < 0 || i >= NValue(len(_NValue_index)-1) { - return "NValue(" + strconv.FormatInt(int64(i), 10) + ")" + return "NValue(" + strconv.FormatInt(int64(i+-2), 10) + ")" } return _NValue_name[_NValue_index[i]:_NValue_index[i+1]] } diff --git a/argh/parser.go b/argh/parser.go index 87707b1..47c6fca 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -10,13 +10,13 @@ import ( ) const ( - ZeroValue NValue = iota - OneValue - OneOrMoreValue + OneOrMoreValue NValue = -2 + ZeroOrMoreValue NValue = -1 + ZeroValue NValue = 0 ) var ( - errSyntax = errors.New("syntax error") + ErrSyntax = errors.New("syntax error") DefaultParserConfig = &ParserConfig{ Commands: map[string]NValue{}, @@ -54,9 +54,13 @@ type ScanEntry struct { } type ParserConfig struct { - ProgValues NValue - Commands map[string]NValue - Flags map[string]NValue + ProgValues NValue + Commands map[string]NValue + Flags map[string]NValue + + OnUnknownFlag func(string) error + OnUnknownCommand func(string) error + ScannerConfig *ScannerConfig } @@ -97,7 +101,7 @@ func (p *Parser) Parse() (*Argh, error) { func (p *Parser) parseArg() (bool, error) { tok, lit, pos := p.scan() if tok == ILLEGAL { - return false, errors.Wrapf(errSyntax, "illegal value %q at pos=%v", lit, pos) + return false, errors.Wrapf(ErrSyntax, "illegal value %q at pos=%v", lit, pos) } if tok == EOL { @@ -127,6 +131,10 @@ func (p *Parser) nodify() (Node, error) { tracef("nodify tok=%s lit=%q pos=%v", tok, lit, pos) switch tok { + case ARG_DELIMITER: + return ArgDelimiter{}, nil + case ASSIGN: + return nil, errors.Wrapf(ErrSyntax, "bare assignment operator at pos=%v", pos) case IDENT: if len(p.nodes) == 0 { values, err := p.scanValues(lit, pos, p.cfg.ProgValues) @@ -147,8 +155,6 @@ func (p *Parser) nodify() (Node, error) { } return Ident{Literal: lit}, nil - case ARG_DELIMITER: - return ArgDelimiter{}, nil case COMPOUND_SHORT_FLAG: flagNodes := []Node{} @@ -222,7 +228,7 @@ func (p *Parser) scanValues(lit string, pos int, n NValue) ([]string, error) { for { lit, err := p.scanIdent() if err != nil { - if n == OneValue { + if n == NValue(1) { return nil, err } @@ -233,7 +239,7 @@ func (p *Parser) scanValues(lit string, pos int, n NValue) ([]string, error) { ret = append(ret, lit) - if n == OneValue && len(ret) == 1 { + if n == NValue(1) && len(ret) == 1 { break } } @@ -255,10 +261,16 @@ func (p *Parser) scanValues(lit string, pos int, n NValue) ([]string, error) { func (p *Parser) scanIdent() (string, error) { tok, lit, pos := p.scan() + tracef("scanIdent scanned tok=%s lit=%q pos=%v", tok, lit, pos) + unscanBuf := []ScanEntry{} if tok == ASSIGN || tok == ARG_DELIMITER { - unscanBuf = append([]ScanEntry{{tok: tok, lit: lit, pos: pos}}, unscanBuf...) + entry := ScanEntry{tok: tok, lit: lit, pos: pos} + + tracef("scanIdent tok=%s; scanning next and pushing to unscan buffer entry=%+#v", tok, entry) + + unscanBuf = append([]ScanEntry{entry}, unscanBuf...) tok, lit, pos = p.scan() } @@ -267,13 +279,17 @@ func (p *Parser) scanIdent() (string, error) { return lit, nil } - unscanBuf = append([]ScanEntry{{tok: tok, lit: lit, pos: pos}}, unscanBuf...) + entry := ScanEntry{tok: tok, lit: lit, pos: pos} + + tracef("scanIdent tok=%s; unscanning entry=%+#v", tok, entry) + + unscanBuf = append([]ScanEntry{entry}, unscanBuf...) for _, entry := range unscanBuf { p.unscan(entry.tok, entry.lit, entry.pos) } - return "", errors.Wrapf(errSyntax, "expected ident at pos=%v but got %s (%q)", pos, tok, lit) + return "", errors.Wrapf(ErrSyntax, "expected ident at pos=%v but got %s (%q)", pos, tok, lit) } func (p *Parser) scan() (Token, string, int) { diff --git a/argh/parser_test.go b/argh/parser_test.go index 6432105..db9bc03 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -35,7 +35,7 @@ func TestParser(t *testing.T) { name: "one positional arg", args: []string{"pizzas", "excel"}, cfg: &argh.ParserConfig{ - ProgValues: argh.OneValue, + ProgValues: 1, }, expPT: []argh.Node{ argh.Program{Name: "pizzas", Values: []string{"excel"}}, @@ -89,7 +89,7 @@ func TestParser(t *testing.T) { cfg: &argh.ParserConfig{ Commands: map[string]argh.NValue{}, Flags: map[string]argh.NValue{ - "fresh": argh.OneValue, + "fresh": 1, "box": argh.OneOrMoreValue, }, }, @@ -173,7 +173,7 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, cfg: &argh.ParserConfig{ Commands: map[string]argh.NValue{}, - Flags: map[string]argh.NValue{"b": argh.OneValue}, + Flags: map[string]argh.NValue{"b": 1}, }, expPT: []argh.Node{ argh.Program{Name: "pizzas"}, @@ -221,13 +221,13 @@ func TestParser(t *testing.T) { name: "total weirdo", args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.NValue{"goose": argh.OneValue}, + Commands: map[string]argh.NValue{"goose": 1}, Flags: map[string]argh.NValue{ - "w": argh.ZeroValue, - "A": argh.ZeroValue, - "T": argh.OneValue, - "hecking": argh.ZeroValue, - "FIERCENESS": argh.OneValue, + "w": 0, + "A": 0, + "T": 1, + "hecking": 0, + "FIERCENESS": 1, }, ScannerConfig: &argh.ScannerConfig{ AssignmentOperator: '@', @@ -253,6 +253,15 @@ func TestParser(t *testing.T) { argh.Flag{Name: "FIERCENESS", Values: []string{"-2"}}, }, }, + { + name: "invalid bare assignment", + args: []string{"pizzas", "=", "--wat"}, + expErr: argh.ErrSyntax, + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + }, + }, + {}, } { if tc.skip { continue diff --git a/argh/scanner.go b/argh/scanner.go index cc24842..27f01d0 100644 --- a/argh/scanner.go +++ b/argh/scanner.go @@ -167,6 +167,10 @@ func (s *Scanner) scanArg() (Token, string, int) { return STDIN_FLAG, str, pos } + if s.isAssignmentOperator(ch0) { + return ASSIGN, str, pos + } + return IDENT, str, pos } From 10807379310d2bc7fddda866015b01b572e98a67 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 18 May 2022 20:15:31 -0400 Subject: [PATCH 07/23] Work on separate querier + cleanups --- argh/argh.go | 12 -- argh/node.go | 14 +- argh/parser.go | 226 +++++++++++++++++------------ argh/parser_test.go | 121 +++++++++------ argh/{parse_tree.go => querier.go} | 31 +++- argh/querier_test.go | 46 ++++++ 6 files changed, 296 insertions(+), 154 deletions(-) rename argh/{parse_tree.go => querier.go} (52%) create mode 100644 argh/querier_test.go diff --git a/argh/argh.go b/argh/argh.go index 87a6476..f1cc2f9 100644 --- a/argh/argh.go +++ b/argh/argh.go @@ -21,18 +21,6 @@ func init() { traceLogger = log.New(os.Stderr, "ARGH TRACING: ", 0) } -type Argh struct { - ParseTree *ParseTree `json:"parse_tree"` -} - -func (a *Argh) TypedAST() []TypedNode { - return a.ParseTree.typedAST() -} - -func (a *Argh) AST() []Node { - return a.ParseTree.ast() -} - func tracef(format string, v ...any) { if !tracingEnabled { return diff --git a/argh/node.go b/argh/node.go index f51f48c..270be73 100644 --- a/argh/node.go +++ b/argh/node.go @@ -16,8 +16,9 @@ type CompoundShortFlag struct { } type Program struct { - Name string `json:"name"` - Values []string `json:"values"` + Name string `json:"name"` + Values map[string]string `json:"values"` + Nodes []Node `json:"nodes"` } type Ident struct { @@ -25,13 +26,14 @@ type Ident struct { } type Command struct { - Name string `json:"name"` - Values []string `json:"values"` + Name string `json:"name"` + Values map[string]string `json:"values"` + Nodes []Node `json:"nodes"` } type Flag struct { - Name string `json:"name"` - Values []string `json:"values"` + Name string `json:"name"` + Values map[string]string `json:"values"` } type StdinFlag struct{} diff --git a/argh/parser.go b/argh/parser.go index 47c6fca..dc7409f 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -3,6 +3,7 @@ package argh import ( + "fmt" "io" "strings" @@ -19,15 +20,15 @@ var ( ErrSyntax = errors.New("syntax error") DefaultParserConfig = &ParserConfig{ - Commands: map[string]NValue{}, - Flags: map[string]NValue{}, + Commands: map[string]CommandConfig{}, + Flags: map[string]FlagConfig{}, ScannerConfig: DefaultScannerConfig, } ) type NValue int -func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) { +func ParseArgs(args []string, pCfg *ParserConfig) (*ParseTree, error) { reEncoded := strings.Join(args, string(nul)) return NewParser( @@ -39,38 +40,50 @@ func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) { type Parser struct { s *Scanner - buf []ScanEntry + buf []scanEntry cfg *ParserConfig - nodes []Node - stopSeen bool + nodes []Node + node Node } -type ScanEntry struct { +type ParseTree struct { + Nodes []Node `json:"nodes"` +} + +type scanEntry struct { tok Token lit string pos int } type ParserConfig struct { - ProgValues NValue - Commands map[string]NValue - Flags map[string]NValue - - OnUnknownFlag func(string) error - OnUnknownCommand func(string) error + Prog CommandConfig + Commands map[string]CommandConfig + Flags map[string]FlagConfig ScannerConfig *ScannerConfig } +type CommandConfig struct { + NValue NValue + ValueNames []string + Flags map[string]FlagConfig +} + +type FlagConfig struct { + NValue NValue + ValueNames []string +} + func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { if pCfg == nil { pCfg = DefaultParserConfig } parser := &Parser{ - buf: []ScanEntry{}, + buf: []scanEntry{}, s: NewScanner(r, pCfg.ScannerConfig), cfg: pCfg, } @@ -81,7 +94,7 @@ func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { return parser } -func (p *Parser) Parse() (*Argh, error) { +func (p *Parser) Parse() (*ParseTree, error) { p.nodes = []Node{} for { @@ -95,7 +108,7 @@ func (p *Parser) Parse() (*Argh, error) { } } - return &Argh{ParseTree: &ParseTree{Nodes: p.nodes}}, nil + return &ParseTree{Nodes: p.nodes}, nil } func (p *Parser) parseArg() (bool, error) { @@ -110,7 +123,7 @@ func (p *Parser) parseArg() (bool, error) { p.unscan(tok, lit, pos) - node, err := p.nodify() + node, err := p.scanNode() tracef("parseArg node=%+#v err=%+#v", node, err) @@ -125,10 +138,10 @@ func (p *Parser) parseArg() (bool, error) { return false, nil } -func (p *Parser) nodify() (Node, error) { +func (p *Parser) scanNode() (Node, error) { tok, lit, pos := p.scan() - tracef("nodify tok=%s lit=%q pos=%v", tok, lit, pos) + tracef("scanNode tok=%s lit=%q pos=%v", tok, lit, pos) switch tok { case ARG_DELIMITER: @@ -136,94 +149,120 @@ func (p *Parser) nodify() (Node, error) { case ASSIGN: return nil, errors.Wrapf(ErrSyntax, "bare assignment operator at pos=%v", pos) case IDENT: - if len(p.nodes) == 0 { - values, err := p.scanValues(lit, pos, p.cfg.ProgValues) - if err != nil { - return nil, err - } + p.unscan(tok, lit, pos) + return p.scanCommandOrIdent() + case COMPOUND_SHORT_FLAG: + p.unscan(tok, lit, pos) + return p.scanCompoundShortFlag() + case SHORT_FLAG, LONG_FLAG: + p.unscan(tok, lit, pos) + return p.scanFlag() + default: + } - return Program{Name: lit, Values: values}, nil - } + return Ident{Literal: lit}, nil +} - if n, ok := p.cfg.Commands[lit]; ok { - values, err := p.scanValues(lit, pos, n) - if err != nil { - return nil, err - } +func (p *Parser) scanCommandOrIdent() (Node, error) { + tok, lit, pos := p.scan() - return Command{Name: lit, Values: values}, nil + if len(p.nodes) == 0 { + p.unscan(tok, lit, pos) + values, err := p.scanValues(p.cfg.Prog.NValue, p.cfg.Prog.ValueNames) + if err != nil { + return nil, err } - return Ident{Literal: lit}, nil - case COMPOUND_SHORT_FLAG: - flagNodes := []Node{} + return Program{Name: lit, Values: values}, nil + } - withoutFlagPrefix := lit[1:] + if cfg, ok := p.cfg.Commands[lit]; ok { + p.unscan(tok, lit, pos) + values, err := p.scanValues(cfg.NValue, cfg.ValueNames) + if err != nil { + return nil, err + } - for i, r := range withoutFlagPrefix { - if i == len(withoutFlagPrefix)-1 { - flagName := string(r) + return Command{Name: lit, Values: values}, nil + } - if n, ok := p.cfg.Flags[flagName]; ok { - values, err := p.scanValues(flagName, pos, n) - if err != nil { - return nil, err - } + return Ident{Literal: lit}, nil +} - flagNodes = append(flagNodes, Flag{Name: flagName, Values: values}) +func (p *Parser) scanFlag() (Node, error) { + tok, lit, pos := p.scan() - continue - } - } + flagName := string(lit[1:]) + if tok == LONG_FLAG { + flagName = string(lit[2:]) + } - flagNodes = append( - flagNodes, - Flag{ - Name: string(r), - }, - ) + if cfg, ok := p.cfg.Flags[flagName]; ok { + p.unscan(tok, flagName, pos) + + values, err := p.scanValues(cfg.NValue, cfg.ValueNames) + if err != nil { + return nil, err } - return CompoundShortFlag{Nodes: flagNodes}, nil - case SHORT_FLAG: - flagName := string(lit[1:]) - if n, ok := p.cfg.Flags[flagName]; ok { - values, err := p.scanValues(flagName, pos, n) - if err != nil { - return nil, err - } + return Flag{Name: flagName, Values: values}, nil + } - return Flag{Name: flagName, Values: values}, nil - } + return Flag{Name: flagName}, nil +} - return Flag{Name: flagName}, nil - case LONG_FLAG: - flagName := string(lit[2:]) - if n, ok := p.cfg.Flags[flagName]; ok { - values, err := p.scanValues(flagName, pos, n) - if err != nil { - return nil, err - } +func (p *Parser) scanCompoundShortFlag() (Node, error) { + tok, lit, pos := p.scan() + + flagNodes := []Node{} + + withoutFlagPrefix := lit[1:] - return Flag{Name: flagName, Values: values}, nil + for i, r := range withoutFlagPrefix { + if i == len(withoutFlagPrefix)-1 { + flagName := string(r) + + if cfg, ok := p.cfg.Flags[flagName]; ok { + p.unscan(tok, flagName, pos) + + values, err := p.scanValues(cfg.NValue, cfg.ValueNames) + if err != nil { + return nil, err + } + + flagNodes = append(flagNodes, Flag{Name: flagName, Values: values}) + + continue + } } - return Flag{Name: flagName}, nil - default: + flagNodes = append( + flagNodes, + Flag{ + Name: string(r), + }, + ) } - return Ident{Literal: lit}, nil + return CompoundShortFlag{Nodes: flagNodes}, nil +} + +func (p *Parser) scanValuesAndFlags() (map[string]string, []Node, error) { + return nil, nil, nil } -func (p *Parser) scanValues(lit string, pos int, n NValue) ([]string, error) { - tracef("scanValues lit=%q pos=%v n=%v", lit, pos, n) +func (p *Parser) scanValues(n NValue, valueNames []string) (map[string]string, error) { + _, lit, pos := p.scan() + + tracef("scanValues lit=%q pos=%v n=%v valueNames=%+v", lit, pos, n, valueNames) - values, err := func() ([]string, error) { + values, err := func() (map[string]string, error) { if n == ZeroValue { - return []string{}, nil + return map[string]string{}, nil } - ret := []string{} + ret := map[string]string{} + i := 0 for { lit, err := p.scanIdent() @@ -237,11 +276,20 @@ func (p *Parser) scanValues(lit string, pos int, n NValue) ([]string, error) { } } - ret = append(ret, lit) + name := fmt.Sprintf("%d", i) + if len(valueNames)-1 >= i { + name = valueNames[i] + } else if len(valueNames) > 0 && strings.HasSuffix(valueNames[len(valueNames)-1], "+") { + name = strings.TrimSuffix(valueNames[len(valueNames)-1], "+") + } + + ret[name] = lit if n == NValue(1) && len(ret) == 1 { break } + + i++ } return ret, nil @@ -263,14 +311,14 @@ func (p *Parser) scanIdent() (string, error) { tracef("scanIdent scanned tok=%s lit=%q pos=%v", tok, lit, pos) - unscanBuf := []ScanEntry{} + unscanBuf := []scanEntry{} if tok == ASSIGN || tok == ARG_DELIMITER { - entry := ScanEntry{tok: tok, lit: lit, pos: pos} + entry := scanEntry{tok: tok, lit: lit, pos: pos} tracef("scanIdent tok=%s; scanning next and pushing to unscan buffer entry=%+#v", tok, entry) - unscanBuf = append([]ScanEntry{entry}, unscanBuf...) + unscanBuf = append([]scanEntry{entry}, unscanBuf...) tok, lit, pos = p.scan() } @@ -279,11 +327,11 @@ func (p *Parser) scanIdent() (string, error) { return lit, nil } - entry := ScanEntry{tok: tok, lit: lit, pos: pos} + entry := scanEntry{tok: tok, lit: lit, pos: pos} tracef("scanIdent tok=%s; unscanning entry=%+#v", tok, entry) - unscanBuf = append([]ScanEntry{entry}, unscanBuf...) + unscanBuf = append([]scanEntry{entry}, unscanBuf...) for _, entry := range unscanBuf { p.unscan(entry.tok, entry.lit, entry.pos) @@ -303,13 +351,13 @@ func (p *Parser) scan() (Token, string, int) { tok, lit, pos := p.s.Scan() - tracef("scan returning next=%s %+#v", tok, ScanEntry{tok: tok, lit: lit, pos: pos}) + tracef("scan returning next=%s %+#v", tok, scanEntry{tok: tok, lit: lit, pos: pos}) return tok, lit, pos } func (p *Parser) unscan(tok Token, lit string, pos int) { - entry := ScanEntry{tok: tok, lit: lit, pos: pos} + entry := scanEntry{tok: tok, lit: lit, pos: pos} tracef("unscan entry=%s %+#v", tok, entry) diff --git a/argh/parser_test.go b/argh/parser_test.go index db9bc03..743de16 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -7,10 +7,6 @@ import ( "github.com/stretchr/testify/assert" ) -func ptr[T any](v T) *T { - return &v -} - func TestParser(t *testing.T) { for _, tc := range []struct { name string @@ -19,7 +15,6 @@ func TestParser(t *testing.T) { expPT []argh.Node expAST []argh.Node expErr error - skip bool }{ { name: "bare", @@ -35,26 +30,26 @@ func TestParser(t *testing.T) { name: "one positional arg", args: []string{"pizzas", "excel"}, cfg: &argh.ParserConfig{ - ProgValues: 1, + Prog: argh.CommandConfig{NValue: 1}, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas", Values: []string{"excel"}}, + argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel"}}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas", Values: []string{"excel"}}, + argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel"}}, }, }, { name: "many positional args", args: []string{"pizzas", "excel", "wildly", "when", "feral"}, cfg: &argh.ParserConfig{ - ProgValues: argh.OneOrMoreValue, + Prog: argh.CommandConfig{NValue: argh.OneOrMoreValue}, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas", Values: []string{"excel", "wildly", "when", "feral"}}, + argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas", Values: []string{"excel", "wildly", "when", "feral"}}, + argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, }, }, { @@ -87,10 +82,10 @@ func TestParser(t *testing.T) { "--please", }, cfg: &argh.ParserConfig{ - Commands: map[string]argh.NValue{}, - Flags: map[string]argh.NValue{ - "fresh": 1, - "box": argh.OneOrMoreValue, + Commands: map[string]argh.CommandConfig{}, + Flags: map[string]argh.FlagConfig{ + "fresh": argh.FlagConfig{NValue: 1}, + "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, }, }, expPT: []argh.Node{ @@ -98,20 +93,20 @@ func TestParser(t *testing.T) { argh.ArgDelimiter{}, argh.Flag{Name: "tasty"}, argh.ArgDelimiter{}, - argh.Flag{Name: "fresh", Values: []string{"soon"}}, + argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, argh.ArgDelimiter{}, argh.Flag{Name: "super-hot-right-now"}, argh.ArgDelimiter{}, - argh.Flag{Name: "box", Values: []string{"square", "shaped", "hot"}}, + argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, argh.ArgDelimiter{}, argh.Flag{Name: "please"}, }, expAST: []argh.Node{ argh.Program{Name: "pizzas"}, argh.Flag{Name: "tasty"}, - argh.Flag{Name: "fresh", Values: []string{"soon"}}, + argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, argh.Flag{Name: "super-hot-right-now"}, - argh.Flag{Name: "box", Values: []string{"square", "shaped", "hot"}}, + argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, argh.Flag{Name: "please"}, }, }, @@ -172,8 +167,10 @@ func TestParser(t *testing.T) { name: "mixed long short value flags", args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.NValue{}, - Flags: map[string]argh.NValue{"b": 1}, + Commands: map[string]argh.CommandConfig{}, + Flags: map[string]argh.FlagConfig{ + "b": argh.FlagConfig{NValue: 1}, + }, }, expPT: []argh.Node{ argh.Program{Name: "pizzas"}, @@ -182,7 +179,7 @@ func TestParser(t *testing.T) { argh.ArgDelimiter{}, argh.Flag{Name: "ca"}, argh.ArgDelimiter{}, - argh.Flag{Name: "b", Values: []string{"1312"}}, + argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, argh.ArgDelimiter{}, argh.CompoundShortFlag{ Nodes: []argh.Node{ @@ -196,7 +193,7 @@ func TestParser(t *testing.T) { argh.Program{Name: "pizzas"}, argh.Flag{Name: "a"}, argh.Flag{Name: "ca"}, - argh.Flag{Name: "b", Values: []string{"1312"}}, + argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, argh.Flag{Name: "l"}, argh.Flag{Name: "o"}, argh.Flag{Name: "l"}, @@ -206,28 +203,74 @@ func TestParser(t *testing.T) { name: "commands", args: []string{"pizzas", "fly", "fry"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.NValue{"fly": argh.ZeroValue, "fry": argh.ZeroValue}, - Flags: map[string]argh.NValue{}, + Commands: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{}, + "fry": argh.CommandConfig{}, + }, + Flags: map[string]argh.FlagConfig{}, + }, + expPT: []argh.Node{ + argh.Program{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fly"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fry"}, + }, + }, + { + name: "command specific flags", + args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt"}, + cfg: &argh.ParserConfig{ + Commands: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "freely": {}, + }, + }, + "fry": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "deeply": {}, + "w": {}, + "A": {}, + "t": {}, + }, + }, + }, + Flags: map[string]argh.FlagConfig{}, }, expPT: []argh.Node{ argh.Program{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Command{Name: "fly"}, argh.ArgDelimiter{}, + argh.Flag{Name: "freely"}, + argh.ArgDelimiter{}, argh.Command{Name: "fry"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "deeply"}, + argh.ArgDelimiter{}, + argh.CompoundShortFlag{ + Nodes: []argh.Node{ + argh.Flag{Name: "w"}, + argh.Flag{Name: "A"}, + argh.Flag{Name: "t"}, + }, + }, }, }, { name: "total weirdo", args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.NValue{"goose": 1}, - Flags: map[string]argh.NValue{ - "w": 0, - "A": 0, - "T": 1, - "hecking": 0, - "FIERCENESS": 1, + Commands: map[string]argh.CommandConfig{ + "goose": argh.CommandConfig{NValue: 1}, + }, + Flags: map[string]argh.FlagConfig{ + "w": argh.FlagConfig{}, + "A": argh.FlagConfig{}, + "T": argh.FlagConfig{NValue: 1}, + "hecking": argh.FlagConfig{}, + "FIERCENESS": argh.FlagConfig{NValue: 1}, }, ScannerConfig: &argh.ScannerConfig{ AssignmentOperator: '@', @@ -242,15 +285,15 @@ func TestParser(t *testing.T) { Nodes: []argh.Node{ argh.Flag{Name: "w"}, argh.Flag{Name: "A"}, - argh.Flag{Name: "T", Values: []string{"golf"}}, + argh.Flag{Name: "T", Values: map[string]string{"0": "golf"}}, }, }, argh.ArgDelimiter{}, argh.Flag{Name: "hecKing"}, argh.ArgDelimiter{}, - argh.Command{Name: "goose", Values: []string{"bonk"}}, + argh.Command{Name: "goose", Values: map[string]string{"0": "bonk"}}, argh.ArgDelimiter{}, - argh.Flag{Name: "FIERCENESS", Values: []string{"-2"}}, + argh.Flag{Name: "FIERCENESS", Values: map[string]string{"0": "-2"}}, }, }, { @@ -263,10 +306,6 @@ func TestParser(t *testing.T) { }, {}, } { - if tc.skip { - continue - } - if tc.expPT != nil { t.Run(tc.name+" parse tree", func(ct *testing.T) { actual, err := argh.ParseArgs(tc.args, tc.cfg) @@ -275,7 +314,7 @@ func TestParser(t *testing.T) { return } - assert.Equal(ct, tc.expPT, actual.ParseTree.Nodes) + assert.Equal(ct, tc.expPT, actual.Nodes) }) } @@ -287,7 +326,7 @@ func TestParser(t *testing.T) { return } - assert.Equal(ct, tc.expAST, actual.AST()) + assert.Equal(ct, tc.expAST, argh.NewQuerier(actual).AST()) }) } } diff --git a/argh/parse_tree.go b/argh/querier.go similarity index 52% rename from argh/parse_tree.go rename to argh/querier.go index 7478abe..27e2b9c 100644 --- a/argh/parse_tree.go +++ b/argh/querier.go @@ -2,14 +2,33 @@ package argh import "fmt" -type ParseTree struct { - Nodes []Node `json:"nodes"` +type Querier interface { + Program() (Program, bool) + TypedAST() []TypedNode + AST() []Node } -func (pt *ParseTree) typedAST() []TypedNode { +func NewQuerier(pt *ParseTree) Querier { + return &defaultQuerier{pt: pt} +} + +type defaultQuerier struct { + pt *ParseTree +} + +func (dq *defaultQuerier) Program() (Program, bool) { + if len(dq.pt.Nodes) == 0 { + return Program{}, false + } + + v, ok := dq.pt.Nodes[0].(Program) + return v, ok +} + +func (dq *defaultQuerier) TypedAST() []TypedNode { ret := []TypedNode{} - for _, node := range pt.Nodes { + for _, node := range dq.pt.Nodes { if _, ok := node.(ArgDelimiter); ok { continue } @@ -30,10 +49,10 @@ func (pt *ParseTree) typedAST() []TypedNode { return ret } -func (pt *ParseTree) ast() []Node { +func (dq *defaultQuerier) AST() []Node { ret := []Node{} - for _, node := range pt.Nodes { + for _, node := range dq.pt.Nodes { if _, ok := node.(ArgDelimiter); ok { continue } diff --git a/argh/querier_test.go b/argh/querier_test.go new file mode 100644 index 0000000..3ec6793 --- /dev/null +++ b/argh/querier_test.go @@ -0,0 +1,46 @@ +package argh_test + +import ( + "testing" + + "git.meatballhat.com/x/box-o-sand/argh" + "github.com/stretchr/testify/require" +) + +func TestQuerier_Program(t *testing.T) { + for _, tc := range []struct { + name string + args []string + cfg *argh.ParserConfig + exp argh.Program + expOK bool + }{ + { + name: "typical", + args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, + exp: argh.Program{Name: "pizzas"}, + expOK: true, + }, + { + name: "minimal", + args: []string{"pizzas"}, + exp: argh.Program{Name: "pizzas"}, + expOK: true, + }, + { + name: "invalid", + args: []string{}, + exp: argh.Program{}, + expOK: false, + }, + } { + t.Run(tc.name, func(ct *testing.T) { + pt, err := argh.ParseArgs(tc.args, tc.cfg) + require.Nil(ct, err) + + prog, ok := argh.NewQuerier(pt).Program() + require.Equal(ct, tc.exp, prog) + require.Equal(ct, tc.expOK, ok) + }) + } +} From 58842504c450365975727a69185e02dfdd3759c2 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 18 May 2022 22:19:53 -0400 Subject: [PATCH 08/23] Minor bits while giving up (for now?) on command context --- argh/node.go | 4 ---- argh/parser_test.go | 5 +++++ argh/querier_test.go | 6 ++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/argh/node.go b/argh/node.go index 270be73..e35c1cc 100644 --- a/argh/node.go +++ b/argh/node.go @@ -7,10 +7,6 @@ type TypedNode struct { Node Node `json:"node"` } -type Args struct { - Nodes []Node `json:"nodes"` -} - type CompoundShortFlag struct { Nodes []Node `json:"nodes"` } diff --git a/argh/parser_test.go b/argh/parser_test.go index 743de16..9cb5bb5 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -15,6 +15,7 @@ func TestParser(t *testing.T) { expPT []argh.Node expAST []argh.Node expErr error + skip bool }{ { name: "bare", @@ -306,6 +307,10 @@ func TestParser(t *testing.T) { }, {}, } { + if tc.skip { + continue + } + if tc.expPT != nil { t.Run(tc.name+" parse tree", func(ct *testing.T) { actual, err := argh.ParseArgs(tc.args, tc.cfg) diff --git a/argh/querier_test.go b/argh/querier_test.go index 3ec6793..d37c7e1 100644 --- a/argh/querier_test.go +++ b/argh/querier_test.go @@ -33,6 +33,12 @@ func TestQuerier_Program(t *testing.T) { exp: argh.Program{}, expOK: false, }, + { + name: "invalid flag only", + args: []string{"--oh-no"}, + exp: argh.Program{}, + expOK: false, + }, } { t.Run(tc.name, func(ct *testing.T) { pt, err := argh.ParseArgs(tc.args, tc.cfg) From f2e0de1b667d7582ae0c95d2fdac4a7f9e963e54 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 22 May 2022 08:47:45 -0400 Subject: [PATCH 09/23] Making a mess with a parser that works more like go/parser --- argh/node.go | 10 +++ argh/parser.go | 43 ++-------- argh/parser2.go | 188 ++++++++++++++++++++++++++++++++++++++++++ argh/parser2_test.go | 36 ++++++++ argh/parser_config.go | 37 +++++++++ argh/scanner.go | 88 ++++++++++++++++++-- argh/token.go | 52 +++++++++--- argh/token_string.go | 19 ++--- 8 files changed, 404 insertions(+), 69 deletions(-) create mode 100644 argh/parser2.go create mode 100644 argh/parser2_test.go create mode 100644 argh/parser_config.go diff --git a/argh/node.go b/argh/node.go index e35c1cc..f5bddf0 100644 --- a/argh/node.go +++ b/argh/node.go @@ -7,6 +7,10 @@ type TypedNode struct { Node Node `json:"node"` } +type PassthroughArgs struct { + Nodes []Node `json:"nodes"` +} + type CompoundShortFlag struct { Nodes []Node `json:"nodes"` } @@ -21,6 +25,12 @@ type Ident struct { Literal string `json:"literal"` } +type BadArg struct { + Literal string + From Pos + To Pos +} + type Command struct { Name string `json:"name"` Values map[string]string `json:"values"` diff --git a/argh/parser.go b/argh/parser.go index dc7409f..713356a 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -10,24 +10,10 @@ import ( "github.com/pkg/errors" ) -const ( - OneOrMoreValue NValue = -2 - ZeroOrMoreValue NValue = -1 - ZeroValue NValue = 0 -) - var ( ErrSyntax = errors.New("syntax error") - - DefaultParserConfig = &ParserConfig{ - Commands: map[string]CommandConfig{}, - Flags: map[string]FlagConfig{}, - ScannerConfig: DefaultScannerConfig, - } ) -type NValue int - func ParseArgs(args []string, pCfg *ParserConfig) (*ParseTree, error) { reEncoded := strings.Join(args, string(nul)) @@ -55,26 +41,7 @@ type ParseTree struct { type scanEntry struct { tok Token lit string - pos int -} - -type ParserConfig struct { - Prog CommandConfig - Commands map[string]CommandConfig - Flags map[string]FlagConfig - - ScannerConfig *ScannerConfig -} - -type CommandConfig struct { - NValue NValue - ValueNames []string - Flags map[string]FlagConfig -} - -type FlagConfig struct { - NValue NValue - ValueNames []string + pos Pos } func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { @@ -314,7 +281,7 @@ func (p *Parser) scanIdent() (string, error) { unscanBuf := []scanEntry{} if tok == ASSIGN || tok == ARG_DELIMITER { - entry := scanEntry{tok: tok, lit: lit, pos: pos} + entry := scanEntry{tok: tok, lit: lit, pos: Pos(pos)} tracef("scanIdent tok=%s; scanning next and pushing to unscan buffer entry=%+#v", tok, entry) @@ -327,7 +294,7 @@ func (p *Parser) scanIdent() (string, error) { return lit, nil } - entry := scanEntry{tok: tok, lit: lit, pos: pos} + entry := scanEntry{tok: tok, lit: lit, pos: Pos(pos)} tracef("scanIdent tok=%s; unscanning entry=%+#v", tok, entry) @@ -340,7 +307,7 @@ func (p *Parser) scanIdent() (string, error) { return "", errors.Wrapf(ErrSyntax, "expected ident at pos=%v but got %s (%q)", pos, tok, lit) } -func (p *Parser) scan() (Token, string, int) { +func (p *Parser) scan() (Token, string, Pos) { if len(p.buf) != 0 { entry, buf := p.buf[len(p.buf)-1], p.buf[:len(p.buf)-1] p.buf = buf @@ -356,7 +323,7 @@ func (p *Parser) scan() (Token, string, int) { return tok, lit, pos } -func (p *Parser) unscan(tok Token, lit string, pos int) { +func (p *Parser) unscan(tok Token, lit string, pos Pos) { entry := scanEntry{tok: tok, lit: lit, pos: pos} tracef("unscan entry=%s %+#v", tok, entry) diff --git a/argh/parser2.go b/argh/parser2.go new file mode 100644 index 0000000..f428c5d --- /dev/null +++ b/argh/parser2.go @@ -0,0 +1,188 @@ +package argh + +import ( + "fmt" + "io" + "strings" +) + +type parser2 struct { + s *Scanner + + commands map[string]struct{} + + errors ScannerErrorList + + tok Token + lit string + pos Pos +} + +func ParseArgs2(args, commands []string) (*ParseTree, error) { + parser := &parser2{} + parser.init( + strings.NewReader(strings.Join(args, string(nul))), + commands, + ) + + tracef("ParseArgs2 parser=%+#v", parser) + + return parser.parseArgs() +} + +func (p *parser2) init(r io.Reader, commands []string) { + p.errors = ScannerErrorList{} + commandMap := map[string]struct{}{} + + for _, c := range commands { + commandMap[c] = struct{}{} + } + + p.s = NewScanner(r, nil) + p.commands = commandMap + + p.next() +} + +func (p *parser2) parseArgs() (*ParseTree, error) { + if p.errors.Len() != 0 { + tracef("parseArgs bailing due to initial error") + return nil, p.errors.Err() + } + + prog := &Program{ + Name: p.lit, + Values: map[string]string{}, + Nodes: []Node{}, + } + p.next() + + for p.tok != EOL && p.tok != STOP_FLAG { + prog.Nodes = append(prog.Nodes, p.parseArg()) + } + + return &ParseTree{ + Nodes: []Node{ + prog, p.parsePassthrough(), + }, + }, nil +} + +func (p *parser2) next() { + tracef("parser2.next() <- %v %q %v", p.tok, p.lit, p.pos) + defer func() { + tracef("parser2.next() -> %v %q %v", p.tok, p.lit, p.pos) + }() + + p.tok, p.lit, p.pos = p.s.Scan() +} + +func (p *parser2) parseArg() Node { + switch p.tok { + case ARG_DELIMITER: + p.next() + return &ArgDelimiter{} + case IDENT: + if _, ok := p.commands[p.lit]; ok { + return p.parseCommand() + } + return p.parseIdent() + case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: + return p.parseFlag() + } + + pos := p.pos + lit := p.lit + p.advanceArg() + return &BadArg{Literal: lit, From: pos, To: p.pos} +} + +func (p *parser2) advanceArg() { + for ; p.tok != EOL; p.next() { + switch p.tok { + case IDENT, LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: + return + } + } +} + +func (p *parser2) parseCommand() Node { + node := &Command{Name: p.lit, Values: map[string]string{}, Nodes: []Node{}} + + for i := 0; p.tok != EOL; i++ { + p.next() + + if _, ok := p.commands[p.lit]; ok { + break + } + + switch p.tok { + case ARG_DELIMITER: + continue + case IDENT, STDIN_FLAG: + node.Values[fmt.Sprintf("%d", i)] = p.lit + case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: + node.Nodes = append(node.Nodes, p.parseFlag()) + default: + break + } + } + + return node +} + +func (p *parser2) parseIdent() Node { + defer p.next() + + node := &Ident{Literal: p.lit} + return node +} + +func (p *parser2) parseFlag() Node { + defer p.next() + + switch p.tok { + case SHORT_FLAG: + return p.parseShortFlag() + case LONG_FLAG: + return p.parseLongFlag() + case COMPOUND_SHORT_FLAG: + return p.parseCompoundShortFlag() + } + + panic(fmt.Sprintf("token %v cannot be parsed as flag", p.tok)) +} + +func (p *parser2) parseShortFlag() Node { + node := &Flag{Name: string(p.lit[1])} + // TODO: moar stuff + return node +} + +func (p *parser2) parseLongFlag() Node { + node := &Flag{Name: string(p.lit[2:])} + // TODO: moar stuff + return node +} + +func (p *parser2) parseCompoundShortFlag() Node { + flagNodes := []Node{} + + withoutFlagPrefix := p.lit[1:] + + for _, r := range withoutFlagPrefix { + flagNodes = append(flagNodes, &Flag{Name: string(r)}) + } + + return &CompoundShortFlag{Nodes: flagNodes} +} + +func (p *parser2) parsePassthrough() Node { + nodes := []Node{} + + for ; p.tok != EOL; p.next() { + nodes = append(nodes, &Ident{Literal: p.lit}) + } + + return &PassthroughArgs{Nodes: nodes} +} diff --git a/argh/parser2_test.go b/argh/parser2_test.go new file mode 100644 index 0000000..7679d03 --- /dev/null +++ b/argh/parser2_test.go @@ -0,0 +1,36 @@ +package argh_test + +import ( + "testing" + + "git.meatballhat.com/x/box-o-sand/argh" + "github.com/davecgh/go-spew/spew" +) + +func TestParser2(t *testing.T) { + for _, tc := range []struct { + name string + args []string + commands []string + }{ + { + name: "basic", + args: []string{ + "pies", "-eat", "--wat", "hello", + }, + commands: []string{ + "hello", + }, + }, + } { + t.Run(tc.name, func(ct *testing.T) { + pt, err := argh.ParseArgs2(tc.args, tc.commands) + if err != nil { + ct.Logf("err=%+#v", err) + return + } + + spew.Dump(pt) + }) + } +} diff --git a/argh/parser_config.go b/argh/parser_config.go new file mode 100644 index 0000000..6b735db --- /dev/null +++ b/argh/parser_config.go @@ -0,0 +1,37 @@ +package argh + +const ( + OneOrMoreValue NValue = -2 + ZeroOrMoreValue NValue = -1 + ZeroValue NValue = 0 +) + +var ( + DefaultParserConfig = &ParserConfig{ + Commands: map[string]CommandConfig{}, + Flags: map[string]FlagConfig{}, + ScannerConfig: DefaultScannerConfig, + } +) + +type NValue int + +type ParserConfig struct { + Prog CommandConfig + Commands map[string]CommandConfig + Flags map[string]FlagConfig + + ScannerConfig *ScannerConfig +} + +type CommandConfig struct { + NValue NValue + ValueNames []string + Flags map[string]FlagConfig + Commands map[string]CommandConfig +} + +type FlagConfig struct { + NValue NValue + ValueNames []string +} diff --git a/argh/scanner.go b/argh/scanner.go index 27f01d0..c073ad0 100644 --- a/argh/scanner.go +++ b/argh/scanner.go @@ -4,8 +4,10 @@ import ( "bufio" "bytes" "errors" + "fmt" "io" "log" + "sort" "unicode" ) @@ -34,6 +36,74 @@ type ScannerConfig struct { MultiValueDelim rune } +// ScannerError is largely borrowed from go/scanner.Error +type ScannerError struct { + Pos Position + Msg string +} + +func (e ScannerError) Error() string { + if e.Pos.IsValid() { + return e.Pos.String() + ":" + e.Msg + } + return e.Msg +} + +// ScannerErrorList is largely borrowed from go/scanner.ErrorList +type ScannerErrorList []*ScannerError + +func (el *ScannerErrorList) Add(pos Position, msg string) { + *el = append(*el, &ScannerError{Pos: pos, Msg: msg}) +} + +func (el *ScannerErrorList) Reset() { *el = (*el)[0:0] } + +func (el ScannerErrorList) Len() int { return len(el) } + +func (el ScannerErrorList) Swap(i, j int) { el[i], el[j] = el[j], el[i] } + +func (el ScannerErrorList) Less(i, j int) bool { + e := &el[i].Pos + f := &el[j].Pos + + if e.Column != f.Column { + return e.Column < f.Column + } + + return el[i].Msg < el[j].Msg +} + +func (el ScannerErrorList) Sort() { + sort.Sort(el) +} + +func (el ScannerErrorList) Error() string { + switch len(el) { + case 0: + return "no errors" + case 1: + return el[0].Error() + } + return fmt.Sprintf("%s (and %d more errors)", el[0], len(el)-1) +} + +func (el ScannerErrorList) Err() error { + if len(el) == 0 { + return nil + } + return el +} + +func PrintScannerError(w io.Writer, err error) { + if list, ok := err.(ScannerErrorList); ok { + for _, e := range list { + fmt.Fprintf(w, "%s\n", e) + } + } else if err != nil { + fmt.Fprintf(w, "%s\n", err) + } +} + func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { if cfg == nil { cfg = DefaultScannerConfig @@ -45,7 +115,7 @@ func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { } } -func (s *Scanner) Scan() (Token, string, int) { +func (s *Scanner) Scan() (Token, string, Pos) { ch, pos := s.read() if s.isBlankspace(ch) { @@ -77,24 +147,24 @@ func (s *Scanner) Scan() (Token, string, int) { return ILLEGAL, string(ch), pos } -func (s *Scanner) read() (rune, int) { +func (s *Scanner) read() (rune, Pos) { ch, _, err := s.r.ReadRune() s.i++ if errors.Is(err, io.EOF) { - return eol, s.i + return eol, Pos(s.i) } else if err != nil { log.Printf("unknown scanner error=%+v", err) - return eol, s.i + return eol, Pos(s.i) } - return ch, s.i + return ch, Pos(s.i) } -func (s *Scanner) unread() int { +func (s *Scanner) unread() Pos { _ = s.r.UnreadRune() s.i-- - return s.i + return Pos(s.i) } func (s *Scanner) isBlankspace(ch rune) bool { @@ -117,7 +187,7 @@ func (s *Scanner) isAssignmentOperator(ch rune) bool { return ch == s.cfg.AssignmentOperator } -func (s *Scanner) scanBlankspace() (Token, string, int) { +func (s *Scanner) scanBlankspace() (Token, string, Pos) { buf := &bytes.Buffer{} ch, pos := s.read() buf.WriteRune(ch) @@ -138,7 +208,7 @@ func (s *Scanner) scanBlankspace() (Token, string, int) { return BS, buf.String(), pos } -func (s *Scanner) scanArg() (Token, string, int) { +func (s *Scanner) scanArg() (Token, string, Pos) { buf := &bytes.Buffer{} ch, pos := s.read() buf.WriteRune(ch) diff --git a/argh/token.go b/argh/token.go index ec3f758..9e70b81 100644 --- a/argh/token.go +++ b/argh/token.go @@ -2,21 +2,49 @@ package argh +import "fmt" + const ( ILLEGAL Token = iota EOL - EMPTY - BS - IDENT - ARG_DELIMITER - COMMAND - ASSIGN - MULTI_VALUE_DELIMITER - LONG_FLAG - SHORT_FLAG - COMPOUND_SHORT_FLAG - STDIN_FLAG - STOP_FLAG + EMPTY // '' + BS // ' ' '\t' '\n' + IDENT // char group without flag prefix: 'some' 'words' + ARG_DELIMITER // rune(0) + ASSIGN // '=' + MULTI_VALUE_DELIMITER // ',' + LONG_FLAG // char group with double flag prefix: '--flag' + SHORT_FLAG // single char with single flag prefix: '-f' + COMPOUND_SHORT_FLAG // char group with single flag prefix: '-flag' + STDIN_FLAG // '-' + STOP_FLAG // '--' ) type Token int + +// Position is adapted from go/token.Position +type Position struct { + Column int +} + +func (p *Position) IsValid() bool { return p.Column > 0 } + +func (p Position) String() string { + s := "" + if p.IsValid() { + s = fmt.Sprintf("%d", p.Column) + } + if s == "" { + s = "-" + } + return s +} + +// Pos is borrowed from go/token.Pos +type Pos int + +const NoPos Pos = 0 + +func (p Pos) IsValid() bool { + return p != NoPos +} diff --git a/argh/token_string.go b/argh/token_string.go index 8c1b585..ff6a07e 100644 --- a/argh/token_string.go +++ b/argh/token_string.go @@ -14,19 +14,18 @@ func _() { _ = x[BS-3] _ = x[IDENT-4] _ = x[ARG_DELIMITER-5] - _ = x[COMMAND-6] - _ = x[ASSIGN-7] - _ = x[MULTI_VALUE_DELIMITER-8] - _ = x[LONG_FLAG-9] - _ = x[SHORT_FLAG-10] - _ = x[COMPOUND_SHORT_FLAG-11] - _ = x[STDIN_FLAG-12] - _ = x[STOP_FLAG-13] + _ = x[ASSIGN-6] + _ = x[MULTI_VALUE_DELIMITER-7] + _ = x[LONG_FLAG-8] + _ = x[SHORT_FLAG-9] + _ = x[COMPOUND_SHORT_FLAG-10] + _ = x[STDIN_FLAG-11] + _ = x[STOP_FLAG-12] } -const _Token_name = "ILLEGALEOLEMPTYBSIDENTARG_DELIMITERCOMMANDASSIGNMULTI_VALUE_DELIMITERLONG_FLAGSHORT_FLAGCOMPOUND_SHORT_FLAGSTDIN_FLAGSTOP_FLAG" +const _Token_name = "ILLEGALEOLEMPTYBSIDENTARG_DELIMITERASSIGNMULTI_VALUE_DELIMITERLONG_FLAGSHORT_FLAGCOMPOUND_SHORT_FLAGSTDIN_FLAGSTOP_FLAG" -var _Token_index = [...]uint8{0, 7, 10, 15, 17, 22, 35, 42, 48, 69, 78, 88, 107, 117, 126} +var _Token_index = [...]uint8{0, 7, 10, 15, 17, 22, 35, 41, 62, 71, 81, 100, 110, 119} func (i Token) String() string { if i < 0 || i >= Token(len(_Token_index)-1) { From dc3a40b19daaf0f169a59d52881269f201d74ed9 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 22 May 2022 20:49:11 -0400 Subject: [PATCH 10/23] Continuing the work with parser that's more like go/parser --- argh/node.go | 6 - argh/parser.go | 10 +- argh/parser2.go | 126 ++++++----- argh/parser2_test.go | 463 +++++++++++++++++++++++++++++++++++++++-- argh/parser_config.go | 23 +- argh/parser_test.go | 136 ++++++------ argh/querier.go | 30 +-- argh/querier_test.go | 12 +- argh/scanner.go | 133 ++---------- argh/scanner_config.go | 39 ++++ argh/scanner_error.go | 75 +++++++ argh/token.go | 3 + 12 files changed, 766 insertions(+), 290 deletions(-) create mode 100644 argh/scanner_config.go create mode 100644 argh/scanner_error.go diff --git a/argh/node.go b/argh/node.go index f5bddf0..c8fccda 100644 --- a/argh/node.go +++ b/argh/node.go @@ -15,12 +15,6 @@ type CompoundShortFlag struct { Nodes []Node `json:"nodes"` } -type Program struct { - Name string `json:"name"` - Values map[string]string `json:"values"` - Nodes []Node `json:"nodes"` -} - type Ident struct { Literal string `json:"literal"` } diff --git a/argh/parser.go b/argh/parser.go index 713356a..f9ec92a 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -46,7 +46,7 @@ type scanEntry struct { func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { if pCfg == nil { - pCfg = DefaultParserConfig + pCfg = POSIXyParserConfig } parser := &Parser{ @@ -140,10 +140,10 @@ func (p *Parser) scanCommandOrIdent() (Node, error) { return nil, err } - return Program{Name: lit, Values: values}, nil + return Command{Name: lit, Values: values}, nil } - if cfg, ok := p.cfg.Commands[lit]; ok { + if cfg, ok := p.cfg.Prog.Commands[lit]; ok { p.unscan(tok, lit, pos) values, err := p.scanValues(cfg.NValue, cfg.ValueNames) if err != nil { @@ -164,7 +164,7 @@ func (p *Parser) scanFlag() (Node, error) { flagName = string(lit[2:]) } - if cfg, ok := p.cfg.Flags[flagName]; ok { + if cfg, ok := p.cfg.Prog.Flags[flagName]; ok { p.unscan(tok, flagName, pos) values, err := p.scanValues(cfg.NValue, cfg.ValueNames) @@ -189,7 +189,7 @@ func (p *Parser) scanCompoundShortFlag() (Node, error) { if i == len(withoutFlagPrefix)-1 { flagName := string(r) - if cfg, ok := p.cfg.Flags[flagName]; ok { + if cfg, ok := p.cfg.Prog.Flags[flagName]; ok { p.unscan(tok, flagName, pos) values, err := p.scanValues(cfg.NValue, cfg.ValueNames) diff --git a/argh/parser2.go b/argh/parser2.go index f428c5d..1efb242 100644 --- a/argh/parser2.go +++ b/argh/parser2.go @@ -9,7 +9,7 @@ import ( type parser2 struct { s *Scanner - commands map[string]struct{} + cfg *ParserConfig errors ScannerErrorList @@ -18,11 +18,11 @@ type parser2 struct { pos Pos } -func ParseArgs2(args, commands []string) (*ParseTree, error) { +func ParseArgs2(args []string, pCfg *ParserConfig) (*ParseTree, error) { parser := &parser2{} parser.init( strings.NewReader(strings.Join(args, string(nul))), - commands, + pCfg, ) tracef("ParseArgs2 parser=%+#v", parser) @@ -30,16 +30,16 @@ func ParseArgs2(args, commands []string) (*ParseTree, error) { return parser.parseArgs() } -func (p *parser2) init(r io.Reader, commands []string) { +func (p *parser2) init(r io.Reader, pCfg *ParserConfig) { p.errors = ScannerErrorList{} - commandMap := map[string]struct{}{} - for _, c := range commands { - commandMap[c] = struct{}{} + if pCfg == nil { + pCfg = POSIXyParserConfig } - p.s = NewScanner(r, nil) - p.commands = commandMap + p.cfg = pCfg + + p.s = NewScanner(r, pCfg.ScannerConfig) p.next() } @@ -50,84 +50,96 @@ func (p *parser2) parseArgs() (*ParseTree, error) { return nil, p.errors.Err() } - prog := &Program{ - Name: p.lit, - Values: map[string]string{}, - Nodes: []Node{}, - } - p.next() + prog := p.parseCommand(&p.cfg.Prog) - for p.tok != EOL && p.tok != STOP_FLAG { - prog.Nodes = append(prog.Nodes, p.parseArg()) + nodes := []Node{prog} + if v := p.parsePassthrough(); v != nil { + nodes = append(nodes, v) } return &ParseTree{ - Nodes: []Node{ - prog, p.parsePassthrough(), - }, + Nodes: nodes, }, nil } func (p *parser2) next() { - tracef("parser2.next() <- %v %q %v", p.tok, p.lit, p.pos) - defer func() { - tracef("parser2.next() -> %v %q %v", p.tok, p.lit, p.pos) - }() + tracef("parser2.next() current: %v %q %v", p.tok, p.lit, p.pos) p.tok, p.lit, p.pos = p.s.Scan() -} - -func (p *parser2) parseArg() Node { - switch p.tok { - case ARG_DELIMITER: - p.next() - return &ArgDelimiter{} - case IDENT: - if _, ok := p.commands[p.lit]; ok { - return p.parseCommand() - } - return p.parseIdent() - case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: - return p.parseFlag() - } - pos := p.pos - lit := p.lit - p.advanceArg() - return &BadArg{Literal: lit, From: pos, To: p.pos} + tracef("parser2.next() next: %v %q %v", p.tok, p.lit, p.pos) } -func (p *parser2) advanceArg() { - for ; p.tok != EOL; p.next() { - switch p.tok { - case IDENT, LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: - return - } +func (p *parser2) parseCommand(cCfg *CommandConfig) Node { + tracef("parseCommand cfg=%+#v", cCfg) + + node := &Command{ + Name: p.lit, + Values: map[string]string{}, + Nodes: []Node{}, } -} -func (p *parser2) parseCommand() Node { - node := &Command{Name: p.lit, Values: map[string]string{}, Nodes: []Node{}} + identIndex := 0 for i := 0; p.tok != EOL; i++ { p.next() - if _, ok := p.commands[p.lit]; ok { + tracef("parseCommand for=%d node.Values=%+#v", i, node.Values) + tracef("parseCommand for=%d node.Nodes=%+#v", i, node.Values) + + if subCfg, ok := cCfg.Commands[p.lit]; ok { + subCommand := p.lit + + node.Nodes = append(node.Nodes, p.parseCommand(&subCfg)) + + tracef("parseCommand breaking after sub-command=%v", subCommand) break } switch p.tok { case ARG_DELIMITER: + tracef("parseCommand handling %s", p.tok) + + node.Nodes = append(node.Nodes, &ArgDelimiter{}) + continue case IDENT, STDIN_FLAG: - node.Values[fmt.Sprintf("%d", i)] = p.lit + tracef("parseCommand handling %s", p.tok) + + if !cCfg.NValue.Contains(identIndex) { + tracef("parseCommand identIndex=%d exceeds expected=%s; breaking", identIndex, cCfg.NValue) + break + } + + name := fmt.Sprintf("%d", identIndex) + + tracef("parseCommand checking for name of identIndex=%d", identIndex) + + if len(cCfg.ValueNames) > identIndex { + name = cCfg.ValueNames[identIndex] + tracef("parseCommand setting name=%s from config value names", name) + } else if len(cCfg.ValueNames) == 1 && (cCfg.NValue == OneOrMoreValue || cCfg.NValue == ZeroOrMoreValue) { + name = fmt.Sprintf("%s.%d", cCfg.ValueNames[0], identIndex) + tracef("parseCommand setting name=%s from repeating value name", name) + } + + node.Values[name] = p.lit + + identIndex++ case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: - node.Nodes = append(node.Nodes, p.parseFlag()) + tok := p.tok + flagNode := p.parseFlag() + + tracef("parseCommand appending %s node=%+#v", tok, flagNode) + + node.Nodes = append(node.Nodes, flagNode) default: + tracef("parseCommand breaking on %s", p.tok) break } } + tracef("parseCommand returning node=%+#v", node) return node } @@ -184,5 +196,9 @@ func (p *parser2) parsePassthrough() Node { nodes = append(nodes, &Ident{Literal: p.lit}) } + if len(nodes) == 0 { + return nil + } + return &PassthroughArgs{Nodes: nodes} } diff --git a/argh/parser2_test.go b/argh/parser2_test.go index 7679d03..169c4c3 100644 --- a/argh/parser2_test.go +++ b/argh/parser2_test.go @@ -5,32 +5,465 @@ import ( "git.meatballhat.com/x/box-o-sand/argh" "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/assert" ) func TestParser2(t *testing.T) { for _, tc := range []struct { - name string - args []string - commands []string + name string + args []string + cfg *argh.ParserConfig + expErr error + expPT []argh.Node + expAST []argh.Node + skip bool }{ { name: "basic", args: []string{ - "pies", "-eat", "--wat", "hello", + "pies", "-eat", "--wat", "hello", "mario", }, - commands: []string{ - "hello", + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "hello": argh.CommandConfig{ + NValue: 1, + ValueNames: []string{"name"}, + }, + }, + }, + }, + expPT: []argh.Node{ + &argh.Command{ + Name: "pies", + Values: map[string]string{}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "t"}, + }, + }, + &argh.Flag{Name: "wat"}, + &argh.Command{ + Name: "hello", + Values: map[string]string{ + "name": "mario", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + }, + }, + }, + }, + }, + /* + expAST: []argh.Node{ + &argh.Command{ + Name: "pies", + Values: map[string]string{}, + Nodes: []argh.Node{ + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "t"}, + }, + }, + &argh.Flag{Name: "wat"}, + &argh.Command{ + Name: "hello", + Values: map[string]string{ + "name": "mario", + }, + Nodes: []argh.Node{}, + }, + }, + }, + }, + */ + }, + { + name: "bare", + args: []string{"pizzas"}, + expPT: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Values: map[string]string{}, + Nodes: []argh.Node{}, + }, + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Values: map[string]string{}, + Nodes: []argh.Node{}, + }, + }, + }, + { + skip: true, + + name: "one positional arg", + args: []string{"pizzas", "excel"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{NValue: 1}, + }, + expPT: []argh.Node{ + argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + }, + expAST: []argh.Node{ + argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + }, + }, + { + skip: true, + + name: "many positional args", + args: []string{"pizzas", "excel", "wildly", "when", "feral"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + NValue: argh.OneOrMoreValue, + ValueNames: []string{"word"}, + }, + }, + expPT: []argh.Node{ + argh.Command{ + Name: "pizzas", + Values: map[string]string{ + "word": "excel", + "word.1": "wildly", + "word.2": "when", + "word.3": "feral", + }, + }, + }, + expAST: []argh.Node{ + argh.Command{ + Name: "pizzas", + Values: map[string]string{ + "word": "excel", + "word.1": "wildly", + "word.2": "when", + "word.3": "feral", + }, + }, + }, + }, + { + skip: true, + + name: "long value-less flags", + args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, + expPT: []argh.Node{ + argh.Command{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.Command{Name: "pizzas"}, + argh.Flag{Name: "tasty"}, + argh.Flag{Name: "fresh"}, + argh.Flag{Name: "super-hot-right-now"}, + }, + }, + { + skip: true, + + name: "long flags mixed", + args: []string{ + "pizzas", + "--tasty", + "--fresh", "soon", + "--super-hot-right-now", + "--box", "square", "shaped", "hot", + "--please", + }, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{}, + Flags: map[string]argh.FlagConfig{ + "fresh": argh.FlagConfig{NValue: 1}, + "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, + }, + }, + }, + expPT: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "tasty"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, + argh.ArgDelimiter{}, + argh.Flag{Name: "super-hot-right-now"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, + argh.ArgDelimiter{}, + argh.Flag{Name: "please"}, + }, + expAST: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.Flag{Name: "tasty"}, + argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, + argh.Flag{Name: "super-hot-right-now"}, + argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, + argh.Flag{Name: "please"}, + }, + }, + { + skip: true, + + name: "short value-less flags", + args: []string{"pizzas", "-t", "-f", "-s"}, + expPT: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "t"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "f"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "s"}, + }, + expAST: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.Flag{Name: "t"}, + argh.Flag{Name: "f"}, + argh.Flag{Name: "s"}, + }, + }, + { + skip: true, + + name: "compound short flags", + args: []string{"pizzas", "-aca", "-blol"}, + expPT: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.CompoundShortFlag{ + Nodes: []argh.Node{ + argh.Flag{Name: "a"}, + argh.Flag{Name: "c"}, + argh.Flag{Name: "a"}, + }, + }, + argh.ArgDelimiter{}, + argh.CompoundShortFlag{ + Nodes: []argh.Node{ + argh.Flag{Name: "b"}, + argh.Flag{Name: "l"}, + argh.Flag{Name: "o"}, + argh.Flag{Name: "l"}, + }, + }, + }, + expAST: []argh.Node{ + argh.Command{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: "mixed long short value flags", + args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{}, + Flags: map[string]argh.FlagConfig{ + "b": argh.FlagConfig{NValue: 1}, + }, + }, + }, + expPT: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "a"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "ca"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, + argh.ArgDelimiter{}, + argh.CompoundShortFlag{ + Nodes: []argh.Node{ + argh.Flag{Name: "l"}, + argh.Flag{Name: "o"}, + argh.Flag{Name: "l"}, + }, + }, + }, + expAST: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.Flag{Name: "a"}, + argh.Flag{Name: "ca"}, + argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, + argh.Flag{Name: "l"}, + argh.Flag{Name: "o"}, + argh.Flag{Name: "l"}, + }, + }, + { + skip: true, + + name: "commands", + args: []string{"pizzas", "fly", "fry"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{}, + "fry": argh.CommandConfig{}, + }, + Flags: map[string]argh.FlagConfig{}, + }, + }, + expPT: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fly"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fry"}, + }, + }, + { + skip: true, + + name: "command specific flags", + args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "freely": {}, + }, + }, + "fry": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "deeply": {}, + "w": {}, + "A": {}, + "t": {}, + }, + }, + }, + Flags: map[string]argh.FlagConfig{}, + }, + }, + expPT: []argh.Node{ + argh.Command{Name: "pizzas"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fly"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "freely"}, + argh.ArgDelimiter{}, + argh.Command{Name: "fry"}, + argh.ArgDelimiter{}, + argh.Flag{Name: "deeply"}, + argh.ArgDelimiter{}, + argh.CompoundShortFlag{ + Nodes: []argh.Node{ + argh.Flag{Name: "w"}, + argh.Flag{Name: "A"}, + argh.Flag{Name: "t"}, + }, + }, + }, + }, + { + skip: true, + + name: "total weirdo", + args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "goose": argh.CommandConfig{NValue: 1}, + }, + Flags: map[string]argh.FlagConfig{ + "w": argh.FlagConfig{}, + "A": argh.FlagConfig{}, + "T": argh.FlagConfig{NValue: 1}, + "hecking": argh.FlagConfig{}, + "FIERCENESS": argh.FlagConfig{NValue: 1}, + }, + }, + ScannerConfig: &argh.ScannerConfig{ + AssignmentOperator: '@', + FlagPrefix: '^', + MultiValueDelim: ',', + }, + }, + expPT: []argh.Node{ + argh.Command{Name: "PIZZAs"}, + argh.ArgDelimiter{}, + argh.CompoundShortFlag{ + Nodes: []argh.Node{ + argh.Flag{Name: "w"}, + argh.Flag{Name: "A"}, + argh.Flag{Name: "T", Values: map[string]string{"0": "golf"}}, + }, + }, + argh.ArgDelimiter{}, + argh.Flag{Name: "hecKing"}, + argh.ArgDelimiter{}, + argh.Command{Name: "goose", Values: map[string]string{"0": "bonk"}}, + argh.ArgDelimiter{}, + argh.Flag{Name: "FIERCENESS", Values: map[string]string{"0": "-2"}}, + }, + }, + { + skip: true, + name: "invalid bare assignment", + args: []string{"pizzas", "=", "--wat"}, + expErr: argh.ErrSyntax, + expPT: []argh.Node{ + argh.Command{Name: "pizzas"}, }, }, } { - t.Run(tc.name, func(ct *testing.T) { - pt, err := argh.ParseArgs2(tc.args, tc.commands) - if err != nil { - ct.Logf("err=%+#v", err) - return - } - - spew.Dump(pt) - }) + if tc.skip { + continue + } + + if tc.expPT != nil { + t.Run(tc.name+" parse tree", func(ct *testing.T) { + pt, err := argh.ParseArgs2(tc.args, tc.cfg) + if err != nil { + assert.ErrorIs(ct, err, tc.expErr) + return + } + + if !assert.Equal(ct, tc.expPT, pt.Nodes) { + spew.Dump(pt) + } + }) + } + + if tc.expAST != nil { + t.Run(tc.name+" ast", func(ct *testing.T) { + pt, err := argh.ParseArgs2(tc.args, tc.cfg) + if err != nil { + ct.Logf("err=%+#v", err) + return + } + + ast := argh.NewQuerier(pt.Nodes).AST() + + if !assert.Equal(ct, tc.expAST, ast) { + spew.Dump(ast) + } + }) + } } } diff --git a/argh/parser_config.go b/argh/parser_config.go index 6b735db..62b209e 100644 --- a/argh/parser_config.go +++ b/argh/parser_config.go @@ -7,19 +7,28 @@ const ( ) var ( - DefaultParserConfig = &ParserConfig{ - Commands: map[string]CommandConfig{}, - Flags: map[string]FlagConfig{}, - ScannerConfig: DefaultScannerConfig, + POSIXyParserConfig = &ParserConfig{ + Prog: CommandConfig{}, + ScannerConfig: POSIXyScannerConfig, } ) type NValue int +func (nv NValue) Contains(i int) bool { + if i < int(ZeroValue) { + return false + } + + if nv == OneOrMoreValue || nv == ZeroOrMoreValue { + return true + } + + return int(nv) > i +} + type ParserConfig struct { - Prog CommandConfig - Commands map[string]CommandConfig - Flags map[string]FlagConfig + Prog CommandConfig ScannerConfig *ScannerConfig } diff --git a/argh/parser_test.go b/argh/parser_test.go index 9cb5bb5..5f89369 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -21,10 +21,10 @@ func TestParser(t *testing.T) { name: "bare", args: []string{"pizzas"}, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, }, }, { @@ -34,10 +34,10 @@ func TestParser(t *testing.T) { Prog: argh.CommandConfig{NValue: 1}, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, }, }, { @@ -47,17 +47,17 @@ func TestParser(t *testing.T) { Prog: argh.CommandConfig{NValue: argh.OneOrMoreValue}, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, + argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, + argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, }, }, { name: "long value-less flags", args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Flag{Name: "tasty"}, argh.ArgDelimiter{}, @@ -66,7 +66,7 @@ func TestParser(t *testing.T) { argh.Flag{Name: "super-hot-right-now"}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.Flag{Name: "tasty"}, argh.Flag{Name: "fresh"}, argh.Flag{Name: "super-hot-right-now"}, @@ -83,14 +83,16 @@ func TestParser(t *testing.T) { "--please", }, cfg: &argh.ParserConfig{ - Commands: map[string]argh.CommandConfig{}, - Flags: map[string]argh.FlagConfig{ - "fresh": argh.FlagConfig{NValue: 1}, - "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{}, + Flags: map[string]argh.FlagConfig{ + "fresh": argh.FlagConfig{NValue: 1}, + "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, + }, }, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Flag{Name: "tasty"}, argh.ArgDelimiter{}, @@ -103,7 +105,7 @@ func TestParser(t *testing.T) { argh.Flag{Name: "please"}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.Flag{Name: "tasty"}, argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, argh.Flag{Name: "super-hot-right-now"}, @@ -115,7 +117,7 @@ func TestParser(t *testing.T) { name: "short value-less flags", args: []string{"pizzas", "-t", "-f", "-s"}, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Flag{Name: "t"}, argh.ArgDelimiter{}, @@ -124,7 +126,7 @@ func TestParser(t *testing.T) { argh.Flag{Name: "s"}, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.Flag{Name: "t"}, argh.Flag{Name: "f"}, argh.Flag{Name: "s"}, @@ -134,7 +136,7 @@ func TestParser(t *testing.T) { name: "compound short flags", args: []string{"pizzas", "-aca", "-blol"}, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.ArgDelimiter{}, argh.CompoundShortFlag{ Nodes: []argh.Node{ @@ -154,7 +156,7 @@ func TestParser(t *testing.T) { }, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.Flag{Name: "a"}, argh.Flag{Name: "c"}, argh.Flag{Name: "a"}, @@ -168,13 +170,15 @@ func TestParser(t *testing.T) { name: "mixed long short value flags", args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.CommandConfig{}, - Flags: map[string]argh.FlagConfig{ - "b": argh.FlagConfig{NValue: 1}, + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{}, + Flags: map[string]argh.FlagConfig{ + "b": argh.FlagConfig{NValue: 1}, + }, }, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Flag{Name: "a"}, argh.ArgDelimiter{}, @@ -191,7 +195,7 @@ func TestParser(t *testing.T) { }, }, expAST: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.Flag{Name: "a"}, argh.Flag{Name: "ca"}, argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, @@ -204,14 +208,16 @@ func TestParser(t *testing.T) { name: "commands", args: []string{"pizzas", "fly", "fry"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{}, - "fry": argh.CommandConfig{}, + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{}, + "fry": argh.CommandConfig{}, + }, + Flags: map[string]argh.FlagConfig{}, }, - Flags: map[string]argh.FlagConfig{}, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Command{Name: "fly"}, argh.ArgDelimiter{}, @@ -222,25 +228,27 @@ func TestParser(t *testing.T) { name: "command specific flags", args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "freely": {}, + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "freely": {}, + }, }, - }, - "fry": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "deeply": {}, - "w": {}, - "A": {}, - "t": {}, + "fry": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "deeply": {}, + "w": {}, + "A": {}, + "t": {}, + }, }, }, + Flags: map[string]argh.FlagConfig{}, }, - Flags: map[string]argh.FlagConfig{}, }, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, argh.ArgDelimiter{}, argh.Command{Name: "fly"}, argh.ArgDelimiter{}, @@ -263,15 +271,17 @@ func TestParser(t *testing.T) { name: "total weirdo", args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, cfg: &argh.ParserConfig{ - Commands: map[string]argh.CommandConfig{ - "goose": argh.CommandConfig{NValue: 1}, - }, - Flags: map[string]argh.FlagConfig{ - "w": argh.FlagConfig{}, - "A": argh.FlagConfig{}, - "T": argh.FlagConfig{NValue: 1}, - "hecking": argh.FlagConfig{}, - "FIERCENESS": argh.FlagConfig{NValue: 1}, + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "goose": argh.CommandConfig{NValue: 1}, + }, + Flags: map[string]argh.FlagConfig{ + "w": argh.FlagConfig{}, + "A": argh.FlagConfig{}, + "T": argh.FlagConfig{NValue: 1}, + "hecking": argh.FlagConfig{}, + "FIERCENESS": argh.FlagConfig{NValue: 1}, + }, }, ScannerConfig: &argh.ScannerConfig{ AssignmentOperator: '@', @@ -280,7 +290,7 @@ func TestParser(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Program{Name: "PIZZAs"}, + argh.Command{Name: "PIZZAs"}, argh.ArgDelimiter{}, argh.CompoundShortFlag{ Nodes: []argh.Node{ @@ -302,7 +312,7 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "=", "--wat"}, expErr: argh.ErrSyntax, expPT: []argh.Node{ - argh.Program{Name: "pizzas"}, + argh.Command{Name: "pizzas"}, }, }, {}, @@ -323,16 +333,18 @@ func TestParser(t *testing.T) { }) } - 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 - } + /* + 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, argh.NewQuerier(actual).AST()) - }) - } + assert.Equal(ct, tc.expAST, argh.NewQuerier(actual.Nodes).AST()) + }) + } + */ } } diff --git a/argh/querier.go b/argh/querier.go index 27e2b9c..a64f075 100644 --- a/argh/querier.go +++ b/argh/querier.go @@ -3,32 +3,32 @@ package argh import "fmt" type Querier interface { - Program() (Program, bool) + Program() (Command, bool) TypedAST() []TypedNode AST() []Node } -func NewQuerier(pt *ParseTree) Querier { - return &defaultQuerier{pt: pt} +func NewQuerier(nodes []Node) Querier { + return &defaultQuerier{nodes: nodes} } type defaultQuerier struct { - pt *ParseTree + nodes []Node } -func (dq *defaultQuerier) Program() (Program, bool) { - if len(dq.pt.Nodes) == 0 { - return Program{}, false +func (dq *defaultQuerier) Program() (Command, bool) { + if len(dq.nodes) == 0 { + return Command{}, false } - v, ok := dq.pt.Nodes[0].(Program) + v, ok := dq.nodes[0].(Command) return v, ok } func (dq *defaultQuerier) TypedAST() []TypedNode { ret := []TypedNode{} - for _, node := range dq.pt.Nodes { + for _, node := range dq.nodes { if _, ok := node.(ArgDelimiter); ok { continue } @@ -52,7 +52,7 @@ func (dq *defaultQuerier) TypedAST() []TypedNode { func (dq *defaultQuerier) AST() []Node { ret := []Node{} - for _, node := range dq.pt.Nodes { + for _, node := range dq.nodes { if _, ok := node.(ArgDelimiter); ok { continue } @@ -62,9 +62,13 @@ func (dq *defaultQuerier) AST() []Node { } if v, ok := node.(CompoundShortFlag); ok { - for _, subNode := range v.Nodes { - ret = append(ret, subNode) - } + ret = append(ret, NewQuerier(v.Nodes).AST()...) + + continue + } + + if v, ok := node.(Command); ok { + ret = append(ret, NewQuerier(v.Nodes).AST()...) continue } diff --git a/argh/querier_test.go b/argh/querier_test.go index d37c7e1..f335e6e 100644 --- a/argh/querier_test.go +++ b/argh/querier_test.go @@ -12,31 +12,31 @@ func TestQuerier_Program(t *testing.T) { name string args []string cfg *argh.ParserConfig - exp argh.Program + exp argh.Command expOK bool }{ { name: "typical", args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, - exp: argh.Program{Name: "pizzas"}, + exp: argh.Command{Name: "pizzas"}, expOK: true, }, { name: "minimal", args: []string{"pizzas"}, - exp: argh.Program{Name: "pizzas"}, + exp: argh.Command{Name: "pizzas"}, expOK: true, }, { name: "invalid", args: []string{}, - exp: argh.Program{}, + exp: argh.Command{}, expOK: false, }, { name: "invalid flag only", args: []string{"--oh-no"}, - exp: argh.Program{}, + exp: argh.Command{}, expOK: false, }, } { @@ -44,7 +44,7 @@ func TestQuerier_Program(t *testing.T) { pt, err := argh.ParseArgs(tc.args, tc.cfg) require.Nil(ct, err) - prog, ok := argh.NewQuerier(pt).Program() + prog, ok := argh.NewQuerier(pt.Nodes).Program() require.Equal(ct, tc.exp, prog) require.Equal(ct, tc.expOK, ok) }) diff --git a/argh/scanner.go b/argh/scanner.go index c073ad0..3cc8d45 100644 --- a/argh/scanner.go +++ b/argh/scanner.go @@ -4,109 +4,20 @@ import ( "bufio" "bytes" "errors" - "fmt" "io" "log" - "sort" "unicode" ) -const ( - nul = rune(0) - eol = rune(-1) -) - -var ( - DefaultScannerConfig = &ScannerConfig{ - AssignmentOperator: '=', - FlagPrefix: '-', - MultiValueDelim: ',', - } -) - type Scanner struct { r *bufio.Reader i int cfg *ScannerConfig } -type ScannerConfig struct { - AssignmentOperator rune - FlagPrefix rune - MultiValueDelim rune -} - -// ScannerError is largely borrowed from go/scanner.Error -type ScannerError struct { - Pos Position - Msg string -} - -func (e ScannerError) Error() string { - if e.Pos.IsValid() { - return e.Pos.String() + ":" + e.Msg - } - return e.Msg -} - -// ScannerErrorList is largely borrowed from go/scanner.ErrorList -type ScannerErrorList []*ScannerError - -func (el *ScannerErrorList) Add(pos Position, msg string) { - *el = append(*el, &ScannerError{Pos: pos, Msg: msg}) -} - -func (el *ScannerErrorList) Reset() { *el = (*el)[0:0] } - -func (el ScannerErrorList) Len() int { return len(el) } - -func (el ScannerErrorList) Swap(i, j int) { el[i], el[j] = el[j], el[i] } - -func (el ScannerErrorList) Less(i, j int) bool { - e := &el[i].Pos - f := &el[j].Pos - - if e.Column != f.Column { - return e.Column < f.Column - } - - return el[i].Msg < el[j].Msg -} - -func (el ScannerErrorList) Sort() { - sort.Sort(el) -} - -func (el ScannerErrorList) Error() string { - switch len(el) { - case 0: - return "no errors" - case 1: - return el[0].Error() - } - return fmt.Sprintf("%s (and %d more errors)", el[0], len(el)-1) -} - -func (el ScannerErrorList) Err() error { - if len(el) == 0 { - return nil - } - return el -} - -func PrintScannerError(w io.Writer, err error) { - if list, ok := err.(ScannerErrorList); ok { - for _, e := range list { - fmt.Fprintf(w, "%s\n", e) - } - } else if err != nil { - fmt.Fprintf(w, "%s\n", err) - } -} - func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { if cfg == nil { - cfg = DefaultScannerConfig + cfg = POSIXyScannerConfig } return &Scanner{ @@ -118,16 +29,16 @@ func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { func (s *Scanner) Scan() (Token, string, Pos) { ch, pos := s.read() - if s.isBlankspace(ch) { + if s.cfg.IsBlankspace(ch) { _ = s.unread() return s.scanBlankspace() } - if s.isAssignmentOperator(ch) { + if s.cfg.IsAssignmentOperator(ch) { return ASSIGN, string(ch), pos } - if s.isMultiValueDelim(ch) { + if s.cfg.IsMultiValueDelim(ch) { return MULTI_VALUE_DELIMITER, string(ch), pos } @@ -167,26 +78,6 @@ func (s *Scanner) unread() Pos { return Pos(s.i) } -func (s *Scanner) isBlankspace(ch rune) bool { - return ch == ' ' || ch == '\t' || ch == '\n' -} - -func (s *Scanner) isUnderscore(ch rune) bool { - return ch == '_' -} - -func (s *Scanner) isFlagPrefix(ch rune) bool { - return ch == s.cfg.FlagPrefix -} - -func (s *Scanner) isMultiValueDelim(ch rune) bool { - return ch == s.cfg.MultiValueDelim -} - -func (s *Scanner) isAssignmentOperator(ch rune) bool { - return ch == s.cfg.AssignmentOperator -} - func (s *Scanner) scanBlankspace() (Token, string, Pos) { buf := &bytes.Buffer{} ch, pos := s.read() @@ -197,7 +88,7 @@ func (s *Scanner) scanBlankspace() (Token, string, Pos) { if ch == eol { break - } else if !s.isBlankspace(ch) { + } else if !s.cfg.IsBlankspace(ch) { pos = s.unread() break } else { @@ -216,7 +107,7 @@ func (s *Scanner) scanArg() (Token, string, Pos) { for { ch, pos = s.read() - if ch == eol || ch == nul || s.isAssignmentOperator(ch) || s.isMultiValueDelim(ch) { + if ch == eol || ch == nul || s.cfg.IsAssignmentOperator(ch) || s.cfg.IsMultiValueDelim(ch) { pos = s.unread() break } @@ -233,11 +124,11 @@ func (s *Scanner) scanArg() (Token, string, Pos) { ch0 := rune(str[0]) if len(str) == 1 { - if s.isFlagPrefix(ch0) { + if s.cfg.IsFlagPrefix(ch0) { return STDIN_FLAG, str, pos } - if s.isAssignmentOperator(ch0) { + if s.cfg.IsAssignmentOperator(ch0) { return ASSIGN, str, pos } @@ -247,17 +138,17 @@ func (s *Scanner) scanArg() (Token, string, Pos) { ch1 := rune(str[1]) if len(str) == 2 { - if str == string(s.cfg.FlagPrefix)+string(s.cfg.FlagPrefix) { + if s.cfg.IsFlagPrefix(ch0) && s.cfg.IsFlagPrefix(ch1) { return STOP_FLAG, str, pos } - if s.isFlagPrefix(ch0) { + if s.cfg.IsFlagPrefix(ch0) { return SHORT_FLAG, str, pos } } - if s.isFlagPrefix(ch0) { - if s.isFlagPrefix(ch1) { + if s.cfg.IsFlagPrefix(ch0) { + if s.cfg.IsFlagPrefix(ch1) { return LONG_FLAG, str, pos } diff --git a/argh/scanner_config.go b/argh/scanner_config.go new file mode 100644 index 0000000..9bc9be2 --- /dev/null +++ b/argh/scanner_config.go @@ -0,0 +1,39 @@ +package argh + +var ( + // POSIXyScannerConfig defines a scanner config that uses '-' + // as the flag prefix, which also means that "--" is the "long + // flag" prefix, a bare "--" is considered STOP_FLAG, and a + // bare "-" is considered STDIN_FLAG. + POSIXyScannerConfig = &ScannerConfig{ + AssignmentOperator: '=', + FlagPrefix: '-', + MultiValueDelim: ',', + } +) + +type ScannerConfig struct { + AssignmentOperator rune + FlagPrefix rune + MultiValueDelim rune +} + +func (cfg *ScannerConfig) IsFlagPrefix(ch rune) bool { + return ch == cfg.FlagPrefix +} + +func (cfg *ScannerConfig) IsMultiValueDelim(ch rune) bool { + return ch == cfg.MultiValueDelim +} + +func (cfg *ScannerConfig) IsAssignmentOperator(ch rune) bool { + return ch == cfg.AssignmentOperator +} + +func (cfg *ScannerConfig) IsBlankspace(ch rune) bool { + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' +} + +func (cfg *ScannerConfig) IsUnderscore(ch rune) bool { + return ch == '_' +} diff --git a/argh/scanner_error.go b/argh/scanner_error.go new file mode 100644 index 0000000..c9b7441 --- /dev/null +++ b/argh/scanner_error.go @@ -0,0 +1,75 @@ +package argh + +import ( + "fmt" + "io" + "sort" +) + +// ScannerError is largely borrowed from go/scanner.Error +type ScannerError struct { + Pos Position + Msg string +} + +func (e ScannerError) Error() string { + if e.Pos.IsValid() { + return e.Pos.String() + ":" + e.Msg + } + return e.Msg +} + +// ScannerErrorList is largely borrowed from go/scanner.ErrorList +type ScannerErrorList []*ScannerError + +func (el *ScannerErrorList) Add(pos Position, msg string) { + *el = append(*el, &ScannerError{Pos: pos, Msg: msg}) +} + +func (el *ScannerErrorList) Reset() { *el = (*el)[0:0] } + +func (el ScannerErrorList) Len() int { return len(el) } + +func (el ScannerErrorList) Swap(i, j int) { el[i], el[j] = el[j], el[i] } + +func (el ScannerErrorList) Less(i, j int) bool { + e := &el[i].Pos + f := &el[j].Pos + + if e.Column != f.Column { + return e.Column < f.Column + } + + return el[i].Msg < el[j].Msg +} + +func (el ScannerErrorList) Sort() { + sort.Sort(el) +} + +func (el ScannerErrorList) Error() string { + switch len(el) { + case 0: + return "no errors" + case 1: + return el[0].Error() + } + return fmt.Sprintf("%s (and %d more errors)", el[0], len(el)-1) +} + +func (el ScannerErrorList) Err() error { + if len(el) == 0 { + return nil + } + return el +} + +func PrintScannerError(w io.Writer, err error) { + if list, ok := err.(ScannerErrorList); ok { + for _, e := range list { + fmt.Fprintf(w, "%s\n", e) + } + } else if err != nil { + fmt.Fprintf(w, "%s\n", err) + } +} diff --git a/argh/token.go b/argh/token.go index 9e70b81..b26b8b7 100644 --- a/argh/token.go +++ b/argh/token.go @@ -18,6 +18,9 @@ const ( COMPOUND_SHORT_FLAG // char group with single flag prefix: '-flag' STDIN_FLAG // '-' STOP_FLAG // '--' + + nul = rune(0) + eol = rune(-1) ) type Token int From d1ffbe25a31ecb8b71be7da2fd673dd48cd993a7 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 22 May 2022 21:43:02 -0400 Subject: [PATCH 11/23] Do AST better maybe? --- argh/parser2_test.go | 50 ++++++++++++++++++++++---------------------- argh/querier.go | 47 +++++++++++++---------------------------- 2 files changed, 39 insertions(+), 58 deletions(-) diff --git a/argh/parser2_test.go b/argh/parser2_test.go index 169c4c3..4549e94 100644 --- a/argh/parser2_test.go +++ b/argh/parser2_test.go @@ -59,31 +59,25 @@ func TestParser2(t *testing.T) { }, }, }, - /* - expAST: []argh.Node{ - &argh.Command{ - Name: "pies", - Values: map[string]string{}, - Nodes: []argh.Node{ - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "e"}, - &argh.Flag{Name: "a"}, - &argh.Flag{Name: "t"}, - }, - }, - &argh.Flag{Name: "wat"}, - &argh.Command{ - Name: "hello", - Values: map[string]string{ - "name": "mario", - }, - Nodes: []argh.Node{}, + expAST: []argh.Node{ + &argh.Command{ + Name: "pies", + Values: map[string]string{}, + Nodes: []argh.Node{ + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "t"}, + &argh.Flag{Name: "wat"}, + &argh.Command{ + Name: "hello", + Values: map[string]string{ + "name": "mario", }, + Nodes: []argh.Node{}, }, }, }, - */ + }, }, { name: "bare", @@ -432,12 +426,13 @@ func TestParser2(t *testing.T) { }, }, } { - if tc.skip { - continue - } - if tc.expPT != nil { t.Run(tc.name+" parse tree", func(ct *testing.T) { + if tc.skip { + ct.SkipNow() + return + } + pt, err := argh.ParseArgs2(tc.args, tc.cfg) if err != nil { assert.ErrorIs(ct, err, tc.expErr) @@ -452,6 +447,11 @@ func TestParser2(t *testing.T) { if tc.expAST != nil { t.Run(tc.name+" ast", func(ct *testing.T) { + if tc.skip { + ct.SkipNow() + return + } + pt, err := argh.ParseArgs2(tc.args, tc.cfg) if err != nil { ct.Logf("err=%+#v", err) diff --git a/argh/querier.go b/argh/querier.go index a64f075..f2f6c8a 100644 --- a/argh/querier.go +++ b/argh/querier.go @@ -1,10 +1,7 @@ package argh -import "fmt" - type Querier interface { Program() (Command, bool) - TypedAST() []TypedNode AST() []Node } @@ -25,50 +22,34 @@ func (dq *defaultQuerier) Program() (Command, bool) { return v, ok } -func (dq *defaultQuerier) TypedAST() []TypedNode { - ret := []TypedNode{} - - for _, node := range dq.nodes { - if _, ok := node.(ArgDelimiter); ok { - continue - } - - if _, ok := node.(StopFlag); ok { - continue - } - - ret = append( - ret, - TypedNode{ - Type: fmt.Sprintf("%T", node), - Node: node, - }, - ) - } - - return ret -} - func (dq *defaultQuerier) AST() []Node { ret := []Node{} - for _, node := range dq.nodes { - if _, ok := node.(ArgDelimiter); ok { + for i, node := range dq.nodes { + tracef("AST i=%d node type=%T", i, node) + + if _, ok := node.(*ArgDelimiter); ok { continue } - if _, ok := node.(StopFlag); ok { + if _, ok := node.(*StopFlag); ok { continue } - if v, ok := node.(CompoundShortFlag); ok { + if v, ok := node.(*CompoundShortFlag); ok { ret = append(ret, NewQuerier(v.Nodes).AST()...) continue } - if v, ok := node.(Command); ok { - ret = append(ret, NewQuerier(v.Nodes).AST()...) + if v, ok := node.(*Command); ok { + ret = append( + ret, + &Command{ + Name: v.Name, + Values: v.Values, + Nodes: NewQuerier(v.Nodes).AST(), + }) continue } From 03edacc8ec44b2b8ff9778dc640d0254f024c81c Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 May 2022 21:55:16 -0400 Subject: [PATCH 12/23] Implementing long flag values in parser2 --- argh/node.go | 21 +++--- argh/parser2.go | 171 ++++++++++++++++++++++++++++++++----------- argh/parser2_test.go | 134 ++++++++++++++++++++------------- argh/querier.go | 30 +++++++- 4 files changed, 250 insertions(+), 106 deletions(-) diff --git a/argh/node.go b/argh/node.go index c8fccda..a18e7b0 100644 --- a/argh/node.go +++ b/argh/node.go @@ -3,20 +3,20 @@ package argh type Node interface{} type TypedNode struct { - Type string `json:"type"` - Node Node `json:"node"` + Type string + Node Node } type PassthroughArgs struct { - Nodes []Node `json:"nodes"` + Nodes []Node } type CompoundShortFlag struct { - Nodes []Node `json:"nodes"` + Nodes []Node } type Ident struct { - Literal string `json:"literal"` + Literal string } type BadArg struct { @@ -26,14 +26,15 @@ type BadArg struct { } type Command struct { - Name string `json:"name"` - Values map[string]string `json:"values"` - Nodes []Node `json:"nodes"` + Name string + Values map[string]string + Nodes []Node } type Flag struct { - Name string `json:"name"` - Values map[string]string `json:"values"` + Name string + Values map[string]string + Nodes []Node } type StdinFlag struct{} diff --git a/argh/parser2.go b/argh/parser2.go index 1efb242..9aab14d 100644 --- a/argh/parser2.go +++ b/argh/parser2.go @@ -16,6 +16,8 @@ type parser2 struct { tok Token lit string pos Pos + + buffered bool } func ParseArgs2(args []string, pCfg *ParserConfig) (*ParseTree, error) { @@ -25,7 +27,7 @@ func ParseArgs2(args []string, pCfg *ParserConfig) (*ParseTree, error) { pCfg, ) - tracef("ParseArgs2 parser=%+#v", parser) + tracef("ParseArgs2(...) parser=%+#v", parser) return parser.parseArgs() } @@ -46,81 +48,93 @@ func (p *parser2) init(r io.Reader, pCfg *ParserConfig) { func (p *parser2) parseArgs() (*ParseTree, error) { if p.errors.Len() != 0 { - tracef("parseArgs bailing due to initial error") + tracef("parseArgs() bailing due to initial error") return nil, p.errors.Err() } + tracef("parseArgs() parsing %q as program command; cfg=%+#v", p.lit, p.cfg.Prog) prog := p.parseCommand(&p.cfg.Prog) nodes := []Node{prog} if v := p.parsePassthrough(); v != nil { + tracef("parseArgs() appending passthrough argument %v", v) nodes = append(nodes, v) } - return &ParseTree{ - Nodes: nodes, - }, nil + tracef("parseArgs() returning ParseTree") + + return &ParseTree{Nodes: nodes}, nil } func (p *parser2) next() { - tracef("parser2.next() current: %v %q %v", p.tok, p.lit, p.pos) + tracef("next() before scan: %v %q %v", p.tok, p.lit, p.pos) p.tok, p.lit, p.pos = p.s.Scan() - tracef("parser2.next() next: %v %q %v", p.tok, p.lit, p.pos) + tracef("next() after scan: %v %q %v", p.tok, p.lit, p.pos) } func (p *parser2) parseCommand(cCfg *CommandConfig) Node { - tracef("parseCommand cfg=%+#v", cCfg) + tracef("parseCommand(%+#v)", cCfg) node := &Command{ - Name: p.lit, - Values: map[string]string{}, - Nodes: []Node{}, + Name: p.lit, } + values := map[string]string{} + nodes := []Node{} identIndex := 0 for i := 0; p.tok != EOL; i++ { - p.next() + if !p.buffered { + tracef("parseCommand(...) buffered=false; scanning next") + p.next() + } + + p.buffered = false - tracef("parseCommand for=%d node.Values=%+#v", i, node.Values) - tracef("parseCommand for=%d node.Nodes=%+#v", i, node.Values) + tracef("parseCommand(...) for=%d values=%+#v", i, values) + tracef("parseCommand(...) for=%d nodes=%+#v", i, nodes) + tracef("parseCommand(...) for=%d tok=%s lit=%q pos=%v", i, p.tok, p.lit, p.pos) if subCfg, ok := cCfg.Commands[p.lit]; ok { subCommand := p.lit - node.Nodes = append(node.Nodes, p.parseCommand(&subCfg)) + nodes = append(nodes, p.parseCommand(&subCfg)) - tracef("parseCommand breaking after sub-command=%v", subCommand) + tracef("parseCommand(...) breaking after sub-command=%v", subCommand) break } switch p.tok { case ARG_DELIMITER: - tracef("parseCommand handling %s", p.tok) + tracef("parseCommand(...) handling %s", p.tok) - node.Nodes = append(node.Nodes, &ArgDelimiter{}) + nodes = append(nodes, &ArgDelimiter{}) continue case IDENT, STDIN_FLAG: - tracef("parseCommand handling %s", p.tok) + tracef("parseCommand(...) handling %s", p.tok) if !cCfg.NValue.Contains(identIndex) { - tracef("parseCommand identIndex=%d exceeds expected=%s; breaking", identIndex, cCfg.NValue) + tracef("parseCommand(...) identIndex=%d exceeds expected=%s; breaking", identIndex, cCfg.NValue) break } name := fmt.Sprintf("%d", identIndex) - tracef("parseCommand checking for name of identIndex=%d", identIndex) + tracef("parseCommand(...) checking for name of identIndex=%d", identIndex) if len(cCfg.ValueNames) > identIndex { name = cCfg.ValueNames[identIndex] - tracef("parseCommand setting name=%s from config value names", name) + tracef("parseCommand(...) setting name=%s from config value names", name) } else if len(cCfg.ValueNames) == 1 && (cCfg.NValue == OneOrMoreValue || cCfg.NValue == ZeroOrMoreValue) { name = fmt.Sprintf("%s.%d", cCfg.ValueNames[0], identIndex) - tracef("parseCommand setting name=%s from repeating value name", name) + tracef("parseCommand(...) setting name=%s from repeating value name", name) + } + + if node.Values == nil { + node.Values = map[string]string{} } node.Values[name] = p.lit @@ -128,61 +142,132 @@ func (p *parser2) parseCommand(cCfg *CommandConfig) Node { identIndex++ case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: tok := p.tok - flagNode := p.parseFlag() - tracef("parseCommand appending %s node=%+#v", tok, flagNode) + flagNode := p.parseFlag(cCfg.Flags) + + tracef("parseCommand(...) appending %s node=%+#v", tok, flagNode) - node.Nodes = append(node.Nodes, flagNode) + nodes = append(nodes, flagNode) default: - tracef("parseCommand breaking on %s", p.tok) + tracef("parseCommand(...) breaking on %s", p.tok) break } } - tracef("parseCommand returning node=%+#v", node) + if len(nodes) > 0 { + node.Nodes = nodes + } + + if len(values) > 0 { + node.Values = values + } + + tracef("parseCommand(...) returning node=%+#v", node) return node } func (p *parser2) parseIdent() Node { - defer p.next() - node := &Ident{Literal: p.lit} return node } -func (p *parser2) parseFlag() Node { - defer p.next() - +func (p *parser2) parseFlag(flCfgMap map[string]FlagConfig) Node { switch p.tok { case SHORT_FLAG: - return p.parseShortFlag() + return p.parseShortFlag(flCfgMap) case LONG_FLAG: - return p.parseLongFlag() + return p.parseLongFlag(flCfgMap) case COMPOUND_SHORT_FLAG: - return p.parseCompoundShortFlag() + return p.parseCompoundShortFlag(flCfgMap) } panic(fmt.Sprintf("token %v cannot be parsed as flag", p.tok)) } -func (p *parser2) parseShortFlag() Node { - node := &Flag{Name: string(p.lit[1])} - // TODO: moar stuff +func (p *parser2) parseShortFlag(flCfgMap map[string]FlagConfig) Node { + name := string(p.lit[1]) + node := &Flag{Name: name} + tracef("parseShortFlag(...) TODO capture flag value(s)") return node } -func (p *parser2) parseLongFlag() Node { +func (p *parser2) parseLongFlag(flCfgMap map[string]FlagConfig) Node { node := &Flag{Name: string(p.lit[2:])} - // TODO: moar stuff + values := map[string]string{} + nodes := []Node{} + + flCfg, ok := flCfgMap[node.Name] + if !ok { + return node + } + + identIndex := 0 + + for i := 0; p.tok != EOL; i++ { + if !flCfg.NValue.Contains(identIndex) { + tracef("parseLongFlag(...) identIndex=%d exceeds expected=%s; breaking") + break + } + + p.next() + + switch p.tok { + case ARG_DELIMITER: + nodes = append(nodes, &ArgDelimiter{}) + + continue + case IDENT, STDIN_FLAG: + name := fmt.Sprintf("%d", identIndex) + + tracef("parseLongFlag(...) checking for name of identIndex=%d", identIndex) + + if len(flCfg.ValueNames) > identIndex { + name = flCfg.ValueNames[identIndex] + tracef("parseLongFlag(...) setting name=%s from config value names", name) + } else if len(flCfg.ValueNames) == 1 && (flCfg.NValue == OneOrMoreValue || flCfg.NValue == ZeroOrMoreValue) { + name = fmt.Sprintf("%s.%d", flCfg.ValueNames[0], identIndex) + tracef("parseLongFlag(...) setting name=%s from repeating value name", name) + } + + values[name] = p.lit + + identIndex++ + default: + tracef("parseLongFlag(...) breaking on %s %q %v; setting buffered=true", p.tok, p.lit, p.pos) + p.buffered = true + + if len(nodes) > 0 { + node.Nodes = nodes + } + + if len(values) > 0 { + node.Values = values + } + + return node + } + } + + if len(nodes) > 0 { + node.Nodes = nodes + } + + if len(values) > 0 { + node.Values = values + } + return node } -func (p *parser2) parseCompoundShortFlag() Node { +func (p *parser2) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { flagNodes := []Node{} withoutFlagPrefix := p.lit[1:] - for _, r := range withoutFlagPrefix { + for i, r := range withoutFlagPrefix { + if i == len(withoutFlagPrefix)-1 { + tracef("parseCompoundShortFlag(...) TODO capture flag value(s)") + } flagNodes = append(flagNodes, &Flag{Name: string(r)}) } diff --git a/argh/parser2_test.go b/argh/parser2_test.go index 4549e94..83742a2 100644 --- a/argh/parser2_test.go +++ b/argh/parser2_test.go @@ -35,8 +35,7 @@ func TestParser2(t *testing.T) { }, expPT: []argh.Node{ &argh.Command{ - Name: "pies", - Values: map[string]string{}, + Name: "pies", Nodes: []argh.Node{ &argh.ArgDelimiter{}, &argh.CompoundShortFlag{ @@ -46,7 +45,9 @@ func TestParser2(t *testing.T) { &argh.Flag{Name: "t"}, }, }, + &argh.ArgDelimiter{}, &argh.Flag{Name: "wat"}, + &argh.ArgDelimiter{}, &argh.Command{ Name: "hello", Values: map[string]string{ @@ -61,8 +62,7 @@ func TestParser2(t *testing.T) { }, expAST: []argh.Node{ &argh.Command{ - Name: "pies", - Values: map[string]string{}, + Name: "pies", Nodes: []argh.Node{ &argh.Flag{Name: "e"}, &argh.Flag{Name: "a"}, @@ -73,7 +73,6 @@ func TestParser2(t *testing.T) { Values: map[string]string{ "name": "mario", }, - Nodes: []argh.Node{}, }, }, }, @@ -84,37 +83,38 @@ func TestParser2(t *testing.T) { args: []string{"pizzas"}, expPT: []argh.Node{ &argh.Command{ - Name: "pizzas", - Values: map[string]string{}, - Nodes: []argh.Node{}, + Name: "pizzas", }, }, expAST: []argh.Node{ &argh.Command{ - Name: "pizzas", - Values: map[string]string{}, - Nodes: []argh.Node{}, + Name: "pizzas", }, }, }, { - skip: true, - name: "one positional arg", args: []string{"pizzas", "excel"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{NValue: 1}, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + &argh.Command{ + Name: "pizzas", + Values: map[string]string{"0": "excel"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + }, + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + &argh.Command{ + Name: "pizzas", + Values: map[string]string{"0": "excel"}, + }, }, }, { - skip: true, - name: "many positional args", args: []string{"pizzas", "excel", "wildly", "when", "feral"}, cfg: &argh.ParserConfig{ @@ -124,7 +124,7 @@ func TestParser2(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{ + &argh.Command{ Name: "pizzas", Values: map[string]string{ "word": "excel", @@ -132,10 +132,16 @@ func TestParser2(t *testing.T) { "word.2": "when", "word.3": "feral", }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + }, }, }, expAST: []argh.Node{ - argh.Command{ + &argh.Command{ Name: "pizzas", Values: map[string]string{ "word": "excel", @@ -147,29 +153,33 @@ func TestParser2(t *testing.T) { }, }, { - skip: true, - name: "long value-less flags", args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "tasty"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "fresh"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "super-hot-right-now"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &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.Command{Name: "pizzas"}, - argh.Flag{Name: "tasty"}, - argh.Flag{Name: "fresh"}, - argh.Flag{Name: "super-hot-right-now"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "tasty"}, + &argh.Flag{Name: "fresh"}, + &argh.Flag{Name: "super-hot-right-now"}, + }, + }, }, }, { - skip: true, - name: "long flags mixed", args: []string{ "pizzas", @@ -189,25 +199,47 @@ func TestParser2(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "tasty"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, - argh.ArgDelimiter{}, - argh.Flag{Name: "super-hot-right-now"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, - argh.ArgDelimiter{}, - argh.Flag{Name: "please"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "tasty"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "fresh", + Values: map[string]string{"0": "soon"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + }, + }, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "super-hot-right-now"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "box", + Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + }, + }, + &argh.Flag{Name: "please"}, + }, + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.Flag{Name: "tasty"}, - argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, - argh.Flag{Name: "super-hot-right-now"}, - argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, - argh.Flag{Name: "please"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "tasty"}, + &argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, + &argh.Flag{Name: "super-hot-right-now"}, + &argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, + &argh.Flag{Name: "please"}, + }, + }, }, }, { diff --git a/argh/querier.go b/argh/querier.go index f2f6c8a..154914b 100644 --- a/argh/querier.go +++ b/argh/querier.go @@ -37,18 +37,44 @@ func (dq *defaultQuerier) AST() []Node { } if v, ok := node.(*CompoundShortFlag); ok { - ret = append(ret, NewQuerier(v.Nodes).AST()...) + if v.Nodes != nil { + ret = append(ret, NewQuerier(v.Nodes).AST()...) + } continue } if v, ok := node.(*Command); ok { + astNodes := NewQuerier(v.Nodes).AST() + + if len(astNodes) == 0 { + astNodes = nil + } + ret = append( ret, &Command{ Name: v.Name, Values: v.Values, - Nodes: NewQuerier(v.Nodes).AST(), + Nodes: astNodes, + }) + + continue + } + + if v, ok := node.(*Flag); ok { + astNodes := NewQuerier(v.Nodes).AST() + + if len(astNodes) == 0 { + astNodes = nil + } + + ret = append( + ret, + &Flag{ + Name: v.Name, + Values: v.Values, + Nodes: astNodes, }) continue From 3ea30a997a0c9403e7aa46046c891fff7ced3bd3 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 May 2022 22:24:12 -0400 Subject: [PATCH 13/23] Implementing value capture for short flags and ensuring all unknown ident/stdin nodes are retained --- argh/parser2.go | 66 ++++++++------- argh/parser2_test.go | 189 +++++++++++++++++++++++++++---------------- 2 files changed, 159 insertions(+), 96 deletions(-) diff --git a/argh/parser2.go b/argh/parser2.go index 9aab14d..8d35ffa 100644 --- a/argh/parser2.go +++ b/argh/parser2.go @@ -116,29 +116,32 @@ func (p *parser2) parseCommand(cCfg *CommandConfig) Node { case IDENT, STDIN_FLAG: tracef("parseCommand(...) handling %s", p.tok) - if !cCfg.NValue.Contains(identIndex) { - tracef("parseCommand(...) identIndex=%d exceeds expected=%s; breaking", identIndex, cCfg.NValue) - break + if cCfg.NValue.Contains(identIndex) { + name := fmt.Sprintf("%d", identIndex) + + tracef("parseCommand(...) checking for name of identIndex=%d", identIndex) + + if len(cCfg.ValueNames) > identIndex { + name = cCfg.ValueNames[identIndex] + tracef("parseCommand(...) setting name=%s from config value names", name) + } else if len(cCfg.ValueNames) == 1 && (cCfg.NValue == OneOrMoreValue || cCfg.NValue == ZeroOrMoreValue) { + name = fmt.Sprintf("%s.%d", cCfg.ValueNames[0], identIndex) + tracef("parseCommand(...) setting name=%s from repeating value name", name) + } + + if node.Values == nil { + node.Values = map[string]string{} + } + + node.Values[name] = p.lit + } else { + if p.tok == STDIN_FLAG { + nodes = append(nodes, &StdinFlag{}) + } else { + nodes = append(nodes, &Ident{Literal: p.lit}) + } } - name := fmt.Sprintf("%d", identIndex) - - tracef("parseCommand(...) checking for name of identIndex=%d", identIndex) - - if len(cCfg.ValueNames) > identIndex { - name = cCfg.ValueNames[identIndex] - tracef("parseCommand(...) setting name=%s from config value names", name) - } else if len(cCfg.ValueNames) == 1 && (cCfg.NValue == OneOrMoreValue || cCfg.NValue == ZeroOrMoreValue) { - name = fmt.Sprintf("%s.%d", cCfg.ValueNames[0], identIndex) - tracef("parseCommand(...) setting name=%s from repeating value name", name) - } - - if node.Values == nil { - node.Values = map[string]string{} - } - - node.Values[name] = p.lit - identIndex++ case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: tok := p.tok @@ -185,22 +188,31 @@ func (p *parser2) parseFlag(flCfgMap map[string]FlagConfig) Node { } func (p *parser2) parseShortFlag(flCfgMap map[string]FlagConfig) Node { - name := string(p.lit[1]) - node := &Flag{Name: name} - tracef("parseShortFlag(...) TODO capture flag value(s)") - return node + node := &Flag{Name: string(p.lit[1])} + + flCfg, ok := flCfgMap[node.Name] + if !ok { + return node + } + + return p.parseConfiguredFlag(node, flCfg) } func (p *parser2) parseLongFlag(flCfgMap map[string]FlagConfig) Node { node := &Flag{Name: string(p.lit[2:])} - values := map[string]string{} - nodes := []Node{} flCfg, ok := flCfgMap[node.Name] if !ok { return node } + return p.parseConfiguredFlag(node, flCfg) +} + +func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { + values := map[string]string{} + nodes := []Node{} + identIndex := 0 for i := 0; p.tok != EOL; i++ { diff --git a/argh/parser2_test.go b/argh/parser2_test.go index 83742a2..cfa0837 100644 --- a/argh/parser2_test.go +++ b/argh/parser2_test.go @@ -243,65 +243,75 @@ func TestParser2(t *testing.T) { }, }, { - skip: true, - name: "short value-less flags", args: []string{"pizzas", "-t", "-f", "-s"}, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "t"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "f"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "s"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "t"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "f"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "s"}, + }, + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.Flag{Name: "t"}, - argh.Flag{Name: "f"}, - argh.Flag{Name: "s"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "t"}, + &argh.Flag{Name: "f"}, + &argh.Flag{Name: "s"}, + }, + }, }, }, { - skip: true, - name: "compound short flags", args: []string{"pizzas", "-aca", "-blol"}, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "a"}, - argh.Flag{Name: "c"}, - argh.Flag{Name: "a"}, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "c"}, + &argh.Flag{Name: "a"}, + }, + }, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "b"}, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "o"}, + &argh.Flag{Name: "l"}, + }, + }, }, }, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "b"}, - argh.Flag{Name: "l"}, - argh.Flag{Name: "o"}, - argh.Flag{Name: "l"}, + &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"}, }, }, }, - expAST: []argh.Node{ - argh.Command{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: "mixed long short value flags", args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, cfg: &argh.ParserConfig{ @@ -313,52 +323,93 @@ func TestParser2(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "a"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "ca"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "l"}, - argh.Flag{Name: "o"}, - argh.Flag{Name: "l"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "a"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "ca"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "b", + Values: map[string]string{"0": "1312"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + }, + }, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "o"}, + &argh.Flag{Name: "l"}, + }, + }, }, }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.Flag{Name: "a"}, - argh.Flag{Name: "ca"}, - argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, - argh.Flag{Name: "l"}, - argh.Flag{Name: "o"}, - argh.Flag{Name: "l"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "ca"}, + &argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "o"}, + &argh.Flag{Name: "l"}, + }, + }, }, }, { - skip: true, - - name: "commands", - args: []string{"pizzas", "fly", "fry"}, + name: "nested commands with positional args", + args: []string{"pizzas", "fly", "freely", "sometimes", "and", "other", "times", "fry", "deeply", "--forever"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{}, - "fry": argh.CommandConfig{}, + "fly": argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "fry": argh.CommandConfig{}, + }, + }, }, Flags: map[string]argh.FlagConfig{}, }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fly"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fry"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "freely"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "sometimes"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "and"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "other"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "times"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "deeply"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "forever"}, + }, + }, + }, + }, + }, + }, }, }, { From a6526af6ff1b65e5855b4ddd6b10000a68b737c5 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 May 2022 22:34:32 -0400 Subject: [PATCH 14/23] Implement value capture for last compound short flag --- argh/parser2.go | 38 +++++++++++++++---------- argh/parser2_test.go | 66 ++++++++++++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/argh/parser2.go b/argh/parser2.go index 8d35ffa..0a12726 100644 --- a/argh/parser2.go +++ b/argh/parser2.go @@ -209,6 +209,29 @@ func (p *parser2) parseLongFlag(flCfgMap map[string]FlagConfig) Node { return p.parseConfiguredFlag(node, flCfg) } +func (p *parser2) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { + flagNodes := []Node{} + + withoutFlagPrefix := p.lit[1:] + + for i, r := range withoutFlagPrefix { + node := &Flag{Name: string(r)} + + if i == len(withoutFlagPrefix)-1 { + flCfg, ok := flCfgMap[node.Name] + if ok { + flagNodes = append(flagNodes, p.parseConfiguredFlag(node, flCfg)) + + continue + } + } + + flagNodes = append(flagNodes, node) + } + + return &CompoundShortFlag{Nodes: flagNodes} +} + func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { values := map[string]string{} nodes := []Node{} @@ -271,21 +294,6 @@ func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { return node } -func (p *parser2) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { - flagNodes := []Node{} - - withoutFlagPrefix := p.lit[1:] - - for i, r := range withoutFlagPrefix { - if i == len(withoutFlagPrefix)-1 { - tracef("parseCompoundShortFlag(...) TODO capture flag value(s)") - } - flagNodes = append(flagNodes, &Flag{Name: string(r)}) - } - - return &CompoundShortFlag{Nodes: flagNodes} -} - func (p *parser2) parsePassthrough() Node { nodes := []Node{} diff --git a/argh/parser2_test.go b/argh/parser2_test.go index cfa0837..f9e1370 100644 --- a/argh/parser2_test.go +++ b/argh/parser2_test.go @@ -413,10 +413,8 @@ func TestParser2(t *testing.T) { }, }, { - skip: true, - name: "command specific flags", - args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt"}, + args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt", "hugs"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{ @@ -424,13 +422,15 @@ func TestParser2(t *testing.T) { Flags: map[string]argh.FlagConfig{ "freely": {}, }, - }, - "fry": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "deeply": {}, - "w": {}, - "A": {}, - "t": {}, + Commands: map[string]argh.CommandConfig{ + "fry": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "deeply": {}, + "w": {}, + "A": {}, + "t": argh.FlagConfig{NValue: 1}, + }, + }, }, }, }, @@ -438,21 +438,39 @@ func TestParser2(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fly"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "freely"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fry"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "deeply"}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "w"}, - argh.Flag{Name: "A"}, - argh.Flag{Name: "t"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "freely"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "deeply"}, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "w"}, + &argh.Flag{Name: "A"}, + &argh.Flag{ + Name: "t", + Values: map[string]string{"0": "hugs"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + }, + }, + }, + }, + }, + }, + }, + }, }, }, }, From 95453bf197ec6b354ef2fa3c4e54aab0c4a05152 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 May 2022 08:11:31 -0400 Subject: [PATCH 15/23] Retain literals that are values --- argh/parser2.go | 20 ++-- argh/parser2_test.go | 249 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 240 insertions(+), 29 deletions(-) diff --git a/argh/parser2.go b/argh/parser2.go index 0a12726..1eed50a 100644 --- a/argh/parser2.go +++ b/argh/parser2.go @@ -129,17 +129,13 @@ func (p *parser2) parseCommand(cCfg *CommandConfig) Node { tracef("parseCommand(...) setting name=%s from repeating value name", name) } - if node.Values == nil { - node.Values = map[string]string{} - } + values[name] = p.lit + } - node.Values[name] = p.lit + if p.tok == STDIN_FLAG { + nodes = append(nodes, &StdinFlag{}) } else { - if p.tok == STDIN_FLAG { - nodes = append(nodes, &StdinFlag{}) - } else { - nodes = append(nodes, &Ident{Literal: p.lit}) - } + nodes = append(nodes, &Ident{Literal: p.lit}) } identIndex++ @@ -266,6 +262,12 @@ func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { values[name] = p.lit + if p.tok == STDIN_FLAG { + nodes = append(nodes, &StdinFlag{}) + } else { + nodes = append(nodes, &Ident{Literal: p.lit}) + } + identIndex++ default: tracef("parseLongFlag(...) breaking on %s %q %v; setting buffered=true", p.tok, p.lit, p.pos) diff --git a/argh/parser2_test.go b/argh/parser2_test.go index f9e1370..a355e2d 100644 --- a/argh/parser2_test.go +++ b/argh/parser2_test.go @@ -55,6 +55,7 @@ func TestParser2(t *testing.T) { }, Nodes: []argh.Node{ &argh.ArgDelimiter{}, + &argh.Ident{Literal: "mario"}, }, }, }, @@ -73,6 +74,9 @@ func TestParser2(t *testing.T) { Values: map[string]string{ "name": "mario", }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "mario"}, + }, }, }, }, @@ -104,6 +108,7 @@ func TestParser2(t *testing.T) { Values: map[string]string{"0": "excel"}, Nodes: []argh.Node{ &argh.ArgDelimiter{}, + &argh.Ident{Literal: "excel"}, }, }, }, @@ -111,6 +116,9 @@ func TestParser2(t *testing.T) { &argh.Command{ Name: "pizzas", Values: map[string]string{"0": "excel"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "excel"}, + }, }, }, }, @@ -134,9 +142,13 @@ func TestParser2(t *testing.T) { }, Nodes: []argh.Node{ &argh.ArgDelimiter{}, + &argh.Ident{Literal: "excel"}, &argh.ArgDelimiter{}, + &argh.Ident{Literal: "wildly"}, &argh.ArgDelimiter{}, + &argh.Ident{Literal: "when"}, &argh.ArgDelimiter{}, + &argh.Ident{Literal: "feral"}, }, }, }, @@ -149,6 +161,12 @@ func TestParser2(t *testing.T) { "word.2": "when", "word.3": "feral", }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "excel"}, + &argh.Ident{Literal: "wildly"}, + &argh.Ident{Literal: "when"}, + &argh.Ident{Literal: "feral"}, + }, }, }, }, @@ -210,6 +228,7 @@ func TestParser2(t *testing.T) { Values: map[string]string{"0": "soon"}, Nodes: []argh.Node{ &argh.ArgDelimiter{}, + &argh.Ident{Literal: "soon"}, }, }, &argh.ArgDelimiter{}, @@ -220,8 +239,11 @@ func TestParser2(t *testing.T) { Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}, Nodes: []argh.Node{ &argh.ArgDelimiter{}, + &argh.Ident{Literal: "square"}, &argh.ArgDelimiter{}, + &argh.Ident{Literal: "shaped"}, &argh.ArgDelimiter{}, + &argh.Ident{Literal: "hot"}, &argh.ArgDelimiter{}, }, }, @@ -234,9 +256,23 @@ func TestParser2(t *testing.T) { Name: "pizzas", Nodes: []argh.Node{ &argh.Flag{Name: "tasty"}, - &argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, + &argh.Flag{ + Name: "fresh", + Values: map[string]string{"0": "soon"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "soon"}, + }, + }, &argh.Flag{Name: "super-hot-right-now"}, - &argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, + &argh.Flag{ + Name: "box", + Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "square"}, + &argh.Ident{Literal: "shaped"}, + &argh.Ident{Literal: "hot"}, + }, + }, &argh.Flag{Name: "please"}, }, }, @@ -336,6 +372,7 @@ func TestParser2(t *testing.T) { Values: map[string]string{"0": "1312"}, Nodes: []argh.Node{ &argh.ArgDelimiter{}, + &argh.Ident{Literal: "1312"}, }, }, &argh.ArgDelimiter{}, @@ -355,7 +392,13 @@ func TestParser2(t *testing.T) { Nodes: []argh.Node{ &argh.Flag{Name: "a"}, &argh.Flag{Name: "ca"}, - &argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, + &argh.Flag{ + Name: "b", + Values: map[string]string{"0": "1312"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "1312"}, + }, + }, &argh.Flag{Name: "l"}, &argh.Flag{Name: "o"}, &argh.Flag{Name: "l"}, @@ -411,6 +454,131 @@ func TestParser2(t *testing.T) { }, }, }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.Ident{Literal: "freely"}, + &argh.Ident{Literal: "sometimes"}, + &argh.Ident{Literal: "and"}, + &argh.Ident{Literal: "other"}, + &argh.Ident{Literal: "times"}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.Ident{Literal: "deeply"}, + &argh.Flag{Name: "forever"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "compound flags with values", + args: []string{"pizzas", "-need", "sauce", "heat", "love", "-also", "over9000"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "a": {NValue: argh.ZeroOrMoreValue}, + "d": {NValue: argh.OneOrMoreValue}, + "e": {}, + "l": {}, + "n": {}, + "o": {NValue: 1, ValueNames: []string{"level"}}, + "s": {NValue: argh.ZeroOrMoreValue}, + }, + }, + }, + expPT: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "n"}, + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "e"}, + &argh.Flag{ + Name: "d", + Values: map[string]string{ + "0": "sauce", + "1": "heat", + "2": "love", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "sauce"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "heat"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "love"}, + &argh.ArgDelimiter{}, + }, + }, + }, + }, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "s"}, + &argh.Flag{ + Name: "o", + Values: map[string]string{ + "level": "over9000", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "over9000"}, + }, + }, + }, + }, + }, + }, + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "n"}, + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "e"}, + &argh.Flag{ + Name: "d", + Values: map[string]string{ + "0": "sauce", + "1": "heat", + "2": "love", + }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "sauce"}, + &argh.Ident{Literal: "heat"}, + &argh.Ident{Literal: "love"}, + }, + }, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "s"}, + &argh.Flag{ + Name: "o", + Values: map[string]string{ + "level": "over9000", + }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "over9000"}, + }, + }, + }, + }, + }, }, { name: "command specific flags", @@ -463,6 +631,7 @@ func TestParser2(t *testing.T) { Values: map[string]string{"0": "hugs"}, Nodes: []argh.Node{ &argh.ArgDelimiter{}, + &argh.Ident{Literal: "hugs"}, }, }, }, @@ -474,6 +643,34 @@ func TestParser2(t *testing.T) { }, }, }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.Flag{Name: "freely"}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.Flag{Name: "deeply"}, + &argh.Flag{Name: "w"}, + &argh.Flag{Name: "A"}, + &argh.Flag{ + Name: "t", + Values: map[string]string{"0": "hugs"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "hugs"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, { skip: true, @@ -500,30 +697,42 @@ func TestParser2(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "PIZZAs"}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "PIZZAs", Nodes: []argh.Node{ - argh.Flag{Name: "w"}, - argh.Flag{Name: "A"}, - argh.Flag{Name: "T", Values: map[string]string{"0": "golf"}}, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "w"}, + &argh.Flag{Name: "A"}, + &argh.Flag{Name: "T", Values: map[string]string{"0": "golf"}}, + }, + }, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "hecKing"}, + &argh.ArgDelimiter{}, + &argh.Command{Name: "goose", Values: map[string]string{"0": "bonk"}}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "FIERCENESS", Values: map[string]string{"0": "-2"}}, }, }, - argh.ArgDelimiter{}, - argh.Flag{Name: "hecKing"}, - argh.ArgDelimiter{}, - argh.Command{Name: "goose", Values: map[string]string{"0": "bonk"}}, - argh.ArgDelimiter{}, - argh.Flag{Name: "FIERCENESS", Values: map[string]string{"0": "-2"}}, }, }, { - skip: true, + skip: true, + name: "invalid bare assignment", args: []string{"pizzas", "=", "--wat"}, expErr: argh.ErrSyntax, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "wat"}, + }, + }, }, }, } { @@ -535,7 +744,7 @@ func TestParser2(t *testing.T) { } pt, err := argh.ParseArgs2(tc.args, tc.cfg) - if err != nil { + if err != nil || tc.expErr != nil { assert.ErrorIs(ct, err, tc.expErr) return } @@ -554,8 +763,8 @@ func TestParser2(t *testing.T) { } pt, err := argh.ParseArgs2(tc.args, tc.cfg) - if err != nil { - ct.Logf("err=%+#v", err) + if err != nil || tc.expErr != nil { + assert.ErrorIs(ct, err, tc.expErr) return } From cc29386cee408f7bd62df03ee039602ec91fc1f1 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 May 2022 08:22:07 -0400 Subject: [PATCH 16/23] Use parser2 in querier tests --- argh/parser2.go | 2 ++ argh/querier.go | 15 +++++++++++---- argh/querier_test.go | 17 +++++------------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/argh/parser2.go b/argh/parser2.go index 1eed50a..c57c51c 100644 --- a/argh/parser2.go +++ b/argh/parser2.go @@ -55,6 +55,8 @@ func (p *parser2) parseArgs() (*ParseTree, error) { tracef("parseArgs() parsing %q as program command; cfg=%+#v", p.lit, p.cfg.Prog) prog := p.parseCommand(&p.cfg.Prog) + tracef("parseArgs() top level node is %T", prog) + nodes := []Node{prog} if v := p.parsePassthrough(); v != nil { tracef("parseArgs() appending passthrough argument %v", v) diff --git a/argh/querier.go b/argh/querier.go index 154914b..2d1ca54 100644 --- a/argh/querier.go +++ b/argh/querier.go @@ -1,7 +1,7 @@ package argh type Querier interface { - Program() (Command, bool) + Program() (*Command, bool) AST() []Node } @@ -13,12 +13,19 @@ type defaultQuerier struct { nodes []Node } -func (dq *defaultQuerier) Program() (Command, bool) { +func (dq *defaultQuerier) Program() (*Command, bool) { if len(dq.nodes) == 0 { - return Command{}, false + tracef("Program nodes are empty") + return nil, false + } + + tracef("Program node[0] is %T", dq.nodes[0]) + + v, ok := dq.nodes[0].(*Command) + if ok && v.Name == "" { + return v, false } - v, ok := dq.nodes[0].(Command) return v, ok } diff --git a/argh/querier_test.go b/argh/querier_test.go index f335e6e..edd5330 100644 --- a/argh/querier_test.go +++ b/argh/querier_test.go @@ -12,41 +12,34 @@ func TestQuerier_Program(t *testing.T) { name string args []string cfg *argh.ParserConfig - exp argh.Command + exp string expOK bool }{ { name: "typical", args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, - exp: argh.Command{Name: "pizzas"}, + exp: "pizzas", expOK: true, }, { name: "minimal", args: []string{"pizzas"}, - exp: argh.Command{Name: "pizzas"}, + exp: "pizzas", expOK: true, }, { name: "invalid", args: []string{}, - exp: argh.Command{}, - expOK: false, - }, - { - name: "invalid flag only", - args: []string{"--oh-no"}, - exp: argh.Command{}, expOK: false, }, } { t.Run(tc.name, func(ct *testing.T) { - pt, err := argh.ParseArgs(tc.args, tc.cfg) + pt, err := argh.ParseArgs2(tc.args, tc.cfg) require.Nil(ct, err) prog, ok := argh.NewQuerier(pt.Nodes).Program() - require.Equal(ct, tc.exp, prog) require.Equal(ct, tc.expOK, ok) + require.Equal(ct, tc.exp, prog.Name) }) } } From 56c6a8cf09ba33e1917e7d4248b6a11866a76639 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 29 May 2022 19:04:31 -0400 Subject: [PATCH 17/23] Handle remaining skipped cases --- argh/argh.go | 2 +- argh/node.go | 2 ++ argh/parser2.go | 27 ++++++++++++++---- argh/parser2_test.go | 66 ++++++++++++++++++++++++++++++------------- argh/scanner_error.go | 13 +++++++++ 5 files changed, 84 insertions(+), 26 deletions(-) diff --git a/argh/argh.go b/argh/argh.go index f1cc2f9..47f3ca1 100644 --- a/argh/argh.go +++ b/argh/argh.go @@ -26,7 +26,7 @@ func tracef(format string, v ...any) { return } - if _, file, line, ok := runtime.Caller(2); ok { + if _, file, line, ok := runtime.Caller(1); ok { format = fmt.Sprintf("%v:%v ", filepath.Base(file), line) + format } diff --git a/argh/node.go b/argh/node.go index a18e7b0..b4ce2e9 100644 --- a/argh/node.go +++ b/argh/node.go @@ -42,3 +42,5 @@ type StdinFlag struct{} type StopFlag struct{} type ArgDelimiter struct{} + +type Assign struct{} diff --git a/argh/parser2.go b/argh/parser2.go index c57c51c..66a0142 100644 --- a/argh/parser2.go +++ b/argh/parser2.go @@ -65,7 +65,7 @@ func (p *parser2) parseArgs() (*ParseTree, error) { tracef("parseArgs() returning ParseTree") - return &ParseTree{Nodes: nodes}, nil + return &ParseTree{Nodes: nodes}, p.errors.Err() } func (p *parser2) next() { @@ -149,6 +149,12 @@ func (p *parser2) parseCommand(cCfg *CommandConfig) Node { tracef("parseCommand(...) appending %s node=%+#v", tok, flagNode) nodes = append(nodes, flagNode) + case ASSIGN: + tracef("parseCommand(...) error on bare %s", p.tok) + + p.errors.Add(Position{Column: int(p.pos)}, "invalid bare assignment") + + break default: tracef("parseCommand(...) breaking on %s", p.tok) break @@ -175,10 +181,13 @@ func (p *parser2) parseIdent() Node { func (p *parser2) parseFlag(flCfgMap map[string]FlagConfig) Node { switch p.tok { case SHORT_FLAG: + tracef("parseFlag(...) parsing short flag with config=%+#v", flCfgMap) return p.parseShortFlag(flCfgMap) case LONG_FLAG: + tracef("parseFlag(...) parsing long flag with config=%+#v", flCfgMap) return p.parseLongFlag(flCfgMap) case COMPOUND_SHORT_FLAG: + tracef("parseFlag(...) parsing compound short flag with config=%+#v", flCfgMap) return p.parseCompoundShortFlag(flCfgMap) } @@ -238,7 +247,7 @@ func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { for i := 0; p.tok != EOL; i++ { if !flCfg.NValue.Contains(identIndex) { - tracef("parseLongFlag(...) identIndex=%d exceeds expected=%s; breaking") + tracef("parseConfiguredFlag(...) identIndex=%d exceeds expected=%v; breaking", identIndex, flCfg.NValue) break } @@ -248,18 +257,24 @@ func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { case ARG_DELIMITER: nodes = append(nodes, &ArgDelimiter{}) + continue + case ASSIGN: + nodes = append(nodes, &Assign{}) + continue case IDENT, STDIN_FLAG: name := fmt.Sprintf("%d", identIndex) - tracef("parseLongFlag(...) checking for name of identIndex=%d", identIndex) + tracef("parseConfiguredFlag(...) checking for name of identIndex=%d", identIndex) if len(flCfg.ValueNames) > identIndex { name = flCfg.ValueNames[identIndex] - tracef("parseLongFlag(...) setting name=%s from config value names", name) + tracef("parseConfiguredFlag(...) setting name=%s from config value names", name) } else if len(flCfg.ValueNames) == 1 && (flCfg.NValue == OneOrMoreValue || flCfg.NValue == ZeroOrMoreValue) { name = fmt.Sprintf("%s.%d", flCfg.ValueNames[0], identIndex) - tracef("parseLongFlag(...) setting name=%s from repeating value name", name) + tracef("parseConfiguredFlag(...) setting name=%s from repeating value name", name) + } else { + tracef("parseConfiguredFlag(...) setting name=%s", name) } values[name] = p.lit @@ -272,7 +287,7 @@ func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { identIndex++ default: - tracef("parseLongFlag(...) breaking on %s %q %v; setting buffered=true", p.tok, p.lit, p.pos) + tracef("parseConfiguredFlag(...) breaking on %s %q %v; setting buffered=true", p.tok, p.lit, p.pos) p.buffered = true if len(nodes) > 0 { diff --git a/argh/parser2_test.go b/argh/parser2_test.go index a355e2d..5265e09 100644 --- a/argh/parser2_test.go +++ b/argh/parser2_test.go @@ -673,21 +673,23 @@ func TestParser2(t *testing.T) { }, }, { - skip: true, - name: "total weirdo", args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{ - "goose": argh.CommandConfig{NValue: 1}, + "goose": argh.CommandConfig{ + NValue: 1, + Flags: map[string]argh.FlagConfig{ + "FIERCENESS": argh.FlagConfig{NValue: 1}, + }, + }, }, Flags: map[string]argh.FlagConfig{ - "w": argh.FlagConfig{}, - "A": argh.FlagConfig{}, - "T": argh.FlagConfig{NValue: 1}, - "hecking": argh.FlagConfig{}, - "FIERCENESS": argh.FlagConfig{NValue: 1}, + "w": argh.FlagConfig{}, + "A": argh.FlagConfig{}, + "T": argh.FlagConfig{NValue: 1}, + "hecking": argh.FlagConfig{}, }, }, ScannerConfig: &argh.ScannerConfig{ @@ -705,25 +707,46 @@ func TestParser2(t *testing.T) { Nodes: []argh.Node{ &argh.Flag{Name: "w"}, &argh.Flag{Name: "A"}, - &argh.Flag{Name: "T", Values: map[string]string{"0": "golf"}}, + &argh.Flag{ + Name: "T", + Values: map[string]string{"0": "golf"}, + Nodes: []argh.Node{ + &argh.Assign{}, + &argh.Ident{Literal: "golf"}, + }, + }, }, }, &argh.ArgDelimiter{}, &argh.Flag{Name: "hecKing"}, &argh.ArgDelimiter{}, - &argh.Command{Name: "goose", Values: map[string]string{"0": "bonk"}}, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "FIERCENESS", Values: map[string]string{"0": "-2"}}, + &argh.Command{ + Name: "goose", + Values: map[string]string{"0": "bonk"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "bonk"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "FIERCENESS", + Values: map[string]string{"0": "-2"}, + Nodes: []argh.Node{ + &argh.Assign{}, + &argh.Ident{Literal: "-2"}, + }, + }, + }, + }, }, }, }, }, { - skip: true, - - name: "invalid bare assignment", - args: []string{"pizzas", "=", "--wat"}, - expErr: argh.ErrSyntax, + name: "invalid bare assignment", + args: []string{"pizzas", "=", "--wat"}, + expErr: argh.ScannerErrorList{ + &argh.ScannerError{Pos: argh.Position{Column: 8}, Msg: "invalid bare assignment"}, + }, expPT: []argh.Node{ &argh.Command{ Name: "pizzas", @@ -745,7 +768,10 @@ func TestParser2(t *testing.T) { pt, err := argh.ParseArgs2(tc.args, tc.cfg) if err != nil || tc.expErr != nil { - assert.ErrorIs(ct, err, tc.expErr) + if !assert.ErrorIs(ct, err, tc.expErr) { + spew.Dump(err, tc.expErr) + spew.Dump(pt) + } return } @@ -764,7 +790,9 @@ func TestParser2(t *testing.T) { pt, err := argh.ParseArgs2(tc.args, tc.cfg) if err != nil || tc.expErr != nil { - assert.ErrorIs(ct, err, tc.expErr) + if !assert.ErrorIs(ct, err, tc.expErr) { + spew.Dump(pt) + } return } diff --git a/argh/scanner_error.go b/argh/scanner_error.go index c9b7441..e5b634b 100644 --- a/argh/scanner_error.go +++ b/argh/scanner_error.go @@ -16,6 +16,7 @@ func (e ScannerError) Error() string { if e.Pos.IsValid() { return e.Pos.String() + ":" + e.Msg } + return e.Msg } @@ -64,6 +65,18 @@ func (el ScannerErrorList) Err() error { return el } +func (el ScannerErrorList) Is(other error) bool { + if _, ok := other.(ScannerErrorList); ok { + return el.Error() == other.Error() + } + + if v, ok := other.(*ScannerErrorList); ok { + return el.Error() == (*v).Error() + } + + return false +} + func PrintScannerError(w io.Writer, err error) { if list, ok := err.(ScannerErrorList); ok { for _, e := range list { From 1452d544bbcf71529b8aaab7247f7591023a2c17 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 29 May 2022 19:10:16 -0400 Subject: [PATCH 18/23] Dropping previous parser attempt --- argh/cmd/argh/main.go | 2 +- argh/parser.go | 444 +++++++++++------------ argh/parser2.go | 328 ----------------- argh/parser2_test.go | 807 ------------------------------------------ argh/parser_test.go | 781 +++++++++++++++++++++++++++++++--------- 5 files changed, 842 insertions(+), 1520 deletions(-) delete mode 100644 argh/parser2.go delete mode 100644 argh/parser2_test.go diff --git a/argh/cmd/argh/main.go b/argh/cmd/argh/main.go index bd9b32c..1313da4 100644 --- a/argh/cmd/argh/main.go +++ b/argh/cmd/argh/main.go @@ -12,7 +12,7 @@ import ( func main() { log.SetFlags(0) - ast, err := argh.ParseArgs(os.Args, nil) + ast, err := argh.ParseArgs2(os.Args, nil) if err != nil { log.Fatal(err) } diff --git a/argh/parser.go b/argh/parser.go index f9ec92a..d994efe 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -1,332 +1,332 @@ -//go:generate stringer -type NValue - package argh import ( "fmt" "io" "strings" - - "github.com/pkg/errors" -) - -var ( - ErrSyntax = errors.New("syntax error") ) -func ParseArgs(args []string, pCfg *ParserConfig) (*ParseTree, error) { - reEncoded := strings.Join(args, string(nul)) - - return NewParser( - strings.NewReader(reEncoded), - pCfg, - ).Parse() -} - -type Parser struct { +type parser struct { s *Scanner - buf []scanEntry - cfg *ParserConfig - nodes []Node - node Node + errors ScannerErrorList + + tok Token + lit string + pos Pos + + buffered bool } type ParseTree struct { Nodes []Node `json:"nodes"` } -type scanEntry struct { - tok Token - lit string - pos Pos +func ParseArgs2(args []string, pCfg *ParserConfig) (*ParseTree, error) { + p := &parser{} + p.init( + strings.NewReader(strings.Join(args, string(nul))), + pCfg, + ) + + tracef("ParseArgs2(...) parser=%+#v", p) + + return p.parseArgs() } -func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { +func (p *parser) init(r io.Reader, pCfg *ParserConfig) { + p.errors = ScannerErrorList{} + if pCfg == nil { pCfg = POSIXyParserConfig } - parser := &Parser{ - buf: []scanEntry{}, - s: NewScanner(r, pCfg.ScannerConfig), - cfg: pCfg, - } + p.cfg = pCfg - tracef("NewParser parser=%+#v", parser) - tracef("NewParser pCfg=%+#v", pCfg) + p.s = NewScanner(r, pCfg.ScannerConfig) - return parser + p.next() } -func (p *Parser) Parse() (*ParseTree, error) { - p.nodes = []Node{} - - for { - br, err := p.parseArg() - if err != nil { - return nil, err - } - - if br { - break - } +func (p *parser) parseArgs() (*ParseTree, error) { + if p.errors.Len() != 0 { + tracef("parseArgs() bailing due to initial error") + return nil, p.errors.Err() } - return &ParseTree{Nodes: p.nodes}, nil -} + tracef("parseArgs() parsing %q as program command; cfg=%+#v", p.lit, p.cfg.Prog) + prog := p.parseCommand(&p.cfg.Prog) -func (p *Parser) parseArg() (bool, error) { - tok, lit, pos := p.scan() - if tok == ILLEGAL { - return false, errors.Wrapf(ErrSyntax, "illegal value %q at pos=%v", lit, pos) - } + tracef("parseArgs() top level node is %T", prog) - if tok == EOL { - return true, nil + nodes := []Node{prog} + if v := p.parsePassthrough(); v != nil { + tracef("parseArgs() appending passthrough argument %v", v) + nodes = append(nodes, v) } - p.unscan(tok, lit, pos) + tracef("parseArgs() returning ParseTree") - node, err := p.scanNode() + return &ParseTree{Nodes: nodes}, p.errors.Err() +} - tracef("parseArg node=%+#v err=%+#v", node, err) +func (p *parser) next() { + tracef("next() before scan: %v %q %v", p.tok, p.lit, p.pos) - if err != nil { - return false, errors.Wrapf(err, "value %q at pos=%v", lit, pos) - } + p.tok, p.lit, p.pos = p.s.Scan() - if node != nil { - p.nodes = append(p.nodes, node) - } - - return false, nil + tracef("next() after scan: %v %q %v", p.tok, p.lit, p.pos) } -func (p *Parser) scanNode() (Node, error) { - tok, lit, pos := p.scan() - - tracef("scanNode tok=%s lit=%q pos=%v", tok, lit, pos) +func (p *parser) parseCommand(cCfg *CommandConfig) Node { + tracef("parseCommand(%+#v)", cCfg) - switch tok { - case ARG_DELIMITER: - return ArgDelimiter{}, nil - case ASSIGN: - return nil, errors.Wrapf(ErrSyntax, "bare assignment operator at pos=%v", pos) - case IDENT: - p.unscan(tok, lit, pos) - return p.scanCommandOrIdent() - case COMPOUND_SHORT_FLAG: - p.unscan(tok, lit, pos) - return p.scanCompoundShortFlag() - case SHORT_FLAG, LONG_FLAG: - p.unscan(tok, lit, pos) - return p.scanFlag() - default: + node := &Command{ + Name: p.lit, } + values := map[string]string{} + nodes := []Node{} - return Ident{Literal: lit}, nil -} - -func (p *Parser) scanCommandOrIdent() (Node, error) { - tok, lit, pos := p.scan() + identIndex := 0 - if len(p.nodes) == 0 { - p.unscan(tok, lit, pos) - values, err := p.scanValues(p.cfg.Prog.NValue, p.cfg.Prog.ValueNames) - if err != nil { - return nil, err + for i := 0; p.tok != EOL; i++ { + if !p.buffered { + tracef("parseCommand(...) buffered=false; scanning next") + p.next() } - return Command{Name: lit, Values: values}, nil - } + p.buffered = false - if cfg, ok := p.cfg.Prog.Commands[lit]; ok { - p.unscan(tok, lit, pos) - values, err := p.scanValues(cfg.NValue, cfg.ValueNames) - if err != nil { - return nil, err - } + tracef("parseCommand(...) for=%d values=%+#v", i, values) + tracef("parseCommand(...) for=%d nodes=%+#v", i, nodes) + tracef("parseCommand(...) for=%d tok=%s lit=%q pos=%v", i, p.tok, p.lit, p.pos) - return Command{Name: lit, Values: values}, nil - } + if subCfg, ok := cCfg.Commands[p.lit]; ok { + subCommand := p.lit - return Ident{Literal: lit}, nil -} + nodes = append(nodes, p.parseCommand(&subCfg)) -func (p *Parser) scanFlag() (Node, error) { - tok, lit, pos := p.scan() + tracef("parseCommand(...) breaking after sub-command=%v", subCommand) + break + } - flagName := string(lit[1:]) - if tok == LONG_FLAG { - flagName = string(lit[2:]) - } + switch p.tok { + case ARG_DELIMITER: + tracef("parseCommand(...) handling %s", p.tok) - if cfg, ok := p.cfg.Prog.Flags[flagName]; ok { - p.unscan(tok, flagName, pos) + nodes = append(nodes, &ArgDelimiter{}) - values, err := p.scanValues(cfg.NValue, cfg.ValueNames) - if err != nil { - return nil, err - } + continue + case IDENT, STDIN_FLAG: + tracef("parseCommand(...) handling %s", p.tok) - return Flag{Name: flagName, Values: values}, nil - } + if cCfg.NValue.Contains(identIndex) { + name := fmt.Sprintf("%d", identIndex) - return Flag{Name: flagName}, nil -} + tracef("parseCommand(...) checking for name of identIndex=%d", identIndex) -func (p *Parser) scanCompoundShortFlag() (Node, error) { - tok, lit, pos := p.scan() + if len(cCfg.ValueNames) > identIndex { + name = cCfg.ValueNames[identIndex] + tracef("parseCommand(...) setting name=%s from config value names", name) + } else if len(cCfg.ValueNames) == 1 && (cCfg.NValue == OneOrMoreValue || cCfg.NValue == ZeroOrMoreValue) { + name = fmt.Sprintf("%s.%d", cCfg.ValueNames[0], identIndex) + tracef("parseCommand(...) setting name=%s from repeating value name", name) + } - flagNodes := []Node{} + values[name] = p.lit + } - withoutFlagPrefix := lit[1:] + if p.tok == STDIN_FLAG { + nodes = append(nodes, &StdinFlag{}) + } else { + nodes = append(nodes, &Ident{Literal: p.lit}) + } - for i, r := range withoutFlagPrefix { - if i == len(withoutFlagPrefix)-1 { - flagName := string(r) + identIndex++ + case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: + tok := p.tok - if cfg, ok := p.cfg.Prog.Flags[flagName]; ok { - p.unscan(tok, flagName, pos) + flagNode := p.parseFlag(cCfg.Flags) - values, err := p.scanValues(cfg.NValue, cfg.ValueNames) - if err != nil { - return nil, err - } + tracef("parseCommand(...) appending %s node=%+#v", tok, flagNode) - flagNodes = append(flagNodes, Flag{Name: flagName, Values: values}) + nodes = append(nodes, flagNode) + case ASSIGN: + tracef("parseCommand(...) error on bare %s", p.tok) - continue - } + p.errors.Add(Position{Column: int(p.pos)}, "invalid bare assignment") + + break + default: + tracef("parseCommand(...) breaking on %s", p.tok) + break } + } - flagNodes = append( - flagNodes, - Flag{ - Name: string(r), - }, - ) + if len(nodes) > 0 { + node.Nodes = nodes } - return CompoundShortFlag{Nodes: flagNodes}, nil + if len(values) > 0 { + node.Values = values + } + + tracef("parseCommand(...) returning node=%+#v", node) + return node } -func (p *Parser) scanValuesAndFlags() (map[string]string, []Node, error) { - return nil, nil, nil +func (p *parser) parseIdent() Node { + node := &Ident{Literal: p.lit} + return node } -func (p *Parser) scanValues(n NValue, valueNames []string) (map[string]string, error) { - _, lit, pos := p.scan() +func (p *parser) parseFlag(flCfgMap map[string]FlagConfig) Node { + switch p.tok { + case SHORT_FLAG: + tracef("parseFlag(...) parsing short flag with config=%+#v", flCfgMap) + return p.parseShortFlag(flCfgMap) + case LONG_FLAG: + tracef("parseFlag(...) parsing long flag with config=%+#v", flCfgMap) + return p.parseLongFlag(flCfgMap) + case COMPOUND_SHORT_FLAG: + tracef("parseFlag(...) parsing compound short flag with config=%+#v", flCfgMap) + return p.parseCompoundShortFlag(flCfgMap) + } - tracef("scanValues lit=%q pos=%v n=%v valueNames=%+v", lit, pos, n, valueNames) + panic(fmt.Sprintf("token %v cannot be parsed as flag", p.tok)) +} - values, err := func() (map[string]string, error) { - if n == ZeroValue { - return map[string]string{}, nil - } +func (p *parser) parseShortFlag(flCfgMap map[string]FlagConfig) Node { + node := &Flag{Name: string(p.lit[1])} - ret := map[string]string{} - i := 0 + flCfg, ok := flCfgMap[node.Name] + if !ok { + return node + } - for { - lit, err := p.scanIdent() - if err != nil { - if n == NValue(1) { - return nil, err - } + return p.parseConfiguredFlag(node, flCfg) +} - if n == OneOrMoreValue { - break - } - } +func (p *parser) parseLongFlag(flCfgMap map[string]FlagConfig) Node { + node := &Flag{Name: string(p.lit[2:])} - name := fmt.Sprintf("%d", i) - if len(valueNames)-1 >= i { - name = valueNames[i] - } else if len(valueNames) > 0 && strings.HasSuffix(valueNames[len(valueNames)-1], "+") { - name = strings.TrimSuffix(valueNames[len(valueNames)-1], "+") - } + flCfg, ok := flCfgMap[node.Name] + if !ok { + return node + } - ret[name] = lit + return p.parseConfiguredFlag(node, flCfg) +} - if n == NValue(1) && len(ret) == 1 { - break - } +func (p *parser) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { + flagNodes := []Node{} - i++ - } + withoutFlagPrefix := p.lit[1:] - return ret, nil - }() + for i, r := range withoutFlagPrefix { + node := &Flag{Name: string(r)} - if err != nil { - return nil, err - } + if i == len(withoutFlagPrefix)-1 { + flCfg, ok := flCfgMap[node.Name] + if ok { + flagNodes = append(flagNodes, p.parseConfiguredFlag(node, flCfg)) - if len(values) == 0 { - return nil, nil + continue + } + } + + flagNodes = append(flagNodes, node) } - return values, nil + return &CompoundShortFlag{Nodes: flagNodes} } -func (p *Parser) scanIdent() (string, error) { - tok, lit, pos := p.scan() +func (p *parser) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { + values := map[string]string{} + nodes := []Node{} - tracef("scanIdent scanned tok=%s lit=%q pos=%v", tok, lit, pos) + identIndex := 0 - unscanBuf := []scanEntry{} + for i := 0; p.tok != EOL; i++ { + if !flCfg.NValue.Contains(identIndex) { + tracef("parseConfiguredFlag(...) identIndex=%d exceeds expected=%v; breaking", identIndex, flCfg.NValue) + break + } - if tok == ASSIGN || tok == ARG_DELIMITER { - entry := scanEntry{tok: tok, lit: lit, pos: Pos(pos)} + p.next() - tracef("scanIdent tok=%s; scanning next and pushing to unscan buffer entry=%+#v", tok, entry) + switch p.tok { + case ARG_DELIMITER: + nodes = append(nodes, &ArgDelimiter{}) - unscanBuf = append([]scanEntry{entry}, unscanBuf...) + continue + case ASSIGN: + nodes = append(nodes, &Assign{}) - tok, lit, pos = p.scan() - } + continue + case IDENT, STDIN_FLAG: + name := fmt.Sprintf("%d", identIndex) - if tok == IDENT { - return lit, nil - } + tracef("parseConfiguredFlag(...) checking for name of identIndex=%d", identIndex) - entry := scanEntry{tok: tok, lit: lit, pos: Pos(pos)} + if len(flCfg.ValueNames) > identIndex { + name = flCfg.ValueNames[identIndex] + tracef("parseConfiguredFlag(...) setting name=%s from config value names", name) + } else if len(flCfg.ValueNames) == 1 && (flCfg.NValue == OneOrMoreValue || flCfg.NValue == ZeroOrMoreValue) { + name = fmt.Sprintf("%s.%d", flCfg.ValueNames[0], identIndex) + tracef("parseConfiguredFlag(...) setting name=%s from repeating value name", name) + } else { + tracef("parseConfiguredFlag(...) setting name=%s", name) + } - tracef("scanIdent tok=%s; unscanning entry=%+#v", tok, entry) + values[name] = p.lit - unscanBuf = append([]scanEntry{entry}, unscanBuf...) + if p.tok == STDIN_FLAG { + nodes = append(nodes, &StdinFlag{}) + } else { + nodes = append(nodes, &Ident{Literal: p.lit}) + } - for _, entry := range unscanBuf { - p.unscan(entry.tok, entry.lit, entry.pos) - } + identIndex++ + default: + tracef("parseConfiguredFlag(...) breaking on %s %q %v; setting buffered=true", p.tok, p.lit, p.pos) + p.buffered = true - return "", errors.Wrapf(ErrSyntax, "expected ident at pos=%v but got %s (%q)", pos, tok, lit) -} + if len(nodes) > 0 { + node.Nodes = nodes + } -func (p *Parser) scan() (Token, string, Pos) { - if len(p.buf) != 0 { - entry, buf := p.buf[len(p.buf)-1], p.buf[:len(p.buf)-1] - p.buf = buf + if len(values) > 0 { + node.Values = values + } - tracef("scan returning buffer entry=%s %+#v", entry.tok, entry) - return entry.tok, entry.lit, entry.pos + return node + } } - tok, lit, pos := p.s.Scan() + if len(nodes) > 0 { + node.Nodes = nodes + } - tracef("scan returning next=%s %+#v", tok, scanEntry{tok: tok, lit: lit, pos: pos}) + if len(values) > 0 { + node.Values = values + } - return tok, lit, pos + return node } -func (p *Parser) unscan(tok Token, lit string, pos Pos) { - entry := scanEntry{tok: tok, lit: lit, pos: pos} +func (p *parser) parsePassthrough() Node { + nodes := []Node{} - tracef("unscan entry=%s %+#v", tok, entry) + for ; p.tok != EOL; p.next() { + nodes = append(nodes, &Ident{Literal: p.lit}) + } + + if len(nodes) == 0 { + return nil + } - p.buf = append(p.buf, entry) + return &PassthroughArgs{Nodes: nodes} } diff --git a/argh/parser2.go b/argh/parser2.go deleted file mode 100644 index 66a0142..0000000 --- a/argh/parser2.go +++ /dev/null @@ -1,328 +0,0 @@ -package argh - -import ( - "fmt" - "io" - "strings" -) - -type parser2 struct { - s *Scanner - - cfg *ParserConfig - - errors ScannerErrorList - - tok Token - lit string - pos Pos - - buffered bool -} - -func ParseArgs2(args []string, pCfg *ParserConfig) (*ParseTree, error) { - parser := &parser2{} - parser.init( - strings.NewReader(strings.Join(args, string(nul))), - pCfg, - ) - - tracef("ParseArgs2(...) parser=%+#v", parser) - - return parser.parseArgs() -} - -func (p *parser2) init(r io.Reader, pCfg *ParserConfig) { - p.errors = ScannerErrorList{} - - if pCfg == nil { - pCfg = POSIXyParserConfig - } - - p.cfg = pCfg - - p.s = NewScanner(r, pCfg.ScannerConfig) - - p.next() -} - -func (p *parser2) parseArgs() (*ParseTree, error) { - if p.errors.Len() != 0 { - tracef("parseArgs() bailing due to initial error") - return nil, p.errors.Err() - } - - tracef("parseArgs() parsing %q as program command; cfg=%+#v", p.lit, p.cfg.Prog) - prog := p.parseCommand(&p.cfg.Prog) - - tracef("parseArgs() top level node is %T", prog) - - nodes := []Node{prog} - if v := p.parsePassthrough(); v != nil { - tracef("parseArgs() appending passthrough argument %v", v) - nodes = append(nodes, v) - } - - tracef("parseArgs() returning ParseTree") - - return &ParseTree{Nodes: nodes}, p.errors.Err() -} - -func (p *parser2) next() { - tracef("next() before scan: %v %q %v", p.tok, p.lit, p.pos) - - p.tok, p.lit, p.pos = p.s.Scan() - - tracef("next() after scan: %v %q %v", p.tok, p.lit, p.pos) -} - -func (p *parser2) parseCommand(cCfg *CommandConfig) Node { - tracef("parseCommand(%+#v)", cCfg) - - node := &Command{ - Name: p.lit, - } - values := map[string]string{} - nodes := []Node{} - - identIndex := 0 - - for i := 0; p.tok != EOL; i++ { - if !p.buffered { - tracef("parseCommand(...) buffered=false; scanning next") - p.next() - } - - p.buffered = false - - tracef("parseCommand(...) for=%d values=%+#v", i, values) - tracef("parseCommand(...) for=%d nodes=%+#v", i, nodes) - tracef("parseCommand(...) for=%d tok=%s lit=%q pos=%v", i, p.tok, p.lit, p.pos) - - if subCfg, ok := cCfg.Commands[p.lit]; ok { - subCommand := p.lit - - nodes = append(nodes, p.parseCommand(&subCfg)) - - tracef("parseCommand(...) breaking after sub-command=%v", subCommand) - break - } - - switch p.tok { - case ARG_DELIMITER: - tracef("parseCommand(...) handling %s", p.tok) - - nodes = append(nodes, &ArgDelimiter{}) - - continue - case IDENT, STDIN_FLAG: - tracef("parseCommand(...) handling %s", p.tok) - - if cCfg.NValue.Contains(identIndex) { - name := fmt.Sprintf("%d", identIndex) - - tracef("parseCommand(...) checking for name of identIndex=%d", identIndex) - - if len(cCfg.ValueNames) > identIndex { - name = cCfg.ValueNames[identIndex] - tracef("parseCommand(...) setting name=%s from config value names", name) - } else if len(cCfg.ValueNames) == 1 && (cCfg.NValue == OneOrMoreValue || cCfg.NValue == ZeroOrMoreValue) { - name = fmt.Sprintf("%s.%d", cCfg.ValueNames[0], identIndex) - tracef("parseCommand(...) setting name=%s from repeating value name", name) - } - - values[name] = p.lit - } - - if p.tok == STDIN_FLAG { - nodes = append(nodes, &StdinFlag{}) - } else { - nodes = append(nodes, &Ident{Literal: p.lit}) - } - - identIndex++ - case LONG_FLAG, SHORT_FLAG, COMPOUND_SHORT_FLAG: - tok := p.tok - - flagNode := p.parseFlag(cCfg.Flags) - - tracef("parseCommand(...) appending %s node=%+#v", tok, flagNode) - - nodes = append(nodes, flagNode) - case ASSIGN: - tracef("parseCommand(...) error on bare %s", p.tok) - - p.errors.Add(Position{Column: int(p.pos)}, "invalid bare assignment") - - break - default: - tracef("parseCommand(...) breaking on %s", p.tok) - break - } - } - - if len(nodes) > 0 { - node.Nodes = nodes - } - - if len(values) > 0 { - node.Values = values - } - - tracef("parseCommand(...) returning node=%+#v", node) - return node -} - -func (p *parser2) parseIdent() Node { - node := &Ident{Literal: p.lit} - return node -} - -func (p *parser2) parseFlag(flCfgMap map[string]FlagConfig) Node { - switch p.tok { - case SHORT_FLAG: - tracef("parseFlag(...) parsing short flag with config=%+#v", flCfgMap) - return p.parseShortFlag(flCfgMap) - case LONG_FLAG: - tracef("parseFlag(...) parsing long flag with config=%+#v", flCfgMap) - return p.parseLongFlag(flCfgMap) - case COMPOUND_SHORT_FLAG: - tracef("parseFlag(...) parsing compound short flag with config=%+#v", flCfgMap) - return p.parseCompoundShortFlag(flCfgMap) - } - - panic(fmt.Sprintf("token %v cannot be parsed as flag", p.tok)) -} - -func (p *parser2) parseShortFlag(flCfgMap map[string]FlagConfig) Node { - node := &Flag{Name: string(p.lit[1])} - - flCfg, ok := flCfgMap[node.Name] - if !ok { - return node - } - - return p.parseConfiguredFlag(node, flCfg) -} - -func (p *parser2) parseLongFlag(flCfgMap map[string]FlagConfig) Node { - node := &Flag{Name: string(p.lit[2:])} - - flCfg, ok := flCfgMap[node.Name] - if !ok { - return node - } - - return p.parseConfiguredFlag(node, flCfg) -} - -func (p *parser2) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { - flagNodes := []Node{} - - withoutFlagPrefix := p.lit[1:] - - for i, r := range withoutFlagPrefix { - node := &Flag{Name: string(r)} - - if i == len(withoutFlagPrefix)-1 { - flCfg, ok := flCfgMap[node.Name] - if ok { - flagNodes = append(flagNodes, p.parseConfiguredFlag(node, flCfg)) - - continue - } - } - - flagNodes = append(flagNodes, node) - } - - return &CompoundShortFlag{Nodes: flagNodes} -} - -func (p *parser2) parseConfiguredFlag(node *Flag, flCfg FlagConfig) Node { - values := map[string]string{} - nodes := []Node{} - - identIndex := 0 - - for i := 0; p.tok != EOL; i++ { - if !flCfg.NValue.Contains(identIndex) { - tracef("parseConfiguredFlag(...) identIndex=%d exceeds expected=%v; breaking", identIndex, flCfg.NValue) - break - } - - p.next() - - switch p.tok { - case ARG_DELIMITER: - nodes = append(nodes, &ArgDelimiter{}) - - continue - case ASSIGN: - nodes = append(nodes, &Assign{}) - - continue - case IDENT, STDIN_FLAG: - name := fmt.Sprintf("%d", identIndex) - - tracef("parseConfiguredFlag(...) checking for name of identIndex=%d", identIndex) - - if len(flCfg.ValueNames) > identIndex { - name = flCfg.ValueNames[identIndex] - tracef("parseConfiguredFlag(...) setting name=%s from config value names", name) - } else if len(flCfg.ValueNames) == 1 && (flCfg.NValue == OneOrMoreValue || flCfg.NValue == ZeroOrMoreValue) { - name = fmt.Sprintf("%s.%d", flCfg.ValueNames[0], identIndex) - tracef("parseConfiguredFlag(...) setting name=%s from repeating value name", name) - } else { - tracef("parseConfiguredFlag(...) setting name=%s", name) - } - - values[name] = p.lit - - if p.tok == STDIN_FLAG { - nodes = append(nodes, &StdinFlag{}) - } else { - nodes = append(nodes, &Ident{Literal: p.lit}) - } - - identIndex++ - default: - tracef("parseConfiguredFlag(...) breaking on %s %q %v; setting buffered=true", p.tok, p.lit, p.pos) - p.buffered = true - - if len(nodes) > 0 { - node.Nodes = nodes - } - - if len(values) > 0 { - node.Values = values - } - - return node - } - } - - if len(nodes) > 0 { - node.Nodes = nodes - } - - if len(values) > 0 { - node.Values = values - } - - return node -} - -func (p *parser2) parsePassthrough() Node { - nodes := []Node{} - - for ; p.tok != EOL; p.next() { - nodes = append(nodes, &Ident{Literal: p.lit}) - } - - if len(nodes) == 0 { - return nil - } - - return &PassthroughArgs{Nodes: nodes} -} diff --git a/argh/parser2_test.go b/argh/parser2_test.go deleted file mode 100644 index 5265e09..0000000 --- a/argh/parser2_test.go +++ /dev/null @@ -1,807 +0,0 @@ -package argh_test - -import ( - "testing" - - "git.meatballhat.com/x/box-o-sand/argh" - "github.com/davecgh/go-spew/spew" - "github.com/stretchr/testify/assert" -) - -func TestParser2(t *testing.T) { - for _, tc := range []struct { - name string - args []string - cfg *argh.ParserConfig - expErr error - expPT []argh.Node - expAST []argh.Node - skip bool - }{ - { - name: "basic", - args: []string{ - "pies", "-eat", "--wat", "hello", "mario", - }, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "hello": argh.CommandConfig{ - NValue: 1, - ValueNames: []string{"name"}, - }, - }, - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pies", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "e"}, - &argh.Flag{Name: "a"}, - &argh.Flag{Name: "t"}, - }, - }, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "wat"}, - &argh.ArgDelimiter{}, - &argh.Command{ - Name: "hello", - Values: map[string]string{ - "name": "mario", - }, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "mario"}, - }, - }, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pies", - Nodes: []argh.Node{ - &argh.Flag{Name: "e"}, - &argh.Flag{Name: "a"}, - &argh.Flag{Name: "t"}, - &argh.Flag{Name: "wat"}, - &argh.Command{ - Name: "hello", - Values: map[string]string{ - "name": "mario", - }, - Nodes: []argh.Node{ - &argh.Ident{Literal: "mario"}, - }, - }, - }, - }, - }, - }, - { - name: "bare", - args: []string{"pizzas"}, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - }, - }, - }, - { - name: "one positional arg", - args: []string{"pizzas", "excel"}, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{NValue: 1}, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Values: map[string]string{"0": "excel"}, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "excel"}, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Values: map[string]string{"0": "excel"}, - Nodes: []argh.Node{ - &argh.Ident{Literal: "excel"}, - }, - }, - }, - }, - { - name: "many positional args", - args: []string{"pizzas", "excel", "wildly", "when", "feral"}, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - NValue: argh.OneOrMoreValue, - ValueNames: []string{"word"}, - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Values: map[string]string{ - "word": "excel", - "word.1": "wildly", - "word.2": "when", - "word.3": "feral", - }, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "excel"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "wildly"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "when"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "feral"}, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Values: map[string]string{ - "word": "excel", - "word.1": "wildly", - "word.2": "when", - "word.3": "feral", - }, - Nodes: []argh.Node{ - &argh.Ident{Literal: "excel"}, - &argh.Ident{Literal: "wildly"}, - &argh.Ident{Literal: "when"}, - &argh.Ident{Literal: "feral"}, - }, - }, - }, - }, - { - name: "long value-less flags", - args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &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.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.Flag{Name: "tasty"}, - &argh.Flag{Name: "fresh"}, - &argh.Flag{Name: "super-hot-right-now"}, - }, - }, - }, - }, - { - name: "long flags mixed", - args: []string{ - "pizzas", - "--tasty", - "--fresh", "soon", - "--super-hot-right-now", - "--box", "square", "shaped", "hot", - "--please", - }, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{}, - Flags: map[string]argh.FlagConfig{ - "fresh": argh.FlagConfig{NValue: 1}, - "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, - }, - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Flag{Name: "tasty"}, - &argh.ArgDelimiter{}, - &argh.Flag{ - Name: "fresh", - Values: map[string]string{"0": "soon"}, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "soon"}, - }, - }, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "super-hot-right-now"}, - &argh.ArgDelimiter{}, - &argh.Flag{ - Name: "box", - Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "square"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "shaped"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "hot"}, - &argh.ArgDelimiter{}, - }, - }, - &argh.Flag{Name: "please"}, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.Flag{Name: "tasty"}, - &argh.Flag{ - Name: "fresh", - Values: map[string]string{"0": "soon"}, - Nodes: []argh.Node{ - &argh.Ident{Literal: "soon"}, - }, - }, - &argh.Flag{Name: "super-hot-right-now"}, - &argh.Flag{ - Name: "box", - Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}, - Nodes: []argh.Node{ - &argh.Ident{Literal: "square"}, - &argh.Ident{Literal: "shaped"}, - &argh.Ident{Literal: "hot"}, - }, - }, - &argh.Flag{Name: "please"}, - }, - }, - }, - }, - { - name: "short value-less flags", - args: []string{"pizzas", "-t", "-f", "-s"}, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Flag{Name: "t"}, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "f"}, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "s"}, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &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.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "a"}, - &argh.Flag{Name: "c"}, - &argh.Flag{Name: "a"}, - }, - }, - &argh.ArgDelimiter{}, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "b"}, - &argh.Flag{Name: "l"}, - &argh.Flag{Name: "o"}, - &argh.Flag{Name: "l"}, - }, - }, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &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"}, - }, - }, - }, - }, - { - name: "mixed long short value flags", - args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{}, - Flags: map[string]argh.FlagConfig{ - "b": argh.FlagConfig{NValue: 1}, - }, - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Flag{Name: "a"}, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "ca"}, - &argh.ArgDelimiter{}, - &argh.Flag{ - Name: "b", - Values: map[string]string{"0": "1312"}, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "1312"}, - }, - }, - &argh.ArgDelimiter{}, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "l"}, - &argh.Flag{Name: "o"}, - &argh.Flag{Name: "l"}, - }, - }, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.Flag{Name: "a"}, - &argh.Flag{Name: "ca"}, - &argh.Flag{ - Name: "b", - Values: map[string]string{"0": "1312"}, - Nodes: []argh.Node{ - &argh.Ident{Literal: "1312"}, - }, - }, - &argh.Flag{Name: "l"}, - &argh.Flag{Name: "o"}, - &argh.Flag{Name: "l"}, - }, - }, - }, - }, - { - name: "nested commands with positional args", - args: []string{"pizzas", "fly", "freely", "sometimes", "and", "other", "times", "fry", "deeply", "--forever"}, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "fry": argh.CommandConfig{}, - }, - }, - }, - Flags: map[string]argh.FlagConfig{}, - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Command{ - Name: "fly", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "freely"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "sometimes"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "and"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "other"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "times"}, - &argh.ArgDelimiter{}, - &argh.Command{ - Name: "fry", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "deeply"}, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "forever"}, - }, - }, - }, - }, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.Command{ - Name: "fly", - Nodes: []argh.Node{ - &argh.Ident{Literal: "freely"}, - &argh.Ident{Literal: "sometimes"}, - &argh.Ident{Literal: "and"}, - &argh.Ident{Literal: "other"}, - &argh.Ident{Literal: "times"}, - &argh.Command{ - Name: "fry", - Nodes: []argh.Node{ - &argh.Ident{Literal: "deeply"}, - &argh.Flag{Name: "forever"}, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "compound flags with values", - args: []string{"pizzas", "-need", "sauce", "heat", "love", "-also", "over9000"}, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "a": {NValue: argh.ZeroOrMoreValue}, - "d": {NValue: argh.OneOrMoreValue}, - "e": {}, - "l": {}, - "n": {}, - "o": {NValue: 1, ValueNames: []string{"level"}}, - "s": {NValue: argh.ZeroOrMoreValue}, - }, - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "n"}, - &argh.Flag{Name: "e"}, - &argh.Flag{Name: "e"}, - &argh.Flag{ - Name: "d", - Values: map[string]string{ - "0": "sauce", - "1": "heat", - "2": "love", - }, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "sauce"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "heat"}, - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "love"}, - &argh.ArgDelimiter{}, - }, - }, - }, - }, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "a"}, - &argh.Flag{Name: "l"}, - &argh.Flag{Name: "s"}, - &argh.Flag{ - Name: "o", - Values: map[string]string{ - "level": "over9000", - }, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "over9000"}, - }, - }, - }, - }, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.Flag{Name: "n"}, - &argh.Flag{Name: "e"}, - &argh.Flag{Name: "e"}, - &argh.Flag{ - Name: "d", - Values: map[string]string{ - "0": "sauce", - "1": "heat", - "2": "love", - }, - Nodes: []argh.Node{ - &argh.Ident{Literal: "sauce"}, - &argh.Ident{Literal: "heat"}, - &argh.Ident{Literal: "love"}, - }, - }, - &argh.Flag{Name: "a"}, - &argh.Flag{Name: "l"}, - &argh.Flag{Name: "s"}, - &argh.Flag{ - Name: "o", - Values: map[string]string{ - "level": "over9000", - }, - Nodes: []argh.Node{ - &argh.Ident{Literal: "over9000"}, - }, - }, - }, - }, - }, - }, - { - name: "command specific flags", - args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt", "hugs"}, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "freely": {}, - }, - Commands: map[string]argh.CommandConfig{ - "fry": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "deeply": {}, - "w": {}, - "A": {}, - "t": argh.FlagConfig{NValue: 1}, - }, - }, - }, - }, - }, - Flags: map[string]argh.FlagConfig{}, - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Command{ - Name: "fly", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Flag{Name: "freely"}, - &argh.ArgDelimiter{}, - &argh.Command{ - Name: "fry", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Flag{Name: "deeply"}, - &argh.ArgDelimiter{}, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "w"}, - &argh.Flag{Name: "A"}, - &argh.Flag{ - Name: "t", - Values: map[string]string{"0": "hugs"}, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "hugs"}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - expAST: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.Command{ - Name: "fly", - Nodes: []argh.Node{ - &argh.Flag{Name: "freely"}, - &argh.Command{ - Name: "fry", - Nodes: []argh.Node{ - &argh.Flag{Name: "deeply"}, - &argh.Flag{Name: "w"}, - &argh.Flag{Name: "A"}, - &argh.Flag{ - Name: "t", - Values: map[string]string{"0": "hugs"}, - Nodes: []argh.Node{ - &argh.Ident{Literal: "hugs"}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "total weirdo", - args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, - cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "goose": argh.CommandConfig{ - NValue: 1, - Flags: map[string]argh.FlagConfig{ - "FIERCENESS": argh.FlagConfig{NValue: 1}, - }, - }, - }, - Flags: map[string]argh.FlagConfig{ - "w": argh.FlagConfig{}, - "A": argh.FlagConfig{}, - "T": argh.FlagConfig{NValue: 1}, - "hecking": argh.FlagConfig{}, - }, - }, - ScannerConfig: &argh.ScannerConfig{ - AssignmentOperator: '@', - FlagPrefix: '^', - MultiValueDelim: ',', - }, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "PIZZAs", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.CompoundShortFlag{ - Nodes: []argh.Node{ - &argh.Flag{Name: "w"}, - &argh.Flag{Name: "A"}, - &argh.Flag{ - Name: "T", - Values: map[string]string{"0": "golf"}, - Nodes: []argh.Node{ - &argh.Assign{}, - &argh.Ident{Literal: "golf"}, - }, - }, - }, - }, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "hecKing"}, - &argh.ArgDelimiter{}, - &argh.Command{ - Name: "goose", - Values: map[string]string{"0": "bonk"}, - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.Ident{Literal: "bonk"}, - &argh.ArgDelimiter{}, - &argh.Flag{ - Name: "FIERCENESS", - Values: map[string]string{"0": "-2"}, - Nodes: []argh.Node{ - &argh.Assign{}, - &argh.Ident{Literal: "-2"}, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "invalid bare assignment", - args: []string{"pizzas", "=", "--wat"}, - expErr: argh.ScannerErrorList{ - &argh.ScannerError{Pos: argh.Position{Column: 8}, Msg: "invalid bare assignment"}, - }, - expPT: []argh.Node{ - &argh.Command{ - Name: "pizzas", - Nodes: []argh.Node{ - &argh.ArgDelimiter{}, - &argh.ArgDelimiter{}, - &argh.Flag{Name: "wat"}, - }, - }, - }, - }, - } { - if tc.expPT != nil { - t.Run(tc.name+" parse tree", func(ct *testing.T) { - if tc.skip { - ct.SkipNow() - return - } - - pt, err := argh.ParseArgs2(tc.args, tc.cfg) - if err != nil || tc.expErr != nil { - if !assert.ErrorIs(ct, err, tc.expErr) { - spew.Dump(err, tc.expErr) - spew.Dump(pt) - } - return - } - - if !assert.Equal(ct, tc.expPT, pt.Nodes) { - spew.Dump(pt) - } - }) - } - - if tc.expAST != nil { - t.Run(tc.name+" ast", func(ct *testing.T) { - if tc.skip { - ct.SkipNow() - return - } - - pt, err := argh.ParseArgs2(tc.args, tc.cfg) - if err != nil || tc.expErr != nil { - if !assert.ErrorIs(ct, err, tc.expErr) { - spew.Dump(pt) - } - return - } - - ast := argh.NewQuerier(pt.Nodes).AST() - - if !assert.Equal(ct, tc.expAST, ast) { - spew.Dump(ast) - } - }) - } - } -} diff --git a/argh/parser_test.go b/argh/parser_test.go index 5f89369..5265e09 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -4,27 +4,96 @@ import ( "testing" "git.meatballhat.com/x/box-o-sand/argh" + "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" ) -func TestParser(t *testing.T) { +func TestParser2(t *testing.T) { for _, tc := range []struct { name string args []string cfg *argh.ParserConfig + expErr error expPT []argh.Node expAST []argh.Node - expErr error skip bool }{ + { + name: "basic", + args: []string{ + "pies", "-eat", "--wat", "hello", "mario", + }, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "hello": argh.CommandConfig{ + NValue: 1, + ValueNames: []string{"name"}, + }, + }, + }, + }, + expPT: []argh.Node{ + &argh.Command{ + Name: "pies", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "t"}, + }, + }, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "wat"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "hello", + Values: map[string]string{ + "name": "mario", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "mario"}, + }, + }, + }, + }, + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pies", + Nodes: []argh.Node{ + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "t"}, + &argh.Flag{Name: "wat"}, + &argh.Command{ + Name: "hello", + Values: map[string]string{ + "name": "mario", + }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "mario"}, + }, + }, + }, + }, + }, + }, { name: "bare", args: []string{"pizzas"}, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, + &argh.Command{ + Name: "pizzas", + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas"}, + &argh.Command{ + Name: "pizzas", + }, }, }, { @@ -34,42 +103,98 @@ func TestParser(t *testing.T) { Prog: argh.CommandConfig{NValue: 1}, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + &argh.Command{ + Name: "pizzas", + Values: map[string]string{"0": "excel"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "excel"}, + }, + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel"}}, + &argh.Command{ + Name: "pizzas", + Values: map[string]string{"0": "excel"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "excel"}, + }, + }, }, }, { name: "many positional args", args: []string{"pizzas", "excel", "wildly", "when", "feral"}, cfg: &argh.ParserConfig{ - Prog: argh.CommandConfig{NValue: argh.OneOrMoreValue}, + Prog: argh.CommandConfig{ + NValue: argh.OneOrMoreValue, + ValueNames: []string{"word"}, + }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, + &argh.Command{ + Name: "pizzas", + Values: map[string]string{ + "word": "excel", + "word.1": "wildly", + "word.2": "when", + "word.3": "feral", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "excel"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "wildly"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "when"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "feral"}, + }, + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas", Values: map[string]string{"0": "excel", "1": "wildly", "2": "when", "3": "feral"}}, + &argh.Command{ + Name: "pizzas", + Values: map[string]string{ + "word": "excel", + "word.1": "wildly", + "word.2": "when", + "word.3": "feral", + }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "excel"}, + &argh.Ident{Literal: "wildly"}, + &argh.Ident{Literal: "when"}, + &argh.Ident{Literal: "feral"}, + }, + }, }, }, { name: "long value-less flags", args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "tasty"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "fresh"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "super-hot-right-now"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &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.Command{Name: "pizzas"}, - argh.Flag{Name: "tasty"}, - argh.Flag{Name: "fresh"}, - argh.Flag{Name: "super-hot-right-now"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "tasty"}, + &argh.Flag{Name: "fresh"}, + &argh.Flag{Name: "super-hot-right-now"}, + }, + }, }, }, { @@ -92,79 +217,135 @@ func TestParser(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "tasty"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, - argh.ArgDelimiter{}, - argh.Flag{Name: "super-hot-right-now"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, - argh.ArgDelimiter{}, - argh.Flag{Name: "please"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "tasty"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "fresh", + Values: map[string]string{"0": "soon"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "soon"}, + }, + }, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "super-hot-right-now"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "box", + Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "square"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "shaped"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "hot"}, + &argh.ArgDelimiter{}, + }, + }, + &argh.Flag{Name: "please"}, + }, + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.Flag{Name: "tasty"}, - argh.Flag{Name: "fresh", Values: map[string]string{"0": "soon"}}, - argh.Flag{Name: "super-hot-right-now"}, - argh.Flag{Name: "box", Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}}, - argh.Flag{Name: "please"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "tasty"}, + &argh.Flag{ + Name: "fresh", + Values: map[string]string{"0": "soon"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "soon"}, + }, + }, + &argh.Flag{Name: "super-hot-right-now"}, + &argh.Flag{ + Name: "box", + Values: map[string]string{"0": "square", "1": "shaped", "2": "hot"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "square"}, + &argh.Ident{Literal: "shaped"}, + &argh.Ident{Literal: "hot"}, + }, + }, + &argh.Flag{Name: "please"}, + }, + }, }, }, { name: "short value-less flags", args: []string{"pizzas", "-t", "-f", "-s"}, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "t"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "f"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "s"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "t"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "f"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "s"}, + }, + }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.Flag{Name: "t"}, - argh.Flag{Name: "f"}, - argh.Flag{Name: "s"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &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.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "a"}, - argh.Flag{Name: "c"}, - argh.Flag{Name: "a"}, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "c"}, + &argh.Flag{Name: "a"}, + }, + }, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "b"}, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "o"}, + &argh.Flag{Name: "l"}, + }, + }, }, }, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "b"}, - argh.Flag{Name: "l"}, - argh.Flag{Name: "o"}, - argh.Flag{Name: "l"}, + &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"}, }, }, }, - expAST: []argh.Node{ - argh.Command{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"}, - }, }, { name: "mixed long short value flags", @@ -178,55 +359,230 @@ func TestParser(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "a"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "ca"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "l"}, - argh.Flag{Name: "o"}, - argh.Flag{Name: "l"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "a"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "ca"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "b", + Values: map[string]string{"0": "1312"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "1312"}, + }, + }, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "o"}, + &argh.Flag{Name: "l"}, + }, + }, }, }, }, expAST: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.Flag{Name: "a"}, - argh.Flag{Name: "ca"}, - argh.Flag{Name: "b", Values: map[string]string{"0": "1312"}}, - argh.Flag{Name: "l"}, - argh.Flag{Name: "o"}, - argh.Flag{Name: "l"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "ca"}, + &argh.Flag{ + Name: "b", + Values: map[string]string{"0": "1312"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "1312"}, + }, + }, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "o"}, + &argh.Flag{Name: "l"}, + }, + }, }, }, { - name: "commands", - args: []string{"pizzas", "fly", "fry"}, + name: "nested commands with positional args", + args: []string{"pizzas", "fly", "freely", "sometimes", "and", "other", "times", "fry", "deeply", "--forever"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{}, - "fry": argh.CommandConfig{}, + "fly": argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "fry": argh.CommandConfig{}, + }, + }, }, Flags: map[string]argh.FlagConfig{}, }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fly"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fry"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "freely"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "sometimes"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "and"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "other"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "times"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "deeply"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "forever"}, + }, + }, + }, + }, + }, + }, + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.Ident{Literal: "freely"}, + &argh.Ident{Literal: "sometimes"}, + &argh.Ident{Literal: "and"}, + &argh.Ident{Literal: "other"}, + &argh.Ident{Literal: "times"}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.Ident{Literal: "deeply"}, + &argh.Flag{Name: "forever"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "compound flags with values", + args: []string{"pizzas", "-need", "sauce", "heat", "love", "-also", "over9000"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "a": {NValue: argh.ZeroOrMoreValue}, + "d": {NValue: argh.OneOrMoreValue}, + "e": {}, + "l": {}, + "n": {}, + "o": {NValue: 1, ValueNames: []string{"level"}}, + "s": {NValue: argh.ZeroOrMoreValue}, + }, + }, + }, + expPT: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "n"}, + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "e"}, + &argh.Flag{ + Name: "d", + Values: map[string]string{ + "0": "sauce", + "1": "heat", + "2": "love", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "sauce"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "heat"}, + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "love"}, + &argh.ArgDelimiter{}, + }, + }, + }, + }, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "s"}, + &argh.Flag{ + Name: "o", + Values: map[string]string{ + "level": "over9000", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "over9000"}, + }, + }, + }, + }, + }, + }, + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.Flag{Name: "n"}, + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "e"}, + &argh.Flag{ + Name: "d", + Values: map[string]string{ + "0": "sauce", + "1": "heat", + "2": "love", + }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "sauce"}, + &argh.Ident{Literal: "heat"}, + &argh.Ident{Literal: "love"}, + }, + }, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "l"}, + &argh.Flag{Name: "s"}, + &argh.Flag{ + Name: "o", + Values: map[string]string{ + "level": "over9000", + }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "over9000"}, + }, + }, + }, + }, }, }, { name: "command specific flags", - args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt"}, + args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt", "hugs"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{ @@ -234,13 +590,15 @@ func TestParser(t *testing.T) { Flags: map[string]argh.FlagConfig{ "freely": {}, }, - }, - "fry": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "deeply": {}, - "w": {}, - "A": {}, - "t": {}, + Commands: map[string]argh.CommandConfig{ + "fry": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "deeply": {}, + "w": {}, + "A": {}, + "t": argh.FlagConfig{NValue: 1}, + }, + }, }, }, }, @@ -248,21 +606,68 @@ func TestParser(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fly"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "freely"}, - argh.ArgDelimiter{}, - argh.Command{Name: "fry"}, - argh.ArgDelimiter{}, - argh.Flag{Name: "deeply"}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "freely"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "deeply"}, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "w"}, + &argh.Flag{Name: "A"}, + &argh.Flag{ + Name: "t", + Values: map[string]string{"0": "hugs"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "hugs"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pizzas", Nodes: []argh.Node{ - argh.Flag{Name: "w"}, - argh.Flag{Name: "A"}, - argh.Flag{Name: "t"}, + &argh.Command{ + Name: "fly", + Nodes: []argh.Node{ + &argh.Flag{Name: "freely"}, + &argh.Command{ + Name: "fry", + Nodes: []argh.Node{ + &argh.Flag{Name: "deeply"}, + &argh.Flag{Name: "w"}, + &argh.Flag{Name: "A"}, + &argh.Flag{ + Name: "t", + Values: map[string]string{"0": "hugs"}, + Nodes: []argh.Node{ + &argh.Ident{Literal: "hugs"}, + }, + }, + }, + }, + }, + }, }, }, }, @@ -273,14 +678,18 @@ func TestParser(t *testing.T) { cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{ - "goose": argh.CommandConfig{NValue: 1}, + "goose": argh.CommandConfig{ + NValue: 1, + Flags: map[string]argh.FlagConfig{ + "FIERCENESS": argh.FlagConfig{NValue: 1}, + }, + }, }, Flags: map[string]argh.FlagConfig{ - "w": argh.FlagConfig{}, - "A": argh.FlagConfig{}, - "T": argh.FlagConfig{NValue: 1}, - "hecking": argh.FlagConfig{}, - "FIERCENESS": argh.FlagConfig{NValue: 1}, + "w": argh.FlagConfig{}, + "A": argh.FlagConfig{}, + "T": argh.FlagConfig{NValue: 1}, + "hecking": argh.FlagConfig{}, }, }, ScannerConfig: &argh.ScannerConfig{ @@ -290,61 +699,109 @@ func TestParser(t *testing.T) { }, }, expPT: []argh.Node{ - argh.Command{Name: "PIZZAs"}, - argh.ArgDelimiter{}, - argh.CompoundShortFlag{ + &argh.Command{ + Name: "PIZZAs", Nodes: []argh.Node{ - argh.Flag{Name: "w"}, - argh.Flag{Name: "A"}, - argh.Flag{Name: "T", Values: map[string]string{"0": "golf"}}, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "w"}, + &argh.Flag{Name: "A"}, + &argh.Flag{ + Name: "T", + Values: map[string]string{"0": "golf"}, + Nodes: []argh.Node{ + &argh.Assign{}, + &argh.Ident{Literal: "golf"}, + }, + }, + }, + }, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "hecKing"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "goose", + Values: map[string]string{"0": "bonk"}, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "bonk"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "FIERCENESS", + Values: map[string]string{"0": "-2"}, + Nodes: []argh.Node{ + &argh.Assign{}, + &argh.Ident{Literal: "-2"}, + }, + }, + }, + }, }, }, - argh.ArgDelimiter{}, - argh.Flag{Name: "hecKing"}, - argh.ArgDelimiter{}, - argh.Command{Name: "goose", Values: map[string]string{"0": "bonk"}}, - argh.ArgDelimiter{}, - argh.Flag{Name: "FIERCENESS", Values: map[string]string{"0": "-2"}}, }, }, { - name: "invalid bare assignment", - args: []string{"pizzas", "=", "--wat"}, - expErr: argh.ErrSyntax, + name: "invalid bare assignment", + args: []string{"pizzas", "=", "--wat"}, + expErr: argh.ScannerErrorList{ + &argh.ScannerError{Pos: argh.Position{Column: 8}, Msg: "invalid bare assignment"}, + }, expPT: []argh.Node{ - argh.Command{Name: "pizzas"}, + &argh.Command{ + Name: "pizzas", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "wat"}, + }, + }, }, }, - {}, } { - if tc.skip { - continue - } - 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) + if tc.skip { + ct.SkipNow() + return + } + + pt, err := argh.ParseArgs2(tc.args, tc.cfg) + if err != nil || tc.expErr != nil { + if !assert.ErrorIs(ct, err, tc.expErr) { + spew.Dump(err, tc.expErr) + spew.Dump(pt) + } return } - assert.Equal(ct, tc.expPT, actual.Nodes) + if !assert.Equal(ct, tc.expPT, pt.Nodes) { + spew.Dump(pt) + } }) } - /* - 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 + if tc.expAST != nil { + t.Run(tc.name+" ast", func(ct *testing.T) { + if tc.skip { + ct.SkipNow() + return + } + + pt, err := argh.ParseArgs2(tc.args, tc.cfg) + if err != nil || tc.expErr != nil { + if !assert.ErrorIs(ct, err, tc.expErr) { + spew.Dump(pt) } + return + } + + ast := argh.NewQuerier(pt.Nodes).AST() - assert.Equal(ct, tc.expAST, argh.NewQuerier(actual.Nodes).AST()) - }) - } - */ + if !assert.Equal(ct, tc.expAST, ast) { + spew.Dump(ast) + } + }) + } } } From 622d47071ab4e84f73a1ce05b49ca9287069a091 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 29 May 2022 19:16:13 -0400 Subject: [PATCH 19/23] Rename second parser attempt --- argh/cmd/argh/main.go | 21 ++++++++++++++++----- argh/parser.go | 4 ++-- argh/parser_test.go | 6 +++--- argh/querier_test.go | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/argh/cmd/argh/main.go b/argh/cmd/argh/main.go index 1313da4..e99ff48 100644 --- a/argh/cmd/argh/main.go +++ b/argh/cmd/argh/main.go @@ -7,20 +7,31 @@ import ( "os" "git.meatballhat.com/x/box-o-sand/argh" + "github.com/davecgh/go-spew/spew" ) func main() { + asJSON := os.Getenv("ARGH_OUTPUT_JSON") == "enabled" + log.SetFlags(0) - ast, err := argh.ParseArgs2(os.Args, nil) + pt, err := argh.ParseArgs(os.Args, nil) if err != nil { log.Fatal(err) } - b, err := json.MarshalIndent(ast, "", " ") - if err != nil { - log.Fatal(err) + ast := argh.NewQuerier(pt.Nodes).AST() + + if asJSON { + b, err := json.MarshalIndent(ast, "", " ") + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(b)) + + return } - fmt.Println(string(b)) + spew.Dump(ast) } diff --git a/argh/parser.go b/argh/parser.go index d994efe..f98303e 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -24,14 +24,14 @@ type ParseTree struct { Nodes []Node `json:"nodes"` } -func ParseArgs2(args []string, pCfg *ParserConfig) (*ParseTree, error) { +func ParseArgs(args []string, pCfg *ParserConfig) (*ParseTree, error) { p := &parser{} p.init( strings.NewReader(strings.Join(args, string(nul))), pCfg, ) - tracef("ParseArgs2(...) parser=%+#v", p) + tracef("ParseArgs(...) parser=%+#v", p) return p.parseArgs() } diff --git a/argh/parser_test.go b/argh/parser_test.go index 5265e09..0a4ad7c 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestParser2(t *testing.T) { +func TestParser(t *testing.T) { for _, tc := range []struct { name string args []string @@ -766,7 +766,7 @@ func TestParser2(t *testing.T) { return } - pt, err := argh.ParseArgs2(tc.args, tc.cfg) + pt, err := argh.ParseArgs(tc.args, tc.cfg) if err != nil || tc.expErr != nil { if !assert.ErrorIs(ct, err, tc.expErr) { spew.Dump(err, tc.expErr) @@ -788,7 +788,7 @@ func TestParser2(t *testing.T) { return } - pt, err := argh.ParseArgs2(tc.args, tc.cfg) + pt, err := argh.ParseArgs(tc.args, tc.cfg) if err != nil || tc.expErr != nil { if !assert.ErrorIs(ct, err, tc.expErr) { spew.Dump(pt) diff --git a/argh/querier_test.go b/argh/querier_test.go index edd5330..06e2dae 100644 --- a/argh/querier_test.go +++ b/argh/querier_test.go @@ -34,7 +34,7 @@ func TestQuerier_Program(t *testing.T) { }, } { t.Run(tc.name, func(ct *testing.T) { - pt, err := argh.ParseArgs2(tc.args, tc.cfg) + pt, err := argh.ParseArgs(tc.args, tc.cfg) require.Nil(ct, err) prog, ok := argh.NewQuerier(pt.Nodes).Program() From 395006cdb841ff30ce93f6c2d0126fb64338d0f3 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 29 May 2022 21:12:57 -0400 Subject: [PATCH 20/23] Renaming some error list bits & testing windows-like --- argh/parser.go | 22 +++++++-- argh/parser_error.go | 88 +++++++++++++++++++++++++++++++++ argh/parser_test.go | 110 +++++++++++++++++++++++++++++++++++++++--- argh/querier_test.go | 15 +++++- argh/scanner_error.go | 88 --------------------------------- 5 files changed, 221 insertions(+), 102 deletions(-) create mode 100644 argh/parser_error.go delete mode 100644 argh/scanner_error.go diff --git a/argh/parser.go b/argh/parser.go index f98303e..8c5d0dc 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -11,7 +11,7 @@ type parser struct { cfg *ParserConfig - errors ScannerErrorList + errors ParserErrorList tok Token lit string @@ -36,8 +36,12 @@ func ParseArgs(args []string, pCfg *ParserConfig) (*ParseTree, error) { return p.parseArgs() } +func (p *parser) addError(msg string) { + p.errors.Add(Position{Column: int(p.pos)}, msg) +} + func (p *parser) init(r io.Reader, pCfg *ParserConfig) { - p.errors = ScannerErrorList{} + p.errors = ParserErrorList{} if pCfg == nil { pCfg = POSIXyParserConfig @@ -156,7 +160,7 @@ func (p *parser) parseCommand(cCfg *CommandConfig) Node { case ASSIGN: tracef("parseCommand(...) error on bare %s", p.tok) - p.errors.Add(Position{Column: int(p.pos)}, "invalid bare assignment") + p.addError("invalid bare assignment") break default: @@ -203,6 +207,8 @@ func (p *parser) parseShortFlag(flCfgMap map[string]FlagConfig) Node { flCfg, ok := flCfgMap[node.Name] if !ok { + p.addError(fmt.Sprintf("unknown flag %q", node.Name)) + return node } @@ -214,6 +220,8 @@ func (p *parser) parseLongFlag(flCfgMap map[string]FlagConfig) Node { flCfg, ok := flCfgMap[node.Name] if !ok { + p.addError(fmt.Sprintf("unknown flag %q", node.Name)) + return node } @@ -230,11 +238,15 @@ func (p *parser) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { if i == len(withoutFlagPrefix)-1 { flCfg, ok := flCfgMap[node.Name] - if ok { - flagNodes = append(flagNodes, p.parseConfiguredFlag(node, flCfg)) + if !ok { + p.addError(fmt.Sprintf("unknown flag %q", node.Name)) continue } + + flagNodes = append(flagNodes, p.parseConfiguredFlag(node, flCfg)) + + continue } flagNodes = append(flagNodes, node) diff --git a/argh/parser_error.go b/argh/parser_error.go new file mode 100644 index 0000000..62ae3b7 --- /dev/null +++ b/argh/parser_error.go @@ -0,0 +1,88 @@ +package argh + +import ( + "fmt" + "io" + "sort" +) + +// ParserError is largely borrowed from go/scanner.Error +type ParserError struct { + Pos Position + Msg string +} + +func (e ParserError) Error() string { + if e.Pos.IsValid() { + return e.Pos.String() + ":" + e.Msg + } + + return e.Msg +} + +// ParserErrorList is largely borrowed from go/scanner.ErrorList +type ParserErrorList []*ParserError + +func (el *ParserErrorList) Add(pos Position, msg string) { + *el = append(*el, &ParserError{Pos: pos, Msg: msg}) +} + +func (el *ParserErrorList) Reset() { *el = (*el)[0:0] } + +func (el ParserErrorList) Len() int { return len(el) } + +func (el ParserErrorList) Swap(i, j int) { el[i], el[j] = el[j], el[i] } + +func (el ParserErrorList) Less(i, j int) bool { + e := &el[i].Pos + f := &el[j].Pos + + if e.Column != f.Column { + return e.Column < f.Column + } + + return el[i].Msg < el[j].Msg +} + +func (el ParserErrorList) Sort() { + sort.Sort(el) +} + +func (el ParserErrorList) Error() string { + switch len(el) { + case 0: + return "no errors" + case 1: + return el[0].Error() + } + return fmt.Sprintf("%s (and %d more errors)", el[0], len(el)-1) +} + +func (el ParserErrorList) Err() error { + if len(el) == 0 { + return nil + } + return el +} + +func (el ParserErrorList) Is(other error) bool { + if _, ok := other.(ParserErrorList); ok { + return el.Error() == other.Error() + } + + if v, ok := other.(*ParserErrorList); ok { + return el.Error() == (*v).Error() + } + + return false +} + +func PrintParserError(w io.Writer, err error) { + if list, ok := err.(ParserErrorList); ok { + for _, e := range list { + fmt.Fprintf(w, "%s\n", e) + } + } else if err != nil { + fmt.Fprintf(w, "%s\n", err) + } +} diff --git a/argh/parser_test.go b/argh/parser_test.go index 0a4ad7c..fe57c77 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -25,6 +25,12 @@ func TestParser(t *testing.T) { }, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "e": {}, + "a": {}, + "t": {}, + "wat": {}, + }, Commands: map[string]argh.CommandConfig{ "hello": argh.CommandConfig{ NValue: 1, @@ -173,6 +179,15 @@ func TestParser(t *testing.T) { { name: "long value-less flags", args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "tasty": {}, + "fresh": {}, + "super-hot-right-now": {}, + }, + }, + }, expPT: []argh.Node{ &argh.Command{ Name: "pizzas", @@ -211,8 +226,11 @@ func TestParser(t *testing.T) { Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{}, Flags: map[string]argh.FlagConfig{ - "fresh": argh.FlagConfig{NValue: 1}, - "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, + "tasty": {}, + "fresh": argh.FlagConfig{NValue: 1}, + "super-hot-right-now": {}, + "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, + "please": {}, }, }, }, @@ -281,6 +299,15 @@ func TestParser(t *testing.T) { { name: "short value-less flags", args: []string{"pizzas", "-t", "-f", "-s"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "t": {}, + "f": {}, + "s": {}, + }, + }, + }, expPT: []argh.Node{ &argh.Command{ Name: "pizzas", @@ -308,6 +335,17 @@ func TestParser(t *testing.T) { { name: "compound short flags", args: []string{"pizzas", "-aca", "-blol"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "a": {}, + "b": {}, + "c": {}, + "l": {}, + "o": {}, + }, + }, + }, expPT: []argh.Node{ &argh.Command{ Name: "pizzas", @@ -354,7 +392,11 @@ func TestParser(t *testing.T) { Prog: argh.CommandConfig{ Commands: map[string]argh.CommandConfig{}, Flags: map[string]argh.FlagConfig{ - "b": argh.FlagConfig{NValue: 1}, + "a": {}, + "b": argh.FlagConfig{NValue: 1}, + "ca": {}, + "l": {}, + "o": {}, }, }, }, @@ -414,7 +456,11 @@ func TestParser(t *testing.T) { Commands: map[string]argh.CommandConfig{ "fly": argh.CommandConfig{ Commands: map[string]argh.CommandConfig{ - "fry": argh.CommandConfig{}, + "fry": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "forever": {}, + }, + }, }, }, }, @@ -689,7 +735,7 @@ func TestParser(t *testing.T) { "w": argh.FlagConfig{}, "A": argh.FlagConfig{}, "T": argh.FlagConfig{NValue: 1}, - "hecking": argh.FlagConfig{}, + "hecKing": argh.FlagConfig{}, }, }, ScannerConfig: &argh.ScannerConfig{ @@ -741,11 +787,61 @@ func TestParser(t *testing.T) { }, }, }, + { + name: "windows like", + args: []string{"hotdog", "/f", "/L", "/o:ppy", "hats"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "f": {}, + "L": {}, + "o": argh.FlagConfig{NValue: 1}, + }, + Commands: map[string]argh.CommandConfig{ + "hats": {}, + }, + }, + ScannerConfig: &argh.ScannerConfig{ + AssignmentOperator: ':', + FlagPrefix: '/', + MultiValueDelim: ',', + }, + }, + expPT: []argh.Node{ + &argh.Command{ + Name: "hotdog", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "f"}, + &argh.ArgDelimiter{}, + &argh.Flag{Name: "L"}, + &argh.ArgDelimiter{}, + &argh.Flag{ + Name: "o", + Values: map[string]string{"0": "ppy"}, + Nodes: []argh.Node{ + &argh.Assign{}, + &argh.Ident{Literal: "ppy"}, + }, + }, + &argh.ArgDelimiter{}, + &argh.Command{Name: "hats"}, + }, + }, + }, + }, { name: "invalid bare assignment", args: []string{"pizzas", "=", "--wat"}, - expErr: argh.ScannerErrorList{ - &argh.ScannerError{Pos: argh.Position{Column: 8}, Msg: "invalid bare assignment"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "wat": {}, + }, + }, + }, + expErr: argh.ParserErrorList{ + &argh.ParserError{Pos: argh.Position{Column: 8}, Msg: "invalid bare assignment"}, }, expPT: []argh.Node{ &argh.Command{ diff --git a/argh/querier_test.go b/argh/querier_test.go index 06e2dae..bb3b767 100644 --- a/argh/querier_test.go +++ b/argh/querier_test.go @@ -16,8 +16,19 @@ func TestQuerier_Program(t *testing.T) { expOK bool }{ { - name: "typical", - args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, + name: "typical", + args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: map[string]argh.CommandConfig{ + "ahoy": argh.CommandConfig{ + Flags: map[string]argh.FlagConfig{ + "treatsa": argh.FlagConfig{NValue: 1}, + }, + }, + }, + }, + }, exp: "pizzas", expOK: true, }, diff --git a/argh/scanner_error.go b/argh/scanner_error.go deleted file mode 100644 index e5b634b..0000000 --- a/argh/scanner_error.go +++ /dev/null @@ -1,88 +0,0 @@ -package argh - -import ( - "fmt" - "io" - "sort" -) - -// ScannerError is largely borrowed from go/scanner.Error -type ScannerError struct { - Pos Position - Msg string -} - -func (e ScannerError) Error() string { - if e.Pos.IsValid() { - return e.Pos.String() + ":" + e.Msg - } - - return e.Msg -} - -// ScannerErrorList is largely borrowed from go/scanner.ErrorList -type ScannerErrorList []*ScannerError - -func (el *ScannerErrorList) Add(pos Position, msg string) { - *el = append(*el, &ScannerError{Pos: pos, Msg: msg}) -} - -func (el *ScannerErrorList) Reset() { *el = (*el)[0:0] } - -func (el ScannerErrorList) Len() int { return len(el) } - -func (el ScannerErrorList) Swap(i, j int) { el[i], el[j] = el[j], el[i] } - -func (el ScannerErrorList) Less(i, j int) bool { - e := &el[i].Pos - f := &el[j].Pos - - if e.Column != f.Column { - return e.Column < f.Column - } - - return el[i].Msg < el[j].Msg -} - -func (el ScannerErrorList) Sort() { - sort.Sort(el) -} - -func (el ScannerErrorList) Error() string { - switch len(el) { - case 0: - return "no errors" - case 1: - return el[0].Error() - } - return fmt.Sprintf("%s (and %d more errors)", el[0], len(el)-1) -} - -func (el ScannerErrorList) Err() error { - if len(el) == 0 { - return nil - } - return el -} - -func (el ScannerErrorList) Is(other error) bool { - if _, ok := other.(ScannerErrorList); ok { - return el.Error() == other.Error() - } - - if v, ok := other.(*ScannerErrorList); ok { - return el.Error() == (*v).Error() - } - - return false -} - -func PrintScannerError(w io.Writer, err error) { - if list, ok := err.(ScannerErrorList); ok { - for _, e := range list { - fmt.Fprintf(w, "%s\n", e) - } - } else if err != nil { - fmt.Fprintf(w, "%s\n", err) - } -} From 92e3d6fe5b8e385d70073f6eb1623eb316a9d9bc Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Tue, 31 May 2022 08:24:58 -0400 Subject: [PATCH 21/23] Support persistent flags --- argh/parser.go | 30 +++-- argh/parser_config.go | 55 +++++++- argh/parser_test.go | 291 ++++++++++++++++++++++++++++++------------ argh/querier_test.go | 12 +- 4 files changed, 285 insertions(+), 103 deletions(-) diff --git a/argh/parser.go b/argh/parser.go index 8c5d0dc..72c15d9 100644 --- a/argh/parser.go +++ b/argh/parser.go @@ -107,7 +107,9 @@ func (p *parser) parseCommand(cCfg *CommandConfig) Node { tracef("parseCommand(...) for=%d nodes=%+#v", i, nodes) tracef("parseCommand(...) for=%d tok=%s lit=%q pos=%v", i, p.tok, p.lit, p.pos) - if subCfg, ok := cCfg.Commands[p.lit]; ok { + tracef("parseCommand(...) cCfg=%+#v", cCfg) + + if subCfg, ok := cCfg.GetCommandConfig(p.lit); ok { subCommand := p.lit nodes = append(nodes, p.parseCommand(&subCfg)) @@ -186,26 +188,26 @@ func (p *parser) parseIdent() Node { return node } -func (p *parser) parseFlag(flCfgMap map[string]FlagConfig) Node { +func (p *parser) parseFlag(flags *Flags) Node { switch p.tok { case SHORT_FLAG: - tracef("parseFlag(...) parsing short flag with config=%+#v", flCfgMap) - return p.parseShortFlag(flCfgMap) + tracef("parseFlag(...) parsing short flag with config=%+#v", flags) + return p.parseShortFlag(flags) case LONG_FLAG: - tracef("parseFlag(...) parsing long flag with config=%+#v", flCfgMap) - return p.parseLongFlag(flCfgMap) + tracef("parseFlag(...) parsing long flag with config=%+#v", flags) + return p.parseLongFlag(flags) case COMPOUND_SHORT_FLAG: - tracef("parseFlag(...) parsing compound short flag with config=%+#v", flCfgMap) - return p.parseCompoundShortFlag(flCfgMap) + tracef("parseFlag(...) parsing compound short flag with config=%+#v", flags) + return p.parseCompoundShortFlag(flags) } panic(fmt.Sprintf("token %v cannot be parsed as flag", p.tok)) } -func (p *parser) parseShortFlag(flCfgMap map[string]FlagConfig) Node { +func (p *parser) parseShortFlag(flags *Flags) Node { node := &Flag{Name: string(p.lit[1])} - flCfg, ok := flCfgMap[node.Name] + flCfg, ok := flags.Get(node.Name) if !ok { p.addError(fmt.Sprintf("unknown flag %q", node.Name)) @@ -215,10 +217,10 @@ func (p *parser) parseShortFlag(flCfgMap map[string]FlagConfig) Node { return p.parseConfiguredFlag(node, flCfg) } -func (p *parser) parseLongFlag(flCfgMap map[string]FlagConfig) Node { +func (p *parser) parseLongFlag(flags *Flags) Node { node := &Flag{Name: string(p.lit[2:])} - flCfg, ok := flCfgMap[node.Name] + flCfg, ok := flags.Get(node.Name) if !ok { p.addError(fmt.Sprintf("unknown flag %q", node.Name)) @@ -228,7 +230,7 @@ func (p *parser) parseLongFlag(flCfgMap map[string]FlagConfig) Node { return p.parseConfiguredFlag(node, flCfg) } -func (p *parser) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { +func (p *parser) parseCompoundShortFlag(flags *Flags) Node { flagNodes := []Node{} withoutFlagPrefix := p.lit[1:] @@ -237,7 +239,7 @@ func (p *parser) parseCompoundShortFlag(flCfgMap map[string]FlagConfig) Node { node := &Flag{Name: string(r)} if i == len(withoutFlagPrefix)-1 { - flCfg, ok := flCfgMap[node.Name] + flCfg, ok := flags.Get(node.Name) if !ok { p.addError(fmt.Sprintf("unknown flag %q", node.Name)) diff --git a/argh/parser_config.go b/argh/parser_config.go index 62b209e..e77638e 100644 --- a/argh/parser_config.go +++ b/argh/parser_config.go @@ -36,11 +36,62 @@ type ParserConfig struct { type CommandConfig struct { NValue NValue ValueNames []string - Flags map[string]FlagConfig - Commands map[string]CommandConfig + Flags *Flags + Commands *Commands +} + +func (cCfg *CommandConfig) GetCommandConfig(name string) (CommandConfig, bool) { + if cCfg.Commands == nil { + cCfg.Commands = &Commands{Map: map[string]CommandConfig{}} + } + + return cCfg.Commands.Get(name) +} + +func (cCfg *CommandConfig) GetFlagConfig(name string) (FlagConfig, bool) { + if cCfg.Flags == nil { + cCfg.Flags = &Flags{Map: map[string]FlagConfig{}} + } + + return cCfg.Flags.Get(name) } type FlagConfig struct { NValue NValue + Persist bool ValueNames []string } + +type Flags struct { + Parent *Flags + Map map[string]FlagConfig +} + +func (fl *Flags) Get(name string) (FlagConfig, bool) { + if fl.Map == nil { + fl.Map = map[string]FlagConfig{} + } + + flCfg, ok := fl.Map[name] + if !ok && fl.Parent != nil { + flCfg, ok = fl.Parent.Get(name) + return flCfg, ok && flCfg.Persist + } + + return flCfg, ok +} + +type Commands struct { + Map map[string]CommandConfig +} + +func (cmd *Commands) Get(name string) (CommandConfig, bool) { + tracef("Get(%q)", name) + + if cmd.Map == nil { + cmd.Map = map[string]CommandConfig{} + } + + cmdCfg, ok := cmd.Map[name] + return cmdCfg, ok +} diff --git a/argh/parser_test.go b/argh/parser_test.go index fe57c77..bea803f 100644 --- a/argh/parser_test.go +++ b/argh/parser_test.go @@ -25,16 +25,20 @@ func TestParser(t *testing.T) { }, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "e": {}, - "a": {}, - "t": {}, - "wat": {}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "e": {}, + "a": {}, + "t": {}, + "wat": {}, + }, }, - Commands: map[string]argh.CommandConfig{ - "hello": argh.CommandConfig{ - NValue: 1, - ValueNames: []string{"name"}, + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "hello": argh.CommandConfig{ + NValue: 1, + ValueNames: []string{"name"}, + }, }, }, }, @@ -88,6 +92,89 @@ func TestParser(t *testing.T) { }, }, }, + { + name: "persistent flags", + args: []string{ + "pies", "--wat", "hello", "mario", "-eat", + }, + cfg: &argh.ParserConfig{ + Prog: func() argh.CommandConfig { + cmdCfg := argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "e": {Persist: true}, + "a": {Persist: true}, + "t": {Persist: true}, + "wat": {}, + }, + }, + } + + cmdCfg.Commands = &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "hello": argh.CommandConfig{ + NValue: 1, + ValueNames: []string{"name"}, + Flags: &argh.Flags{ + Parent: cmdCfg.Flags, + Map: map[string]argh.FlagConfig{}, + }, + }, + }, + } + + return cmdCfg + }(), + }, + expPT: []argh.Node{ + &argh.Command{ + Name: "pies", + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Flag{Name: "wat"}, + &argh.ArgDelimiter{}, + &argh.Command{ + Name: "hello", + Values: map[string]string{ + "name": "mario", + }, + Nodes: []argh.Node{ + &argh.ArgDelimiter{}, + &argh.Ident{Literal: "mario"}, + &argh.ArgDelimiter{}, + &argh.CompoundShortFlag{ + Nodes: []argh.Node{ + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "t"}, + }, + }, + }, + }, + }, + }, + }, + expAST: []argh.Node{ + &argh.Command{ + Name: "pies", + Nodes: []argh.Node{ + &argh.Flag{Name: "wat"}, + &argh.Command{ + Name: "hello", + Values: map[string]string{ + "name": "mario", + }, + Nodes: []argh.Node{ + &argh.Ident{Literal: "mario"}, + &argh.Flag{Name: "e"}, + &argh.Flag{Name: "a"}, + &argh.Flag{Name: "t"}, + }, + }, + }, + }, + }, + }, { name: "bare", args: []string{"pizzas"}, @@ -181,10 +268,12 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "tasty": {}, - "fresh": {}, - "super-hot-right-now": {}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "tasty": {}, + "fresh": {}, + "super-hot-right-now": {}, + }, }, }, }, @@ -224,13 +313,15 @@ func TestParser(t *testing.T) { }, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{}, - Flags: map[string]argh.FlagConfig{ - "tasty": {}, - "fresh": argh.FlagConfig{NValue: 1}, - "super-hot-right-now": {}, - "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, - "please": {}, + Commands: &argh.Commands{Map: map[string]argh.CommandConfig{}}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "tasty": {}, + "fresh": argh.FlagConfig{NValue: 1}, + "super-hot-right-now": {}, + "box": argh.FlagConfig{NValue: argh.OneOrMoreValue}, + "please": {}, + }, }, }, }, @@ -301,10 +392,12 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "-t", "-f", "-s"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "t": {}, - "f": {}, - "s": {}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "t": {}, + "f": {}, + "s": {}, + }, }, }, }, @@ -337,12 +430,14 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "-aca", "-blol"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "a": {}, - "b": {}, - "c": {}, - "l": {}, - "o": {}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "a": {}, + "b": {}, + "c": {}, + "l": {}, + "o": {}, + }, }, }, }, @@ -390,13 +485,15 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{}, - Flags: map[string]argh.FlagConfig{ - "a": {}, - "b": argh.FlagConfig{NValue: 1}, - "ca": {}, - "l": {}, - "o": {}, + Commands: &argh.Commands{Map: map[string]argh.CommandConfig{}}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "a": {}, + "b": argh.FlagConfig{NValue: 1}, + "ca": {}, + "l": {}, + "o": {}, + }, }, }, }, @@ -453,18 +550,24 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "fly", "freely", "sometimes", "and", "other", "times", "fry", "deeply", "--forever"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "fry": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "forever": {}, + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{ + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "fry": argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "forever": {}, + }, + }, + }, }, }, }, }, }, - Flags: map[string]argh.FlagConfig{}, + Flags: &argh.Flags{Map: map[string]argh.FlagConfig{}}, }, }, expPT: []argh.Node{ @@ -530,14 +633,16 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "-need", "sauce", "heat", "love", "-also", "over9000"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "a": {NValue: argh.ZeroOrMoreValue}, - "d": {NValue: argh.OneOrMoreValue}, - "e": {}, - "l": {}, - "n": {}, - "o": {NValue: 1, ValueNames: []string{"level"}}, - "s": {NValue: argh.ZeroOrMoreValue}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "a": {NValue: argh.ZeroOrMoreValue}, + "d": {NValue: argh.OneOrMoreValue}, + "e": {}, + "l": {}, + "n": {}, + "o": {NValue: 1, ValueNames: []string{"level"}}, + "s": {NValue: argh.ZeroOrMoreValue}, + }, }, }, }, @@ -631,24 +736,32 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt", "hugs"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "fly": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "freely": {}, - }, - Commands: map[string]argh.CommandConfig{ - "fry": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "deeply": {}, - "w": {}, - "A": {}, - "t": argh.FlagConfig{NValue: 1}, + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "fly": argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "freely": {}, + }, + }, + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "fry": argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "deeply": {}, + "w": {}, + "A": {}, + "t": argh.FlagConfig{NValue: 1}, + }, + }, + }, }, }, }, }, }, - Flags: map[string]argh.FlagConfig{}, + Flags: &argh.Flags{Map: map[string]argh.FlagConfig{}}, }, }, expPT: []argh.Node{ @@ -723,19 +836,25 @@ func TestParser(t *testing.T) { args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "goose": argh.CommandConfig{ - NValue: 1, - Flags: map[string]argh.FlagConfig{ - "FIERCENESS": argh.FlagConfig{NValue: 1}, + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "goose": argh.CommandConfig{ + NValue: 1, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "FIERCENESS": argh.FlagConfig{NValue: 1}, + }, + }, }, }, }, - Flags: map[string]argh.FlagConfig{ - "w": argh.FlagConfig{}, - "A": argh.FlagConfig{}, - "T": argh.FlagConfig{NValue: 1}, - "hecKing": argh.FlagConfig{}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "w": argh.FlagConfig{}, + "A": argh.FlagConfig{}, + "T": argh.FlagConfig{NValue: 1}, + "hecKing": argh.FlagConfig{}, + }, }, }, ScannerConfig: &argh.ScannerConfig{ @@ -792,13 +911,17 @@ func TestParser(t *testing.T) { args: []string{"hotdog", "/f", "/L", "/o:ppy", "hats"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "f": {}, - "L": {}, - "o": argh.FlagConfig{NValue: 1}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "f": {}, + "L": {}, + "o": argh.FlagConfig{NValue: 1}, + }, }, - Commands: map[string]argh.CommandConfig{ - "hats": {}, + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "hats": {}, + }, }, }, ScannerConfig: &argh.ScannerConfig{ @@ -835,8 +958,10 @@ func TestParser(t *testing.T) { args: []string{"pizzas", "=", "--wat"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "wat": {}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "wat": {}, + }, }, }, }, diff --git a/argh/querier_test.go b/argh/querier_test.go index bb3b767..962d869 100644 --- a/argh/querier_test.go +++ b/argh/querier_test.go @@ -20,10 +20,14 @@ func TestQuerier_Program(t *testing.T) { args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, cfg: &argh.ParserConfig{ Prog: argh.CommandConfig{ - Commands: map[string]argh.CommandConfig{ - "ahoy": argh.CommandConfig{ - Flags: map[string]argh.FlagConfig{ - "treatsa": argh.FlagConfig{NValue: 1}, + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "ahoy": argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "treatsa": argh.FlagConfig{NValue: 1}, + }, + }, }, }, }, From a73acedc6dc4996f8b2debccc4fcb1d5c70b0d75 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 3 Jun 2022 23:12:53 -0400 Subject: [PATCH 22/23] Minor tracing tweaks --- argh/parser_config.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/argh/parser_config.go b/argh/parser_config.go index e77638e..78d7277 100644 --- a/argh/parser_config.go +++ b/argh/parser_config.go @@ -16,6 +16,8 @@ var ( type NValue int func (nv NValue) Contains(i int) bool { + tracef("NValue.Contains(%v)", i) + if i < int(ZeroValue) { return false } @@ -41,6 +43,8 @@ type CommandConfig struct { } func (cCfg *CommandConfig) GetCommandConfig(name string) (CommandConfig, bool) { + tracef("CommandConfig.GetCommandConfig(%q)", name) + if cCfg.Commands == nil { cCfg.Commands = &Commands{Map: map[string]CommandConfig{}} } @@ -49,6 +53,8 @@ func (cCfg *CommandConfig) GetCommandConfig(name string) (CommandConfig, bool) { } func (cCfg *CommandConfig) GetFlagConfig(name string) (FlagConfig, bool) { + tracef("CommandConfig.GetFlagConfig(%q)", name) + if cCfg.Flags == nil { cCfg.Flags = &Flags{Map: map[string]FlagConfig{}} } @@ -68,6 +74,8 @@ type Flags struct { } func (fl *Flags) Get(name string) (FlagConfig, bool) { + tracef("Flags.Get(%q)", name) + if fl.Map == nil { fl.Map = map[string]FlagConfig{} } @@ -86,7 +94,7 @@ type Commands struct { } func (cmd *Commands) Get(name string) (CommandConfig, bool) { - tracef("Get(%q)", name) + tracef("Commands.Get(%q)", name) if cmd.Map == nil { cmd.Map = map[string]CommandConfig{} From 3de96b813f2cb918f71ab2b08310e955b3996115 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 4 Jun 2022 17:38:24 -0400 Subject: [PATCH 23/23] Making more messes with parser config --- argh/cmd/{argh => argh-example}/main.go | 11 ++++- argh/parser_config.go | 55 +++++++++++++++++++++---- 2 files changed, 58 insertions(+), 8 deletions(-) rename argh/cmd/{argh => argh-example}/main.go (67%) diff --git a/argh/cmd/argh/main.go b/argh/cmd/argh-example/main.go similarity index 67% rename from argh/cmd/argh/main.go rename to argh/cmd/argh-example/main.go index e99ff48..325c7ec 100644 --- a/argh/cmd/argh/main.go +++ b/argh/cmd/argh-example/main.go @@ -15,7 +15,16 @@ func main() { log.SetFlags(0) - pt, err := argh.ParseArgs(os.Args, nil) + pt, err := argh.ParseArgs(os.Args, argh.NewParserConfig( + &argh.CommandConfig{ + NValue: argh.OneOrMoreValue, + ValueNames: []string{"topping"}, + Flags: &argh.Flags{ + Automatic: true, + }, + }, + nil, + )) if err != nil { log.Fatal(err) } diff --git a/argh/parser_config.go b/argh/parser_config.go index 78d7277..62ea57d 100644 --- a/argh/parser_config.go +++ b/argh/parser_config.go @@ -7,10 +7,10 @@ const ( ) var ( - POSIXyParserConfig = &ParserConfig{ - Prog: CommandConfig{}, - ScannerConfig: POSIXyScannerConfig, - } + POSIXyParserConfig = NewParserConfig( + nil, + POSIXyScannerConfig, + ) ) type NValue int @@ -35,6 +35,25 @@ type ParserConfig struct { ScannerConfig *ScannerConfig } +func NewParserConfig(prog *CommandConfig, sCfg *ScannerConfig) *ParserConfig { + if sCfg == nil { + sCfg = POSIXyScannerConfig + } + + if prog == nil { + prog = &CommandConfig{} + } + + prog.init() + + pCfg := &ParserConfig{ + Prog: *prog, + ScannerConfig: sCfg, + } + + return pCfg +} + type CommandConfig struct { NValue NValue ValueNames []string @@ -42,6 +61,20 @@ type CommandConfig struct { Commands *Commands } +func (cCfg *CommandConfig) init() { + if cCfg.ValueNames == nil { + cCfg.ValueNames = []string{} + } + + if cCfg.Flags == nil { + cCfg.Flags = &Flags{} + } + + if cCfg.Commands == nil { + cCfg.Commands = &Commands{} + } +} + func (cCfg *CommandConfig) GetCommandConfig(name string) (CommandConfig, bool) { tracef("CommandConfig.GetCommandConfig(%q)", name) @@ -71,6 +104,8 @@ type FlagConfig struct { type Flags struct { Parent *Flags Map map[string]FlagConfig + + Automatic bool } func (fl *Flags) Get(name string) (FlagConfig, bool) { @@ -81,9 +116,15 @@ func (fl *Flags) Get(name string) (FlagConfig, bool) { } flCfg, ok := fl.Map[name] - if !ok && fl.Parent != nil { - flCfg, ok = fl.Parent.Get(name) - return flCfg, ok && flCfg.Persist + if !ok { + if fl.Automatic { + return FlagConfig{}, true + } + + if fl.Parent != nil { + flCfg, ok = fl.Parent.Get(name) + return flCfg, ok && flCfg.Persist + } } return flCfg, ok