From 8c280c303e4cc0d61c0e2c421edd2ce6da23d532 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 15 May 2022 14:22:56 -0400 Subject: [PATCH] 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"},