Compare commits

..

2 Commits

5 changed files with 291 additions and 112 deletions

41
argh/node.go Normal file
View File

@ -0,0 +1,41 @@
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"`
Values []string `json:"values"`
}
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
View 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]]
}

View File

@ -42,7 +42,7 @@ func (pt *ParseTree) ast() []Node {
continue continue
} }
if v, ok := node.(Statement); ok { if v, ok := node.(CompoundShortFlag); ok {
for _, subNode := range v.Nodes { for _, subNode := range v.Nodes {
ret = append(ret, subNode) ret = append(ret, subNode)
} }
@ -55,41 +55,3 @@ func (pt *ParseTree) ast() []Node {
return ret 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{}

View File

@ -1,3 +1,5 @@
//go:generate stringer -type NValue
package argh package argh
import ( import (
@ -7,16 +9,24 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const (
ZeroValue NValue = iota
OneValue
OneOrMoreValue
)
var ( var (
errSyntax = errors.New("syntax error") errSyntax = errors.New("syntax error")
DefaultParserConfig = &ParserConfig{ DefaultParserConfig = &ParserConfig{
Commands: []string{}, Commands: map[string]NValue{},
ValueFlags: []string{}, Flags: map[string]NValue{},
ScannerConfig: DefaultScannerConfig, ScannerConfig: DefaultScannerConfig,
} }
) )
type NValue int
func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) { func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) {
reEncoded := strings.Join(args, string(nul)) reEncoded := strings.Join(args, string(nul))
@ -27,50 +37,38 @@ func ParseArgs(args []string, pCfg *ParserConfig) (*Argh, error) {
} }
type Parser struct { type Parser struct {
s *Scanner s *Scanner
buf ParserBuffer
commands map[string]struct{} buf []ScanEntry
valueFlags map[string]struct{}
cfg *ParserConfig
nodes []Node nodes []Node
stopSeen bool stopSeen bool
} }
type ParserBuffer struct { type ScanEntry struct {
tok Token tok Token
lit string lit string
pos int pos int
n int
} }
type ParserConfig struct { type ParserConfig struct {
Commands []string ProgValues NValue
ValueFlags []string Commands map[string]NValue
Flags map[string]NValue
ScannerConfig *ScannerConfig ScannerConfig *ScannerConfig
} }
type parseDirective struct {
Break bool
}
func NewParser(r io.Reader, pCfg *ParserConfig) *Parser { func NewParser(r io.Reader, pCfg *ParserConfig) *Parser {
if pCfg == nil { if pCfg == nil {
pCfg = DefaultParserConfig pCfg = DefaultParserConfig
} }
parser := &Parser{ parser := &Parser{
s: NewScanner(r, pCfg.ScannerConfig), buf: []ScanEntry{},
commands: map[string]struct{}{}, s: NewScanner(r, pCfg.ScannerConfig),
valueFlags: map[string]struct{}{}, cfg: pCfg,
}
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 parser=%+#v", parser)
@ -83,12 +81,12 @@ func (p *Parser) Parse() (*Argh, error) {
p.nodes = []Node{} p.nodes = []Node{}
for { for {
pd, err := p.parseArg() br, err := p.parseArg()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if pd != nil && pd.Break { if br {
break break
} }
} }
@ -96,31 +94,31 @@ func (p *Parser) Parse() (*Argh, error) {
return &Argh{ParseTree: &ParseTree{Nodes: p.nodes}}, nil 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() tok, lit, pos := p.scan()
if tok == ILLEGAL { 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 { if tok == EOL {
return &parseDirective{Break: true}, nil return true, nil
} }
p.unscan() p.unscan(tok, lit, pos)
node, err := p.nodify() node, err := p.nodify()
tracef("parseArg node=%+#v err=%+#v", node, err) tracef("parseArg node=%+#v err=%+#v", node, err)
if err != nil { 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 { if node != nil {
p.nodes = append(p.nodes, node) p.nodes = append(p.nodes, node)
} }
return nil, nil return false, nil
} }
func (p *Parser) nodify() (Node, error) { func (p *Parser) nodify() (Node, error) {
@ -131,11 +129,21 @@ func (p *Parser) nodify() (Node, error) {
switch tok { switch tok {
case IDENT: case IDENT:
if len(p.nodes) == 0 { 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 _, ok := p.commands[lit]; ok { if n, ok := p.cfg.Commands[lit]; ok {
return Command{Name: lit}, nil 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 return Ident{Literal: lit}, nil
@ -144,7 +152,24 @@ func (p *Parser) nodify() (Node, error) {
case COMPOUND_SHORT_FLAG: case COMPOUND_SHORT_FLAG:
flagNodes := []Node{} 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 = append(
flagNodes, flagNodes,
Flag{ Flag{
@ -153,18 +178,28 @@ func (p *Parser) nodify() (Node, error) {
) )
} }
return Statement{Nodes: flagNodes}, nil return CompoundShortFlag{Nodes: flagNodes}, nil
case SHORT_FLAG: case SHORT_FLAG:
flagName := string(lit[1:]) flagName := string(lit[1:])
if _, ok := p.valueFlags[flagName]; ok { if n, ok := p.cfg.Flags[flagName]; ok {
return p.scanValueFlag(flagName, pos) 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 return Flag{Name: flagName}, nil
case LONG_FLAG: case LONG_FLAG:
flagName := string(lit[2:]) flagName := string(lit[2:])
if _, ok := p.valueFlags[flagName]; ok { if n, ok := p.cfg.Flags[flagName]; ok {
return p.scanValueFlag(flagName, pos) 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 return Flag{Name: flagName}, nil
@ -174,24 +209,57 @@ func (p *Parser) nodify() (Node, error) {
return Ident{Literal: lit}, nil return Ident{Literal: lit}, nil
} }
func (p *Parser) scanValueFlag(flagName string, pos int) (Node, error) { func (p *Parser) scanValues(lit string, pos int, n NValue) ([]string, error) {
tracef("scanValueFlag flagName=%q pos=%v", flagName, pos) tracef("scanValues lit=%q pos=%v n=%v", lit, 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 { if err != nil {
return nil, err return nil, err
} }
return Flag{Name: flagName, Value: ptr(lit)}, nil if len(values) == 0 {
return nil, nil
}
return values, nil
} }
func (p *Parser) scanIdent() (string, error) { func (p *Parser) scanIdent() (string, error) {
tok, lit, pos := p.scan() tok, lit, pos := p.scan()
nUnscan := 0 unscanBuf := []ScanEntry{}
if tok == ASSIGN || tok == ARG_DELIMITER { if tok == ASSIGN || tok == ARG_DELIMITER {
nUnscan++ unscanBuf = append([]ScanEntry{{tok: tok, lit: lit, pos: pos}}, unscanBuf...)
tok, lit, pos = p.scan() tok, lit, pos = p.scan()
} }
@ -199,30 +267,35 @@ func (p *Parser) scanIdent() (string, error) {
return lit, nil return lit, nil
} }
for i := 0; i < nUnscan; i++ { unscanBuf = append([]ScanEntry{{tok: tok, lit: lit, pos: pos}}, unscanBuf...)
p.unscan()
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) { func (p *Parser) scan() (Token, string, int) {
if p.buf.n != 0 { if len(p.buf) != 0 {
p.buf.n = 0 entry, buf := p.buf[len(p.buf)-1], p.buf[:len(p.buf)-1]
return p.buf.tok, p.buf.lit, p.buf.pos 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() 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 return tok, lit, pos
} }
func (p *Parser) unscan() { func (p *Parser) unscan(tok Token, lit string, pos int) {
p.buf.n = 1 entry := ScanEntry{tok: tok, lit: lit, pos: pos}
}
func ptr[T any](v T) *T { tracef("unscan entry=%s %+#v", tok, entry)
return &v
p.buf = append(p.buf, entry)
} }

View File

@ -31,6 +31,32 @@ func TestParser(t *testing.T) {
argh.Program{Name: "pizzas"}, 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", name: "long value-less flags",
args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"}, args: []string{"pizzas", "--tasty", "--fresh", "--super-hot-right-now"},
@ -52,25 +78,41 @@ func TestParser(t *testing.T) {
}, },
{ {
name: "long flags mixed", 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{ cfg: &argh.ParserConfig{
Commands: []string{}, Commands: map[string]argh.NValue{},
ValueFlags: []string{"fresh"}, Flags: map[string]argh.NValue{
"fresh": argh.OneValue,
"box": argh.OneOrMoreValue,
},
}, },
expPT: []argh.Node{ expPT: []argh.Node{
argh.Program{Name: "pizzas"}, argh.Program{Name: "pizzas"},
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Flag{Name: "tasty"}, argh.Flag{Name: "tasty"},
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Flag{Name: "fresh", Value: ptr("soon")}, argh.Flag{Name: "fresh", Values: []string{"soon"}},
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Flag{Name: "super-hot-right-now"}, 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{ expAST: []argh.Node{
argh.Program{Name: "pizzas"}, argh.Program{Name: "pizzas"},
argh.Flag{Name: "tasty"}, 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: "super-hot-right-now"},
argh.Flag{Name: "box", Values: []string{"square", "shaped", "hot"}},
argh.Flag{Name: "please"},
}, },
}, },
{ {
@ -98,7 +140,7 @@ func TestParser(t *testing.T) {
expPT: []argh.Node{ expPT: []argh.Node{
argh.Program{Name: "pizzas"}, argh.Program{Name: "pizzas"},
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Statement{ argh.CompoundShortFlag{
Nodes: []argh.Node{ Nodes: []argh.Node{
argh.Flag{Name: "a"}, argh.Flag{Name: "a"},
argh.Flag{Name: "c"}, argh.Flag{Name: "c"},
@ -106,7 +148,7 @@ func TestParser(t *testing.T) {
}, },
}, },
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Statement{ argh.CompoundShortFlag{
Nodes: []argh.Node{ Nodes: []argh.Node{
argh.Flag{Name: "b"}, argh.Flag{Name: "b"},
argh.Flag{Name: "l"}, argh.Flag{Name: "l"},
@ -130,8 +172,8 @@ func TestParser(t *testing.T) {
name: "mixed long short value flags", name: "mixed long short value flags",
args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"}, args: []string{"pizzas", "-a", "--ca", "-b", "1312", "-lol"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Commands: []string{}, Commands: map[string]argh.NValue{},
ValueFlags: []string{"b"}, Flags: map[string]argh.NValue{"b": argh.OneValue},
}, },
expPT: []argh.Node{ expPT: []argh.Node{
argh.Program{Name: "pizzas"}, argh.Program{Name: "pizzas"},
@ -140,9 +182,9 @@ func TestParser(t *testing.T) {
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Flag{Name: "ca"}, argh.Flag{Name: "ca"},
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Flag{Name: "b", Value: ptr("1312")}, argh.Flag{Name: "b", Values: []string{"1312"}},
argh.ArgDelimiter{}, argh.ArgDelimiter{},
argh.Statement{ argh.CompoundShortFlag{
Nodes: []argh.Node{ Nodes: []argh.Node{
argh.Flag{Name: "l"}, argh.Flag{Name: "l"},
argh.Flag{Name: "o"}, argh.Flag{Name: "o"},
@ -154,7 +196,7 @@ func TestParser(t *testing.T) {
argh.Program{Name: "pizzas"}, argh.Program{Name: "pizzas"},
argh.Flag{Name: "a"}, argh.Flag{Name: "a"},
argh.Flag{Name: "ca"}, 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: "l"},
argh.Flag{Name: "o"}, argh.Flag{Name: "o"},
argh.Flag{Name: "l"}, argh.Flag{Name: "l"},
@ -164,8 +206,8 @@ func TestParser(t *testing.T) {
name: "commands", name: "commands",
args: []string{"pizzas", "fly", "fry"}, args: []string{"pizzas", "fly", "fry"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Commands: []string{"fly", "fry"}, Commands: map[string]argh.NValue{"fly": argh.ZeroValue, "fry": argh.ZeroValue},
ValueFlags: []string{}, Flags: map[string]argh.NValue{},
}, },
expPT: []argh.Node{ expPT: []argh.Node{
argh.Program{Name: "pizzas"}, argh.Program{Name: "pizzas"},
@ -175,6 +217,42 @@ func TestParser(t *testing.T) {
argh.Command{Name: "fry"}, 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 { if tc.skip {
continue continue