diff --git a/README.md b/README.md index 5033807..b10c39c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@

# Kong is a command-line parser for Go + [![](https://godoc.org/github.com/alecthomas/kong?status.svg)](http://godoc.org/github.com/alecthomas/kong) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong.svg)](https://circleci.com/gh/alecthomas/kong) [![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/kong)](https://goreportcard.com/report/github.com/alecthomas/kong) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3) @@ -24,6 +25,7 @@ - [Supported tags](#supported-tags) - [Plugins](#plugins) - [Variable interpolation](#variable-interpolation) +- [Validation](#validation) - [Modifying Kong's behaviour](#modifying-kongs-behaviour) - [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description) - [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files) @@ -39,7 +41,8 @@ Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible. -To achieve that, command-lines are expressed as Go types, with the structure and tags directing how the command line is mapped onto the struct. +To achieve that, command-lines are expressed as Go types, with the structure and tags directing how the command line is +mapped onto the struct. For example, the following command-line: @@ -54,32 +57,33 @@ package main import "github.com/alecthomas/kong" var CLI struct { - Rm struct { - Force bool `help:"Force removal."` - Recursive bool `help:"Recursively remove files."` + Rm struct { + Force bool `help:"Force removal."` + Recursive bool `help:"Recursively remove files."` - Paths []string `arg name:"path" help:"Paths to remove." type:"path"` - } `cmd help:"Remove files."` + Paths []string `arg name:"path" help:"Paths to remove." type:"path"` + } `cmd help:"Remove files."` - Ls struct { - Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` - } `cmd help:"List paths."` + Ls struct { + Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` + } `cmd help:"List paths."` } func main() { - ctx := kong.Parse(&CLI) - switch ctx.Command() { - case "rm ": - case "ls": - default: - panic(ctx.Command()) - } + ctx := kong.Parse(&CLI) + switch ctx.Command() { + case "rm ": + case "ls": + default: + panic(ctx.Command()) + } } ``` ## Help -Help is automatically generated. With no other arguments provided, help will display a full summary of all available commands. +Help is automatically generated. With no other arguments provided, help will display a full summary of all available +commands. eg. @@ -117,9 +121,8 @@ eg. -f, --force Force removal. -r, --recursive Recursively remove files. -For flags with associated environment variables, the variable `${env}` can be -interpolated into the help string. In the absence of this variable in the help, - +For flags with associated environment variables, the variable `${env}` can be interpolated into the help string. In the +absence of this variable in the help, ## Command handling @@ -127,7 +130,9 @@ There are two ways to handle commands in Kong. ### Switch on the command string -When you call `kong.Parse()` it will return a unique string representation of the command. Each command branch in the hierarchy will be a bare word and each branching argument or required positional argument will be the name surrounded by angle brackets. Here's an example: +When you call `kong.Parse()` it will return a unique string representation of the command. Each command branch in the +hierarchy will be a bare word and each branching argument or required positional argument will be the name surrounded by +angle brackets. Here's an example: There's an example of this pattern [here](https://github.com/alecthomas/kong/blob/master/_examples/shell/main.go). @@ -139,30 +144,31 @@ package main import "github.com/alecthomas/kong" var CLI struct { - Rm struct { - Force bool `help:"Force removal."` - Recursive bool `help:"Recursively remove files."` + Rm struct { + Force bool `help:"Force removal."` + Recursive bool `help:"Recursively remove files."` - Paths []string `arg name:"path" help:"Paths to remove." type:"path"` - } `cmd help:"Remove files."` + Paths []string `arg name:"path" help:"Paths to remove." type:"path"` + } `cmd help:"Remove files."` - Ls struct { - Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` - } `cmd help:"List paths."` + Ls struct { + Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` + } `cmd help:"List paths."` } func main() { - ctx := kong.Parse(&CLI) - switch ctx.Command() { - case "rm ": - case "ls": - default: - panic(ctx.Command()) - } + ctx := kong.Parse(&CLI) + switch ctx.Command() { + case "rm ": + case "ls": + default: + panic(ctx.Command()) + } } ``` -This has the advantage that it is convenient, but the downside that if you modify your CLI structure, the strings may change. This can be fragile. +This has the advantage that it is convenient, but the downside that if you modify your CLI structure, the strings may +change. This can be fragile. ### Attach a `Run(...) error` method to each command @@ -173,69 +179,71 @@ A more robust approach is to break each command out into their own structs: 3. Call `kong.Kong.Parse()` to obtain a `kong.Context`. 4. Call `kong.Context.Run(bindings...)` to call the selected parsed command. -Once a command node is selected by Kong it will search from that node back to the root. Each -encountered command node with a `Run(...) error` will be called in reverse order. This allows -sub-trees to be re-used fairly conveniently. +Once a command node is selected by Kong it will search from that node back to the root. Each encountered command node +with a `Run(...) error` will be called in reverse order. This allows sub-trees to be re-used fairly conveniently. -In addition to values bound with the `kong.Bind(...)` option, any values -passed through to `kong.Context.Run(...)` are also bindable to the target's +In addition to values bound with the `kong.Bind(...)` option, any values passed through to `kong.Context.Run(...)` are +also bindable to the target's `Run()` arguments. Finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. -There's a full example emulating part of the Docker CLI [here](https://github.com/alecthomas/kong/tree/master/_examples/docker). +There's a full example emulating part of the Docker +CLI [here](https://github.com/alecthomas/kong/tree/master/_examples/docker). eg. ```go type Context struct { - Debug bool +Debug bool } type RmCmd struct { - Force bool `help:"Force removal."` - Recursive bool `help:"Recursively remove files."` +Force bool `help:"Force removal."` +Recursive bool `help:"Recursively remove files."` - Paths []string `arg name:"path" help:"Paths to remove." type:"path"` +Paths []string `arg name:"path" help:"Paths to remove." type:"path"` } func (r *RmCmd) Run(ctx *Context) error { - fmt.Println("rm", r.Paths) - return nil +fmt.Println("rm", r.Paths) +return nil } type LsCmd struct { - Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` +Paths []string `arg optional name:"path" help:"Paths to list." type:"path"` } func (l *LsCmd) Run(ctx *Context) error { - fmt.Println("ls", l.Paths) - return nil +fmt.Println("ls", l.Paths) +return nil } var cli struct { - Debug bool `help:"Enable debug mode."` +Debug bool `help:"Enable debug mode."` - Rm RmCmd `cmd help:"Remove files."` - Ls LsCmd `cmd help:"List paths."` +Rm RmCmd `cmd help:"Remove files."` +Ls LsCmd `cmd help:"List paths."` } func main() { - ctx := kong.Parse(&cli) - // Call the Run() method of the selected parsed command. - err := ctx.Run(&Context{Debug: cli.Debug}) - ctx.FatalIfErrorf(err) +ctx := kong.Parse(&cli) +// Call the Run() method of the selected parsed command. +err := ctx.Run(&Context{Debug: cli.Debug}) +ctx.FatalIfErrorf(err) } ``` ## Hooks: BeforeResolve(), BeforeApply(), AfterApply() and the Bind() option -If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before validation/assignment and after validation/assignment, respectively. +If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, +those methods will be called before validation/assignment and after validation/assignment, respectively. The `--help` flag is implemented with a `BeforeApply` hook. -Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context` and `*Path` are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. +Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context` and `*Path` are +also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`. eg. @@ -244,33 +252,34 @@ eg. type debugFlag bool func (d debugFlag) BeforeApply(logger *log.Logger) error { - logger.SetOutput(os.Stdout) - return nil +logger.SetOutput(os.Stdout) +return nil } var cli struct { - Debug debugFlag `help:"Enable debug logging."` +Debug debugFlag `help:"Enable debug logging."` } func main() { - // Debug logger going to discard. - logger := log.New(ioutil.Discard, "", log.LstdFlags) +// Debug logger going to discard. +logger := log.New(ioutil.Discard, "", log.LstdFlags) - ctx := kong.Parse(&cli, kong.Bind(logger)) +ctx := kong.Parse(&cli, kong.Bind(logger)) - // ... +// ... } ``` ## Flags -Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default. +Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* +tagged with `cmd` or `arg` will be a flag. Flags are optional by default. eg. The command-line `app [--flag="foo"]` can be represented by the following. ```go type CLI struct { - Flag string +Flag string } ``` @@ -282,12 +291,12 @@ eg. The following struct represents the CLI structure `command [--flag="str"] su ```go type CLI struct { - Command struct { - Flag string +Command struct { +Flag string - SubCommand struct { - } `cmd` - } `cmd` +SubCommand struct { +} `cmd` +} `cmd` } ``` @@ -297,7 +306,9 @@ If a sub-command is tagged with `default:"1"` it will be selected if there are n In addition to sub-commands, structs can also be configured as branching positional arguments. -This is achieved by tagging an [unmapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) nested struct field with `arg`, then including a positional argument field inside that struct _with the same name_. For example, the following command structure: +This is achieved by tagging an [unmapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) nested +struct field with `arg`, then including a positional argument field inside that struct _with the same name_. For +example, the following command structure: app rename to @@ -305,16 +316,16 @@ Can be represented with the following: ```go var CLI struct { - Rename struct { - Name struct { - Name string `arg` // <-- NOTE: identical name to enclosing struct field. - To struct { - Name struct { - Name string `arg` - } `arg` - } `cmd` - } `arg` - } `cmd` +Rename struct { +Name struct { +Name string `arg` // <-- NOTE: identical name to enclosing struct field. +To struct { +Name struct { +Name string `arg` +} `arg` +} `cmd` +} `arg` +} `cmd` } ``` @@ -322,13 +333,16 @@ This looks a little verbose in this contrived example, but typically this will n ## Terminating positional arguments -If a [mapped type](#mapper---customising-how-the-command-line-is-mapped-to-go-values) is tagged with `arg` it will be treated as the final positional values to be parsed on the command line. +If a [mapped type](#mapper---customising-how-the-command-line-is-mapped-to-go-values) is tagged with `arg` it will be +treated as the final positional values to be parsed on the command line. If a positional argument is a slice, all remaining arguments will be appended to that slice. ## Slices -Slice values are treated specially. First the input is split on the `sep:""` tag (defaults to `,`), then each element is parsed by the slice element type and appended to the slice. If the same value is encountered multiple times, elements continue to be appended. +Slice values are treated specially. First the input is split on the `sep:""` tag (defaults to `,`), then each +element is parsed by the slice element type and appended to the slice. If the same value is encountered multiple times, +elements continue to be appended. To represent the following command-line: @@ -338,15 +352,16 @@ You would use the following: ```go var CLI struct { - Ls struct { - Files []string `arg type:"existingfile"` - } `cmd` +Ls struct { +Files []string `arg type:"existingfile"` +} `cmd` } ``` ## Maps -Maps are similar to slices except that only one key/value pair can be assigned per value, and the `sep` tag denotes the assignment character and defaults to `=`. +Maps are similar to slices except that only one key/value pair can be assigned per value, and the `sep` tag denotes the +assignment character and defaults to `=`. To represent the following command-line: @@ -356,21 +371,21 @@ You would use the following: ```go var CLI struct { - Config struct { - Set struct { - Config map[string]float64 `arg type:"file:"` - } `cmd` - } `cmd` +Config struct { +Set struct { +Config map[string]float64 `arg type:"file:"` +} `cmd` +} `cmd` } ``` -For flags, multiple key+value pairs should be separated by `mapsep:"rune"` tag (defaults to `;`) eg. `--set="key1=value1;key2=value2"`. +For flags, multiple key+value pairs should be separated by `mapsep:"rune"` tag (defaults to `;`) +eg. `--set="key1=value1;key2=value2"`. ## Custom named decoders -Kong includes a number of builtin custom type mappers. These can be used by -specifying the tag `type:""`. They are registered with the option -function `NamedMapper(name, mapper)`. +Kong includes a number of builtin custom type mappers. These can be used by specifying the tag `type:""`. They are +registered with the option function `NamedMapper(name, mapper)`. | Name | Description |-------------------|--------------------------------------------------- @@ -379,19 +394,16 @@ function `NamedMapper(name, mapper)`. | `existingdir` | An existing directory. ~ expansion is applied. | `counter` | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`. - -Slices and maps treat type tags specially. For slices, the `type:""` tag -specifies the element type. For maps, the tag has the format +Slices and maps treat type tags specially. For slices, the `type:""` tag specifies the element type. For maps, the tag +has the format `tag:"[]:[]"` where either may be omitted. ## Supported field types - ## Custom decoders (mappers) - -Any field implementing `encoding.TextUnmarshaler` or `json.Unmarshaler` will use those interfaces -for decoding values. Kong also includes builtin support for many common Go types: +Any field implementing `encoding.TextUnmarshaler` or `json.Unmarshaler` will use those interfaces for decoding values. +Kong also includes builtin support for many common Go types: | Type | Description |---------------------|-------------------------------------------- @@ -441,18 +453,19 @@ Tag | Description ## Plugins -Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated structs. For example: +Kong CLI's can be extended by embedding the `kong.Plugin` type and populating it with pointers to Kong annotated +structs. For example: ```go var pluginOne struct { - PluginOneFlag string +PluginOneFlag string } var pluginTwo struct { - PluginTwoFlag string +PluginTwoFlag string } var cli struct { - BaseFlag string - kong.Plugins +BaseFlag string +kong.Plugins } cli.Plugins = kong.Plugins{&pluginOne, &pluginTwo} ``` @@ -461,23 +474,20 @@ Additionally if an interface type is embedded, it can also be populated with a K ## Variable interpolation -Kong supports limited variable interpolation into help strings, enum lists and -default values. +Kong supports limited variable interpolation into help strings, enum lists and default values. Variables are in the form: ${} ${=} -Variables are set with the `Vars{"key": "value", ...}` option. Undefined -variable references in the grammar without a default will result in an error at -construction time. +Variables are set with the `Vars{"key": "value", ...}` option. Undefined variable references in the grammar without a +default will result in an error at construction time. -Variables can also be set via the `set:"K=V"` tag. In this case, those variables will be available for that -node and all children. This is useful for composition by allowing the same struct to be reused. +Variables can also be set via the `set:"K=V"` tag. In this case, those variables will be available for that node and all +children. This is useful for composition by allowing the same struct to be reused. -When interpolating into flag or argument help strings, some extra variables -are defined from the value itself: +When interpolating into flag or argument help strings, some extra variables are defined from the value itself: ${default} ${enum} @@ -486,17 +496,32 @@ eg. ```go type cli struct { - Config string `type:"path" default:"${config_file}"` +Config string `type:"path" default:"${config_file}"` } func main() { - kong.Parse(&cli, - kong.Vars{ - "config_file": "~/.app.conf", - }) +kong.Parse(&cli, +kong.Vars{ +"config_file": "~/.app.conf", +}) } ``` +## Validation + +Kong does validation on the structure of a command-line, but also supports +extensible validation. Any node in the tree may implement the following +interface: + +```go +type Validatable interface { + Validate() error +} +``` + +If one of these nodes is in the active command-line it will be called during +normal validation. + ## Modifying Kong's behaviour Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`. @@ -513,7 +538,8 @@ As with all help in Kong, text will be wrapped to the terminal. ### `Configuration(loader, paths...)` - load defaults from configuration files -This option provides Kong with support for loading defaults from a set of configuration files. Each file is opened, if possible, and the loader called to create a resolver for that file. +This option provides Kong with support for loading defaults from a set of configuration files. Each file is opened, if +possible, and the loader called to create a resolver for that file. eg. @@ -521,11 +547,14 @@ eg. kong.Parse(&cli, kong.Configuration(kong.JSON, "/etc/myapp.json", "~/.myapp.json")) ``` -[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L103) for an example of how the JSON file is structured. +[See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L103) for an example of how the JSON +file is structured. ### `Resolver(...)` - support for default values from external sources -Resolvers are Kong's extension point for providing default values from external sources. As an example, support for environment variables via the `env` tag is provided by a resolver. There's also a builtin resolver for JSON configuration files. +Resolvers are Kong's extension point for providing default values from external sources. As an example, support for +environment variables via the `env` tag is provided by a resolver. There's also a builtin resolver for JSON +configuration files. Example resolvers can be found in [resolver.go](https://github.com/alecthomas/kong/blob/master/resolver.go). @@ -544,7 +573,8 @@ type Mapper interface { } ``` -All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have mappers registered by default. Mappers for custom types can be added using `kong.??Mapper(...)` options. Mappers are applied to fields in four ways: +All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have mappers registered by default. +Mappers for custom types can be added using `kong.??Mapper(...)` options. Mappers are applied to fields in four ways: 1. `NamedMapper(string, Mapper)` and using the tag key `type:""`. 2. `KindMapper(reflect.Kind, Mapper)`. @@ -555,9 +585,12 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time` The default help output is usually sufficient, but if not there are two solutions. -1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details). -2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example. -3. Use `HelpFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments. +1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted ( + see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details). +2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which + contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example. +3. Use `HelpFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and + arguments. ### `Bind(...)` - bind values for callback hooks and Run() methods diff --git a/context.go b/context.go index 244c672..f07f537 100644 --- a/context.go +++ b/context.go @@ -44,6 +44,27 @@ func (p *Path) Node() *Node { return nil } +// Visitable returns the Visitable for this path element. +func (p *Path) Visitable() Visitable { + switch { + case p.App != nil: + return p.App + + case p.Argument != nil: + return p.Argument + + case p.Command != nil: + return p.Command + + case p.Flag != nil: + return p.Flag + + case p.Positional != nil: + return p.Positional + } + return nil +} + // Context contains the current parse context. type Context struct { *Kong @@ -136,10 +157,19 @@ func (c *Context) Empty() bool { // Validate the current context. func (c *Context) Validate() error { // nolint: gocyclo err := Visit(c.Model, func(node Visitable, next Next) error { - if value, ok := node.(*Value); ok { - _, ok := os.LookupEnv(value.Tag.Env) - if value.Enum != "" && (!value.Required || value.Default != "" || (value.Tag.Env != "" && ok)) { - if err := checkEnum(value, value.Target); err != nil { + switch node := node.(type) { + case *Value: + _, ok := os.LookupEnv(node.Tag.Env) + if node.Enum != "" && (!node.Required || node.Default != "" || (node.Tag.Env != "" && ok)) { + if err := checkEnum(node, node.Target); err != nil { + return err + } + } + + case *Flag: + _, ok := os.LookupEnv(node.Tag.Env) + if node.Enum != "" && (!node.Required || node.Default != "" || (node.Tag.Env != "" && ok)) { + if err := checkEnum(node.Value, node.Target); err != nil { return err } } @@ -149,6 +179,35 @@ func (c *Context) Validate() error { // nolint: gocyclo if err != nil { return err } + for _, el := range c.Path { + var ( + value reflect.Value + desc string + ) + switch node := el.Visitable().(type) { + case *Value: + value = node.Target + desc = node.ShortSummary() + + case *Flag: + value = node.Target + desc = node.ShortSummary() + + case *Application: + value = node.Target + desc = node.Name + + case *Node: + value = node.Target + desc = node.Path() + } + if validate := isValidatable(value); validate != nil { + err := validate.Validate() + if err != nil { + return errors.Wrap(err, desc) + } + } + } for _, resolver := range c.combineResolvers() { if err := resolver.Validate(c.Model); err != nil { return err @@ -787,3 +846,18 @@ func findPotentialCandidates(needle string, haystack []string, format string, ar } return fmt.Errorf("%s", prefix) } + +type validatable interface{ Validate() error } + +func isValidatable(v reflect.Value) validatable { + if !v.IsValid() || (v.Kind() == reflect.Ptr || v.Kind() == reflect.Slice || v.Kind() == reflect.Map) && v.IsNil() { + return nil + } + if validate, ok := v.Interface().(validatable); ok { + return validate + } + if v.CanAddr() { + return isValidatable(v.Addr()) + } + return nil +} diff --git a/kong_test.go b/kong_test.go index 8105641..3703f4d 100644 --- a/kong_test.go +++ b/kong_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/alecthomas/kong" @@ -887,3 +888,51 @@ func TestPlugins(t *testing.T) { require.Equal(t, "one", pluginOne.One) require.Equal(t, "two", pluginTwo.Two) } + +type validateCmd struct{} + +func (v *validateCmd) Validate() error { return errors.New("cmd error") } + +type validateCli struct { + Cmd validateCmd `cmd:""` +} + +func (v *validateCli) Validate() error { return errors.New("app error") } + +type validateFlag string + +func (v *validateFlag) Validate() error { return errors.New("flag error") } + +func TestValidateApp(t *testing.T) { + cli := validateCli{} + p := mustNew(t, &cli) + _, err := p.Parse([]string{}) + require.EqualError(t, err, "test: app error") +} + +func TestValidateCmd(t *testing.T) { + cli := struct { + Cmd validateCmd `cmd:""` + }{} + p := mustNew(t, &cli) + _, err := p.Parse([]string{"cmd"}) + require.EqualError(t, err, "cmd: cmd error") +} + +func TestValidateFlag(t *testing.T) { + cli := struct { + Flag validateFlag + }{} + p := mustNew(t, &cli) + _, err := p.Parse([]string{"--flag=one"}) + require.EqualError(t, err, "--flag: flag error") +} + +func TestValidateArg(t *testing.T) { + cli := struct { + Arg validateFlag `arg:""` + }{} + p := mustNew(t, &cli) + _, err := p.Parse([]string{"one"}) + require.EqualError(t, err, ": flag error") +}