From c0ae6d8588043ab3abc5c77b0917851c9a86eb27 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 22 May 2022 20:49:11 -0400 Subject: [PATCH] Continuing the work with parser that's more like go/parser --- node.go | 6 - parser.go | 10 +- parser2.go | 126 +++++++------ parser2_test.go | 463 ++++++++++++++++++++++++++++++++++++++++++++-- parser_config.go | 23 ++- parser_test.go | 136 +++++++------- querier.go | 30 +-- querier_test.go | 12 +- scanner.go | 133 ++----------- scanner_config.go | 39 ++++ scanner_error.go | 75 ++++++++ token.go | 3 + 12 files changed, 766 insertions(+), 290 deletions(-) create mode 100644 scanner_config.go create mode 100644 scanner_error.go diff --git a/node.go b/node.go index f5bddf0..c8fccda 100644 --- a/node.go +++ b/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/parser.go b/parser.go index 713356a..f9ec92a 100644 --- a/parser.go +++ b/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/parser2.go b/parser2.go index f428c5d..1efb242 100644 --- a/parser2.go +++ b/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/parser2_test.go b/parser2_test.go index 7679d03..169c4c3 100644 --- a/parser2_test.go +++ b/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/parser_config.go b/parser_config.go index 6b735db..62b209e 100644 --- a/parser_config.go +++ b/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/parser_test.go b/parser_test.go index 9cb5bb5..5f89369 100644 --- a/parser_test.go +++ b/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/querier.go b/querier.go index 27e2b9c..a64f075 100644 --- a/querier.go +++ b/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/querier_test.go b/querier_test.go index d37c7e1..f335e6e 100644 --- a/querier_test.go +++ b/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/scanner.go b/scanner.go index c073ad0..3cc8d45 100644 --- a/scanner.go +++ b/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/scanner_config.go b/scanner_config.go new file mode 100644 index 0000000..9bc9be2 --- /dev/null +++ b/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/scanner_error.go b/scanner_error.go new file mode 100644 index 0000000..c9b7441 --- /dev/null +++ b/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/token.go b/token.go index 9e70b81..b26b8b7 100644 --- a/token.go +++ b/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