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/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 new file mode 100644 index 0000000..47f3ca1 --- /dev/null +++ b/argh/argh.go @@ -0,0 +1,34 @@ +package argh + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime" +) + +var ( + tracingEnabled = os.Getenv("ARGH_TRACING") == "enabled" + traceLogger *log.Logger +) + +func init() { + if !tracingEnabled { + return + } + + traceLogger = log.New(os.Stderr, "ARGH TRACING: ", 0) +} + +func tracef(format string, v ...any) { + if !tracingEnabled { + return + } + + if _, file, line, ok := runtime.Caller(1); ok { + format = fmt.Sprintf("%v:%v ", filepath.Base(file), line) + format + } + + traceLogger.Printf(format, v...) +} diff --git a/argh/cmd/argh-example/main.go b/argh/cmd/argh-example/main.go new file mode 100644 index 0000000..325c7ec --- /dev/null +++ b/argh/cmd/argh-example/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "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) + + 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) + } + + ast := argh.NewQuerier(pt.Nodes).AST() + + if asJSON { + b, err := json.MarshalIndent(ast, "", " ") + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(b)) + + return + } + + spew.Dump(ast) +} diff --git a/argh/go.mod b/argh/go.mod new file mode 100644 index 0000000..7091c9a --- /dev/null +++ b/argh/go.mod @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..842edf5 --- /dev/null +++ b/argh/go.sum @@ -0,0 +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/node.go b/argh/node.go new file mode 100644 index 0000000..b4ce2e9 --- /dev/null +++ b/argh/node.go @@ -0,0 +1,46 @@ +package argh + +type Node interface{} + +type TypedNode struct { + Type string + Node Node +} + +type PassthroughArgs struct { + Nodes []Node +} + +type CompoundShortFlag struct { + Nodes []Node +} + +type Ident struct { + Literal string +} + +type BadArg struct { + Literal string + From Pos + To Pos +} + +type Command struct { + Name string + Values map[string]string + Nodes []Node +} + +type Flag struct { + Name string + Values map[string]string + Nodes []Node +} + +type StdinFlag struct{} + +type StopFlag struct{} + +type ArgDelimiter struct{} + +type Assign struct{} diff --git a/argh/nvalue_string.go b/argh/nvalue_string.go new file mode 100644 index 0000000..d9d1f06 --- /dev/null +++ b/argh/nvalue_string.go @@ -0,0 +1,26 @@ +// 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[OneOrMoreValue - -2] + _ = x[ZeroOrMoreValue - -1] + _ = x[ZeroValue-0] +} + +const _NValue_name = "OneOrMoreValueZeroOrMoreValueZeroValue" + +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+-2), 10) + ")" + } + return _NValue_name[_NValue_index[i]:_NValue_index[i+1]] +} diff --git a/argh/parser.go b/argh/parser.go new file mode 100644 index 0000000..72c15d9 --- /dev/null +++ b/argh/parser.go @@ -0,0 +1,346 @@ +package argh + +import ( + "fmt" + "io" + "strings" +) + +type parser struct { + s *Scanner + + cfg *ParserConfig + + errors ParserErrorList + + tok Token + lit string + pos Pos + + buffered bool +} + +type ParseTree struct { + Nodes []Node `json:"nodes"` +} + +func ParseArgs(args []string, pCfg *ParserConfig) (*ParseTree, error) { + p := &parser{} + p.init( + strings.NewReader(strings.Join(args, string(nul))), + pCfg, + ) + + tracef("ParseArgs(...) parser=%+#v", p) + + 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 = ParserErrorList{} + + if pCfg == nil { + pCfg = POSIXyParserConfig + } + + p.cfg = pCfg + + p.s = NewScanner(r, pCfg.ScannerConfig) + + p.next() +} + +func (p *parser) 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 *parser) 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 *parser) 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) + + tracef("parseCommand(...) cCfg=%+#v", cCfg) + + if subCfg, ok := cCfg.GetCommandConfig(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.addError("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 *parser) parseIdent() Node { + node := &Ident{Literal: p.lit} + return node +} + +func (p *parser) parseFlag(flags *Flags) Node { + switch p.tok { + case SHORT_FLAG: + tracef("parseFlag(...) parsing short flag with config=%+#v", flags) + return p.parseShortFlag(flags) + case LONG_FLAG: + 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", flags) + return p.parseCompoundShortFlag(flags) + } + + panic(fmt.Sprintf("token %v cannot be parsed as flag", p.tok)) +} + +func (p *parser) parseShortFlag(flags *Flags) Node { + node := &Flag{Name: string(p.lit[1])} + + flCfg, ok := flags.Get(node.Name) + if !ok { + p.addError(fmt.Sprintf("unknown flag %q", node.Name)) + + return node + } + + return p.parseConfiguredFlag(node, flCfg) +} + +func (p *parser) parseLongFlag(flags *Flags) Node { + node := &Flag{Name: string(p.lit[2:])} + + flCfg, ok := flags.Get(node.Name) + if !ok { + p.addError(fmt.Sprintf("unknown flag %q", node.Name)) + + return node + } + + return p.parseConfiguredFlag(node, flCfg) +} + +func (p *parser) parseCompoundShortFlag(flags *Flags) Node { + flagNodes := []Node{} + + withoutFlagPrefix := p.lit[1:] + + for i, r := range withoutFlagPrefix { + node := &Flag{Name: string(r)} + + if i == len(withoutFlagPrefix)-1 { + flCfg, ok := flags.Get(node.Name) + if !ok { + p.addError(fmt.Sprintf("unknown flag %q", node.Name)) + + continue + } + + flagNodes = append(flagNodes, p.parseConfiguredFlag(node, flCfg)) + + continue + } + + flagNodes = append(flagNodes, node) + } + + return &CompoundShortFlag{Nodes: flagNodes} +} + +func (p *parser) 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 *parser) 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/parser_config.go b/argh/parser_config.go new file mode 100644 index 0000000..62ea57d --- /dev/null +++ b/argh/parser_config.go @@ -0,0 +1,146 @@ +package argh + +const ( + OneOrMoreValue NValue = -2 + ZeroOrMoreValue NValue = -1 + ZeroValue NValue = 0 +) + +var ( + POSIXyParserConfig = NewParserConfig( + nil, + POSIXyScannerConfig, + ) +) + +type NValue int + +func (nv NValue) Contains(i int) bool { + tracef("NValue.Contains(%v)", i) + + if i < int(ZeroValue) { + return false + } + + if nv == OneOrMoreValue || nv == ZeroOrMoreValue { + return true + } + + return int(nv) > i +} + +type ParserConfig struct { + Prog CommandConfig + + 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 + Flags *Flags + 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) + + if cCfg.Commands == nil { + cCfg.Commands = &Commands{Map: map[string]CommandConfig{}} + } + + return cCfg.Commands.Get(name) +} + +func (cCfg *CommandConfig) GetFlagConfig(name string) (FlagConfig, bool) { + tracef("CommandConfig.GetFlagConfig(%q)", name) + + 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 + + Automatic bool +} + +func (fl *Flags) Get(name string) (FlagConfig, bool) { + tracef("Flags.Get(%q)", name) + + if fl.Map == nil { + fl.Map = map[string]FlagConfig{} + } + + flCfg, ok := fl.Map[name] + 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 +} + +type Commands struct { + Map map[string]CommandConfig +} + +func (cmd *Commands) Get(name string) (CommandConfig, bool) { + tracef("Commands.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_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 new file mode 100644 index 0000000..bea803f --- /dev/null +++ b/argh/parser_test.go @@ -0,0 +1,1028 @@ +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 TestParser(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{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "e": {}, + "a": {}, + "t": {}, + "wat": {}, + }, + }, + Commands: &argh.Commands{ + Map: 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: "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"}, + 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"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "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: &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": {}, + }, + }, + }, + }, + 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"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "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"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "a": {}, + "b": {}, + "c": {}, + "l": {}, + "o": {}, + }, + }, + }, + }, + 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: &argh.Commands{Map: map[string]argh.CommandConfig{}}, + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "a": {}, + "b": argh.FlagConfig{NValue: 1}, + "ca": {}, + "l": {}, + "o": {}, + }, + }, + }, + }, + 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: &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: &argh.Flags{Map: 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: &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}, + }, + }, + }, + }, + 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: &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: &argh.Flags{Map: 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: &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: &argh.Flags{ + Map: 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: "windows like", + args: []string{"hotdog", "/f", "/L", "/o:ppy", "hats"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "f": {}, + "L": {}, + "o": argh.FlagConfig{NValue: 1}, + }, + }, + Commands: &argh.Commands{ + Map: 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"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "wat": {}, + }, + }, + }, + }, + expErr: argh.ParserErrorList{ + &argh.ParserError{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.ParseArgs(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.ParseArgs(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/querier.go b/argh/querier.go new file mode 100644 index 0000000..2d1ca54 --- /dev/null +++ b/argh/querier.go @@ -0,0 +1,94 @@ +package argh + +type Querier interface { + Program() (*Command, bool) + AST() []Node +} + +func NewQuerier(nodes []Node) Querier { + return &defaultQuerier{nodes: nodes} +} + +type defaultQuerier struct { + nodes []Node +} + +func (dq *defaultQuerier) Program() (*Command, bool) { + if len(dq.nodes) == 0 { + 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 + } + + return v, ok +} + +func (dq *defaultQuerier) AST() []Node { + ret := []Node{} + + 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 { + continue + } + + if v, ok := node.(*CompoundShortFlag); ok { + 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: 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 + } + + ret = append(ret, node) + } + + return ret +} diff --git a/argh/querier_test.go b/argh/querier_test.go new file mode 100644 index 0000000..962d869 --- /dev/null +++ b/argh/querier_test.go @@ -0,0 +1,60 @@ +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 string + expOK bool + }{ + { + name: "typical", + args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, + cfg: &argh.ParserConfig{ + Prog: argh.CommandConfig{ + Commands: &argh.Commands{ + Map: map[string]argh.CommandConfig{ + "ahoy": argh.CommandConfig{ + Flags: &argh.Flags{ + Map: map[string]argh.FlagConfig{ + "treatsa": argh.FlagConfig{NValue: 1}, + }, + }, + }, + }, + }, + }, + }, + exp: "pizzas", + expOK: true, + }, + { + name: "minimal", + args: []string{"pizzas"}, + exp: "pizzas", + expOK: true, + }, + { + name: "invalid", + args: []string{}, + 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.Nodes).Program() + require.Equal(ct, tc.expOK, ok) + require.Equal(ct, tc.exp, prog.Name) + }) + } +} diff --git a/argh/scanner.go b/argh/scanner.go new file mode 100644 index 0000000..3cc8d45 --- /dev/null +++ b/argh/scanner.go @@ -0,0 +1,159 @@ +package argh + +import ( + "bufio" + "bytes" + "errors" + "io" + "log" + "unicode" +) + +type Scanner struct { + r *bufio.Reader + i int + cfg *ScannerConfig +} + +func NewScanner(r io.Reader, cfg *ScannerConfig) *Scanner { + if cfg == nil { + cfg = POSIXyScannerConfig + } + + return &Scanner{ + r: bufio.NewReader(r), + cfg: cfg, + } +} + +func (s *Scanner) Scan() (Token, string, Pos) { + ch, pos := s.read() + + if s.cfg.IsBlankspace(ch) { + _ = s.unread() + return s.scanBlankspace() + } + + if s.cfg.IsAssignmentOperator(ch) { + return ASSIGN, string(ch), pos + } + + if s.cfg.IsMultiValueDelim(ch) { + return MULTI_VALUE_DELIMITER, string(ch), pos + } + + if ch == eol { + return EOL, "", pos + } + + if ch == nul { + return ARG_DELIMITER, string(ch), pos + } + + if unicode.IsGraphic(ch) { + _ = s.unread() + return s.scanArg() + } + + return ILLEGAL, string(ch), pos +} + +func (s *Scanner) read() (rune, Pos) { + ch, _, err := s.r.ReadRune() + s.i++ + + if errors.Is(err, io.EOF) { + return eol, Pos(s.i) + } else if err != nil { + log.Printf("unknown scanner error=%+v", err) + return eol, Pos(s.i) + } + + return ch, Pos(s.i) +} + +func (s *Scanner) unread() Pos { + _ = s.r.UnreadRune() + s.i-- + return Pos(s.i) +} + +func (s *Scanner) scanBlankspace() (Token, string, Pos) { + buf := &bytes.Buffer{} + ch, pos := s.read() + buf.WriteRune(ch) + + for { + ch, pos = s.read() + + if ch == eol { + break + } else if !s.cfg.IsBlankspace(ch) { + pos = s.unread() + break + } else { + _, _ = buf.WriteRune(ch) + } + } + + return BS, buf.String(), pos +} + +func (s *Scanner) scanArg() (Token, string, Pos) { + buf := &bytes.Buffer{} + ch, pos := s.read() + buf.WriteRune(ch) + + for { + ch, pos = s.read() + + if ch == eol || ch == nul || s.cfg.IsAssignmentOperator(ch) || s.cfg.IsMultiValueDelim(ch) { + pos = s.unread() + break + } + + _, _ = buf.WriteRune(ch) + } + + str := buf.String() + + if len(str) == 0 { + return EMPTY, str, pos + } + + ch0 := rune(str[0]) + + if len(str) == 1 { + if s.cfg.IsFlagPrefix(ch0) { + return STDIN_FLAG, str, pos + } + + if s.cfg.IsAssignmentOperator(ch0) { + return ASSIGN, str, pos + } + + return IDENT, str, pos + } + + ch1 := rune(str[1]) + + if len(str) == 2 { + if s.cfg.IsFlagPrefix(ch0) && s.cfg.IsFlagPrefix(ch1) { + return STOP_FLAG, str, pos + } + + if s.cfg.IsFlagPrefix(ch0) { + return SHORT_FLAG, str, pos + } + } + + if s.cfg.IsFlagPrefix(ch0) { + if s.cfg.IsFlagPrefix(ch1) { + return LONG_FLAG, str, pos + } + + return COMPOUND_SHORT_FLAG, str, pos + } + + return IDENT, 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/token.go b/argh/token.go new file mode 100644 index 0000000..b26b8b7 --- /dev/null +++ b/argh/token.go @@ -0,0 +1,53 @@ +//go:generate stringer -type Token + +package argh + +import "fmt" + +const ( + ILLEGAL Token = iota + EOL + 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 // '--' + + nul = rune(0) + eol = rune(-1) +) + +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 new file mode 100644 index 0000000..ff6a07e --- /dev/null +++ b/argh/token_string.go @@ -0,0 +1,35 @@ +// 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[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_DELIMITERASSIGNMULTI_VALUE_DELIMITERLONG_FLAGSHORT_FLAGCOMPOUND_SHORT_FLAGSTDIN_FLAGSTOP_FLAG" + +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) { + return "Token(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Token_name[_Token_index[i]:_Token_index[i+1]] +}