Skip to content

Commit

Permalink
parse/prompt: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gobwas committed Jun 30, 2020
1 parent 1c95a0f commit c7c9dd6
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ At the moment these parsers are already implemented:
- [Flag][flag-syntax] syntax arguments parser
- [Posix][posix] program arguments syntax parser
- Environment variables parser
- Prompt interactive parser
- File parsers:
- json
- yaml
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.13

require (
github.com/BurntSushi/toml v0.3.1
github.com/gobwas/prompt v0.0.0-20200614125756-3a711fd8dd13
github.com/gobwas/prompt v0.2.2
github.com/google/go-cmp v0.4.0
gopkg.in/yaml.v2 v2.2.8
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/gobwas/prompt v0.0.0-20200614125756-3a711fd8dd13 h1:uSZXv0wHIxaPzmgy0RwoQkMz5/nK75fde/7dJABpXiI=
github.com/gobwas/prompt v0.0.0-20200614125756-3a711fd8dd13/go.mod h1:eDSc3qYGXAHZ1Yyl68L9uaLvqya9+LIyeHkxS8+Tz9E=
github.com/gobwas/prompt v0.2.2 h1:ri/sgOnLzXAwm2nUI7/jM0kJ24MVMob/01+n5OaZmJ8=
github.com/gobwas/prompt v0.2.2/go.mod h1:UVO2T+b2GvmPMwT07n3gxtzepiAu6N3GP177DjDN1l8=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38=
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
162 changes: 162 additions & 0 deletions parse/prompt/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package prompt

import (
"context"
"flag"
"fmt"

"github.com/gobwas/flagutil/parse"
"github.com/gobwas/prompt"
)

type FlagInfo struct {
Message string
Options []string
Multiple bool
Boolean bool
}

type FlagInfoMapper interface {
FlagInfo(context.Context, *flag.Flag) (FlagInfo, error)
}

type FlagInfoFunc func(context.Context, *flag.Flag) (FlagInfo, error)

func (fn FlagInfoFunc) FlagInfo(ctx context.Context, f *flag.Flag) (FlagInfo, error) {
return fn(ctx, f)
}

type FlagInfoMap map[string]FlagInfo

func (m FlagInfoMap) FlagInfo(_ context.Context, f *flag.Flag) (FlagInfo, error) {
return m[f.Name], nil
}

var DefaultMessage = func(f *flag.Flag, c FlagInfo) string {
m := "Specify " + f.Name + " value"
if c.Multiple {
m += "(s)"
}
m += " (" + f.Usage + ")"
return m
}

type Parser struct {
Retry bool
FlagInfo FlagInfoMapper
Message func(*flag.Flag, FlagInfo) string
}

func (p *Parser) Parse(ctx context.Context, fs parse.FlagSet) (err error) {
fs.VisitUnspecified(func(f *flag.Flag) {
if err != nil {
return
}
repeat:
var set []string
set, err = p.values(ctx, f)
if err != nil {
return
}
for i, s := range set {
err = fs.Set(f.Name, s)
if err != nil && p.Retry && i == 0 {
// i == 0 is required to not leave partially configured flags.
fmt.Println(err)
goto repeat
}
if err != nil {
break
}
}
})
return err
}

func (p *Parser) values(ctx context.Context, f *flag.Flag) ([]string, error) {
cfg, err := p.info(ctx, f)
if err != nil {
return nil, err
}
switch {
case cfg.Options != nil:
return p.opt(ctx, f, cfg)

case cfg.Boolean || isBoolFlag(f):
return p.confirm(ctx, f, cfg)

default:
return p.readLine(ctx, f, cfg)
}
}

func (p *Parser) info(ctx context.Context, f *flag.Flag) (_ FlagInfo, err error) {
if x := p.FlagInfo; x != nil {
return x.FlagInfo(ctx, f)
}
return
}

func (p *Parser) message(f *flag.Flag, c FlagInfo) string {
if msg := c.Message; msg != "" {
return msg
}
if fn := p.Message; fn != nil {
return fn(f, c)
}
return DefaultMessage(f, c)
}

func (p *Parser) opt(ctx context.Context, f *flag.Flag, c FlagInfo) (set []string, err error) {
s := prompt.Select{
Message: p.message(f, c),
Options: c.Options,
}
var xs []int
if c.Multiple {
xs, err = s.Multiple(ctx)
} else {
xs = []int{-1}
xs[0], err = s.Single(ctx)
}
if err != nil {
return nil, err
}
set = make([]string, len(xs))
for i, x := range xs {
set[i] = c.Options[x]
}
return set, nil
}

func (p *Parser) confirm(ctx context.Context, f *flag.Flag, c FlagInfo) (set []string, err error) {
q := prompt.Question{
Message: p.message(f, c),
Strict: true,
Mode: prompt.QuestionSuffix,
}
v, err := q.Confirm(ctx)
if err != nil {
return nil, err
}
return []string{fmt.Sprintf("%t", v)}, nil
}

func (p *Parser) readLine(ctx context.Context, f *flag.Flag, c FlagInfo) (set []string, err error) {
pt := prompt.Prompt{
Message: p.message(f, c) + " ",
Default: f.Value.String(),
}
line, err := pt.ReadLine(ctx)
if err != nil {
return nil, err
}
return []string{line}, nil
}

func isBoolFlag(f *flag.Flag) bool {
x, ok := f.Value.(interface {
IsBoolFlag() bool
})
return ok && x.IsBoolFlag()
}

0 comments on commit c7c9dd6

Please sign in to comment.