Kong Go CLI
I’m a fan of Kong the CLI argument parser, I use it to automatically create manual pages, (bash) completion and do a lot of validation inside it. (This guy also has other really interesting packages, and he’s a huge fan of Go’s struct tags).
I wanted to do validation inside Kong as well, for this Kong has
validation, which is just a
method (Validate() error
) on a type. Now most types I have are just string
or any other builtin
type. For validation to work, all these types must be re-typed to another type on which the method
can be applied, which is annoying. I wanted a better way…
For instance I have this field in a struct for a sub-command:
User string `arg:"" name:"loginname|UID" help:"User or UID to list." optional:"" completion:"c user list --comp"`
The completion
tag is my invention, see https://github.com/miekg/gompletely, on how that works,
anyway, I want to syntax check this User
and with Kong’s custom mappers, you can do the following.
loginMapper := kong.MapperFunc(func(ctx *kong.DecodeContext, v reflect.Value) error {
var login string
if err := ctx.Scan.PopValueInto("string", &login); err != nil {
return err
}
v.SetString(login)
if err := dbuser.Valid(login); err == nil {
return nil
}
// might be uid...
uid, err := strconv.ParseUint(login, 10, 64)
if err != nil {
return err
}
if err := dbuser.ValidUID(uint(uid)); err != nil && uid > 0 {
return err
}
return nil
})
This code calls some other checking function which are not that important, and defines a
kong.MapperFunc
which can be used when parsing the command line:
c := Cli{}
kctx := kong.Parse(&c, kong.NamedMapper("login", loginMapper))
And with this you get a type:"login"
, which you can apply to all the places where you want this
check to happen:
User string `arg:"" name:"loginname|UID" help:"User or UID to list." optional:"" completion:"c user list --comp" type:"login"`
No extra methods, no new types, just type:"login"
. Love it.
Update⌗
A (string)slice arguments, that mapper becomes slightly more complex, but still elegant, although I could refactor this a bit and make it even shorter.
oginMapper = kong.MapperFunc(func(ctx *kong.DecodeContext, v reflect.Value) error {
if v.Type().String() == "[]string" {
logins := ctx.Scan.PopWhile(func(t kong.Token) bool { return !t.IsEOL() })
names := []string{}
for i := range logins {
names = append(names, logins[i].Value.(string))
if err := dbuser.Valid(logins[i].Value.(string)); err == nil {
continue
}
uid, err := strconv.ParseUint(logins[i].Value.(string), 10, 64)
if err != nil {
return err
}
if err := dbuser.ValidUID(uint(uid)); err != nil && uid > 0 {
return err
}
}
v.Set(reflect.ValueOf(names))
return nil
}
var login string
if err := ctx.Scan.PopValueInto("login mapper", &login); err != nil {
return err
}
if login == "" {
return nil
}
v.SetString(login)
if err := dbuser.Valid(login); err == nil {
return nil
}
uid, err := strconv.ParseUint(login, 10, 64)
if err != nil {
return err
}
if err := dbuser.ValidUID(uint(uid)); err != nil && uid > 0 {
return err
}
return nil
})