Compare commits

4 Commits

Author SHA1 Message Date
206e6d6110 Declare correct module name 2022-07-10 17:55:47 -04:00
f1c090f4cb A bit more coverage + say things in README 2022-07-10 17:36:13 -04:00
30449ba506 Pointers and nils again 2022-06-07 09:47:12 -04:00
d0c96803c8 Reduce required args to new parser 2022-06-06 19:06:50 -04:00
9 changed files with 159 additions and 48 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
*.a
*.exe
*.o
*.out
*.so

View File

@@ -1,4 +1,26 @@
# argh command line parser # argh command line parser
> NOTE: much of this is lifted from ## background
> https://blog.gopheracademy.com/advent-2014/parsers-lexers/
The Go standard library [flag](https://pkg.go.dev/flag) way of doing things has long been
a source of frustration while implementing and maintaining the
[urfave/cli](https://github.com/urfave/cli) library. [Many alternate parsers
exist](https://github.com/avelino/awesome-go#standard-cli), including:
- [pflag](https://github.com/spf13/pflag)
- [argparse](https://github.com/akamensky/argparse)
In addition to these other implementations, I also got some help via [this
oldie](https://blog.gopheracademy.com/advent-2014/parsers-lexers/) and the Go standard
library [parser](https://pkg.go.dev/go/parser).
## goals
- get a better understanding of the whole problem space
- support both POSIX-y and Windows-y styles
- build a printable/JSON-able parse tree
- support rich error reporting
<!--
vim:tw=90
-->

View File

@@ -6,7 +6,7 @@ import (
"log" "log"
"os" "os"
"git.meatballhat.com/x/box-o-sand/argh" "git.meatballhat.com/x/argh"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
) )
@@ -15,16 +15,16 @@ func main() {
log.SetFlags(0) log.SetFlags(0)
pt, err := argh.ParseArgs(os.Args, argh.NewParserConfig( pCfg := argh.NewParserConfig()
&argh.CommandConfig{ pCfg.Prog = &argh.CommandConfig{
NValue: argh.OneOrMoreValue, NValue: argh.OneOrMoreValue,
ValueNames: []string{"topping"}, ValueNames: []string{"val"},
Flags: &argh.Flags{ Flags: &argh.Flags{
Automatic: true, Automatic: true,
},
}, },
nil, }
))
pt, err := argh.ParseArgs(os.Args, pCfg)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

2
go.mod
View File

@@ -1,4 +1,4 @@
module git.meatballhat.com/x/box-o-sand/argh module git.meatballhat.com/x/argh
go 1.18 go 1.18

View File

@@ -61,7 +61,7 @@ func (p *parser) parseArgs() (*ParseTree, error) {
} }
tracef("parseArgs() parsing %q as program command; cfg=%+#v", p.lit, p.cfg.Prog) tracef("parseArgs() parsing %q as program command; cfg=%+#v", p.lit, p.cfg.Prog)
prog := p.parseCommand(&p.cfg.Prog) prog := p.parseCommand(p.cfg.Prog)
tracef("parseArgs() top level node is %T", prog) tracef("parseArgs() top level node is %T", prog)

View File

@@ -7,10 +7,7 @@ const (
) )
var ( var (
POSIXyParserConfig = NewParserConfig( POSIXyParserConfig = NewParserConfig()
nil,
POSIXyScannerConfig,
)
) )
type NValue int type NValue int
@@ -30,25 +27,29 @@ func (nv NValue) Contains(i int) bool {
} }
type ParserConfig struct { type ParserConfig struct {
Prog CommandConfig Prog *CommandConfig
ScannerConfig *ScannerConfig ScannerConfig *ScannerConfig
} }
func NewParserConfig(prog *CommandConfig, sCfg *ScannerConfig) *ParserConfig { type ParserOption func(*ParserConfig)
if sCfg == nil {
sCfg = POSIXyScannerConfig func NewParserConfig(opts ...ParserOption) *ParserConfig {
pCfg := &ParserConfig{}
for _, opt := range opts {
if opt != nil {
opt(pCfg)
}
} }
if prog == nil { if pCfg.Prog == nil {
prog = &CommandConfig{} pCfg.Prog = &CommandConfig{}
pCfg.Prog.init()
} }
prog.init() if pCfg.ScannerConfig == nil {
pCfg.ScannerConfig = POSIXyScannerConfig
pCfg := &ParserConfig{
Prog: *prog,
ScannerConfig: sCfg,
} }
return pCfg return pCfg

View File

@@ -3,7 +3,7 @@ package argh_test
import ( import (
"testing" "testing"
"git.meatballhat.com/x/box-o-sand/argh" "git.meatballhat.com/x/argh"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -24,7 +24,7 @@ func TestParser(t *testing.T) {
"pies", "-eat", "--wat", "hello", "mario", "pies", "-eat", "--wat", "hello", "mario",
}, },
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"e": {}, "e": {},
@@ -98,8 +98,8 @@ func TestParser(t *testing.T) {
"pies", "--wat", "hello", "mario", "-eat", "pies", "--wat", "hello", "mario", "-eat",
}, },
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: func() argh.CommandConfig { Prog: func() *argh.CommandConfig {
cmdCfg := argh.CommandConfig{ cmdCfg := &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"e": {Persist: true}, "e": {Persist: true},
@@ -193,7 +193,7 @@ func TestParser(t *testing.T) {
name: "one positional arg", name: "one positional arg",
args: []string{"pizzas", "excel"}, args: []string{"pizzas", "excel"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{NValue: 1}, Prog: &argh.CommandConfig{NValue: 1},
}, },
expPT: []argh.Node{ expPT: []argh.Node{
&argh.Command{ &argh.Command{
@@ -219,7 +219,7 @@ func TestParser(t *testing.T) {
name: "many positional args", name: "many positional args",
args: []string{"pizzas", "excel", "wildly", "when", "feral"}, args: []string{"pizzas", "excel", "wildly", "when", "feral"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
NValue: argh.OneOrMoreValue, NValue: argh.OneOrMoreValue,
ValueNames: []string{"word"}, ValueNames: []string{"word"},
}, },
@@ -267,7 +267,7 @@ func TestParser(t *testing.T) {
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"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"tasty": {}, "tasty": {},
@@ -312,7 +312,7 @@ func TestParser(t *testing.T) {
"--please", "--please",
}, },
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Commands: &argh.Commands{Map: map[string]argh.CommandConfig{}}, Commands: &argh.Commands{Map: map[string]argh.CommandConfig{}},
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
@@ -391,7 +391,7 @@ func TestParser(t *testing.T) {
name: "short value-less flags", name: "short value-less flags",
args: []string{"pizzas", "-t", "-f", "-s"}, args: []string{"pizzas", "-t", "-f", "-s"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"t": {}, "t": {},
@@ -429,7 +429,7 @@ func TestParser(t *testing.T) {
name: "compound short flags", name: "compound short flags",
args: []string{"pizzas", "-aca", "-blol"}, args: []string{"pizzas", "-aca", "-blol"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"a": {}, "a": {},
@@ -484,7 +484,7 @@ 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{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Commands: &argh.Commands{Map: map[string]argh.CommandConfig{}}, Commands: &argh.Commands{Map: map[string]argh.CommandConfig{}},
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
@@ -549,7 +549,7 @@ func TestParser(t *testing.T) {
name: "nested commands with positional args", name: "nested commands with positional args",
args: []string{"pizzas", "fly", "freely", "sometimes", "and", "other", "times", "fry", "deeply", "--forever"}, args: []string{"pizzas", "fly", "freely", "sometimes", "and", "other", "times", "fry", "deeply", "--forever"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Commands: &argh.Commands{ Commands: &argh.Commands{
Map: map[string]argh.CommandConfig{ Map: map[string]argh.CommandConfig{
"fly": argh.CommandConfig{ "fly": argh.CommandConfig{
@@ -632,7 +632,7 @@ func TestParser(t *testing.T) {
name: "compound flags with values", name: "compound flags with values",
args: []string{"pizzas", "-need", "sauce", "heat", "love", "-also", "over9000"}, args: []string{"pizzas", "-need", "sauce", "heat", "love", "-also", "over9000"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"a": {NValue: argh.ZeroOrMoreValue}, "a": {NValue: argh.ZeroOrMoreValue},
@@ -735,7 +735,7 @@ func TestParser(t *testing.T) {
name: "command specific flags", name: "command specific flags",
args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt", "hugs"}, args: []string{"pizzas", "fly", "--freely", "fry", "--deeply", "-wAt", "hugs"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Commands: &argh.Commands{ Commands: &argh.Commands{
Map: map[string]argh.CommandConfig{ Map: map[string]argh.CommandConfig{
"fly": argh.CommandConfig{ "fly": argh.CommandConfig{
@@ -835,7 +835,7 @@ func TestParser(t *testing.T) {
name: "total weirdo", name: "total weirdo",
args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"}, args: []string{"PIZZAs", "^wAT@golf", "^^hecKing", "goose", "bonk", "^^FIERCENESS@-2"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Commands: &argh.Commands{ Commands: &argh.Commands{
Map: map[string]argh.CommandConfig{ Map: map[string]argh.CommandConfig{
"goose": argh.CommandConfig{ "goose": argh.CommandConfig{
@@ -910,7 +910,7 @@ func TestParser(t *testing.T) {
name: "windows like", name: "windows like",
args: []string{"hotdog", "/f", "/L", "/o:ppy", "hats"}, args: []string{"hotdog", "/f", "/L", "/o:ppy", "hats"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"f": {}, "f": {},
@@ -957,7 +957,7 @@ func TestParser(t *testing.T) {
name: "invalid bare assignment", name: "invalid bare assignment",
args: []string{"pizzas", "=", "--wat"}, args: []string{"pizzas", "=", "--wat"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Flags: &argh.Flags{ Flags: &argh.Flags{
Map: map[string]argh.FlagConfig{ Map: map[string]argh.FlagConfig{
"wat": {}, "wat": {},

View File

@@ -3,7 +3,7 @@ package argh_test
import ( import (
"testing" "testing"
"git.meatballhat.com/x/box-o-sand/argh" "git.meatballhat.com/x/argh"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -19,7 +19,7 @@ func TestQuerier_Program(t *testing.T) {
name: "typical", name: "typical",
args: []string{"pizzas", "ahoy", "--treatsa", "fun"}, args: []string{"pizzas", "ahoy", "--treatsa", "fun"},
cfg: &argh.ParserConfig{ cfg: &argh.ParserConfig{
Prog: argh.CommandConfig{ Prog: &argh.CommandConfig{
Commands: &argh.Commands{ Commands: &argh.Commands{
Map: map[string]argh.CommandConfig{ Map: map[string]argh.CommandConfig{
"ahoy": argh.CommandConfig{ "ahoy": argh.CommandConfig{

83
scanner_test.go Normal file
View File

@@ -0,0 +1,83 @@
package argh
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func BenchmarkScannerPOSIXyScannerScan(b *testing.B) {
for i := 0; i < b.N; i++ {
scanner := NewScanner(strings.NewReader(strings.Join([]string{
"walrus",
"-what",
"--ball=awesome",
"--elapsed",
"carrot cake",
}, string(nul))), nil)
for {
tok, _, _ := scanner.Scan()
if tok == EOL {
break
}
}
}
}
func TestScannerPOSIXyScanner(t *testing.T) {
for _, tc := range []struct {
name string
argv []string
expectedTokens []Token
expectedLiterals []string
expectedPositions []Pos
}{
{
name: "simple",
argv: []string{"walrus", "-cake", "--corn-dog", "awkward"},
expectedTokens: []Token{
IDENT,
ARG_DELIMITER,
COMPOUND_SHORT_FLAG,
ARG_DELIMITER,
LONG_FLAG,
ARG_DELIMITER,
IDENT,
EOL,
},
expectedLiterals: []string{
"walrus", string(nul), "-cake", string(nul), "--corn-dog", string(nul), "awkward", "",
},
expectedPositions: []Pos{
6, 7, 12, 13, 23, 24, 31, 32,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
r := require.New(t)
scanner := NewScanner(strings.NewReader(strings.Join(tc.argv, string(nul))), nil)
actualTokens := []Token{}
actualLiterals := []string{}
actualPositions := []Pos{}
for {
tok, lit, pos := scanner.Scan()
actualTokens = append(actualTokens, tok)
actualLiterals = append(actualLiterals, lit)
actualPositions = append(actualPositions, pos)
if tok == EOL {
break
}
}
r.Equal(tc.expectedTokens, actualTokens)
r.Equal(tc.expectedLiterals, actualLiterals)
r.Equal(tc.expectedPositions, actualPositions)
})
}
}