Handle variable count flag values
This commit is contained in:
parent
b2e61cd0d2
commit
8c280c303e
40
argh/node.go
Normal file
40
argh/node.go
Normal file
@ -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{}
|
25
argh/nvalue_string.go
Normal file
25
argh/nvalue_string.go
Normal file
@ -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]]
|
||||
}
|
@ -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{}
|
||||
|
131
argh/parser.go
131
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}
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
tracef("unscan entry=%s %+#v", tok, entry)
|
||||
|
||||
p.buf = append(p.buf, entry)
|
||||
}
|
||||
|
@ -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"},
|
||||
|
Loading…
Reference in New Issue
Block a user