feat(opt groups): Add support for grouped options
Nested structs are now suppored - both pointer and embedded. These nested structs can be presented as option groups with their own group name and help section.
This commit is contained in:
parent
ca8dc31b84
commit
97d1ef3a3c
64
README.md
64
README.md
|
@ -583,6 +583,70 @@ if p.Subcommand() == nil {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Option groups
|
||||||
|
|
||||||
|
Option groups are a hybrid between subcommands and embedded structs. Option
|
||||||
|
groups create logical collections of related arguments with a help description,
|
||||||
|
and can be embedded in other groups and subcommands. Option groups can combine
|
||||||
|
configuration structs of multiple modules without requiring embedding.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Repository struct {
|
||||||
|
URL string `arg:"--host" help:"URL of the repository" default:"docker.io"`
|
||||||
|
User string `arg:"--user,env:REPO_USERNAME" help:"username to connect as"`
|
||||||
|
Password string `arg:"--,env:REPO_PASSWORD" help:"password to connect with"`
|
||||||
|
}
|
||||||
|
type BuildCmd struct {
|
||||||
|
Context string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
type PushCmd struct {
|
||||||
|
Repo Repository `arg:"group:Repository" help:"Change the default registry to push to."`
|
||||||
|
Tag string `help:"Tag"`
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
Build *BuildCmd `arg:"subcommand:build"`
|
||||||
|
Push *PushCmd `arg:"subcommand:push"`
|
||||||
|
Quiet bool `arg:"-q" help:"Quiet"` // this flag is global to all subcommands
|
||||||
|
}
|
||||||
|
|
||||||
|
arg.MustParse(&args)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case args.Build != nil:
|
||||||
|
fmt.Printf("build %s as %q\n", args.Build.Context, args.Build.Tag)
|
||||||
|
case args.Push != nil:
|
||||||
|
fmt.Printf("push %q to %q\n", args.Push.Tag, args.Push.Repo.URL)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The push command help message would look like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: example push [--tag TAG] [--host HOST] [--user USER]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--tag TAG Tag
|
||||||
|
|
||||||
|
Repository options:
|
||||||
|
|
||||||
|
Change the default registry to push to.
|
||||||
|
|
||||||
|
--host HOST URL of the repository [default: docker.io]
|
||||||
|
--user USER username to connect as [env: REPO_USERNAME]
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--quiet, -q Quiet
|
||||||
|
--help, -h display this help and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Some additional rules apply when working with option groups:
|
||||||
|
* The `group` tag can only be used with fields that are structs or pointers to structs.
|
||||||
|
* Specifying default values in nested struct pointers _always_ result in an initialized struct.
|
||||||
|
* Option groups may not contain any positionals.
|
||||||
|
* Option groups cannot contain sub-commands.
|
||||||
|
```
|
||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
|
|
||||||
https://godoc.org/github.com/alexflint/go-arg
|
https://godoc.org/github.com/alexflint/go-arg
|
||||||
|
|
|
@ -253,6 +253,55 @@ func Example_helpTextWithSubcommand() {
|
||||||
// list list available items
|
// list list available items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This example shows the usage string generated by go-arg when using argument groups
|
||||||
|
func Example_helpTextWithGroups() {
|
||||||
|
os.Args = split("./example push --help")
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
URL string `arg:"--host" help:"URL of the repository" default:"docker.io"`
|
||||||
|
User string `arg:"--user,env:REPO_USERNAME" help:"username to connect as"`
|
||||||
|
Password string `arg:"--,env:REPO_PASSWORD" help:"password to connect with"`
|
||||||
|
}
|
||||||
|
type BuildCmd struct {
|
||||||
|
Context string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
type PushCmd struct {
|
||||||
|
Repo Repository `arg:"group:Repository" help:"Change the default registry to push to."`
|
||||||
|
Tag string `help:"Tag"`
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
Build *BuildCmd `arg:"subcommand:build"`
|
||||||
|
Push *PushCmd `arg:"subcommand:push"`
|
||||||
|
Quiet bool `arg:"-q" help:"Quiet"` // this flag is global to all subcommands
|
||||||
|
}
|
||||||
|
|
||||||
|
MustParse(&args)
|
||||||
|
|
||||||
|
// This is only necessary when running inside golang's runnable example harness
|
||||||
|
osExit = func(int) {}
|
||||||
|
stdout = os.Stdout
|
||||||
|
|
||||||
|
MustParse(&args)
|
||||||
|
|
||||||
|
// output:
|
||||||
|
// Usage: example push [--tag TAG] [--host HOST] [--user USER]
|
||||||
|
//
|
||||||
|
// Options:
|
||||||
|
// --tag TAG Tag
|
||||||
|
//
|
||||||
|
// Repository options:
|
||||||
|
//
|
||||||
|
// Change the default registry to push to.
|
||||||
|
//
|
||||||
|
// --host HOST URL of the repository [default: docker.io]
|
||||||
|
// --user USER username to connect as [env: REPO_USERNAME]
|
||||||
|
//
|
||||||
|
// Global options:
|
||||||
|
// --quiet, -q Quiet
|
||||||
|
// --help, -h display this help and exit
|
||||||
|
}
|
||||||
|
|
||||||
// This example shows the usage string generated by go-arg when using subcommands
|
// This example shows the usage string generated by go-arg when using subcommands
|
||||||
func Example_helpTextWhenUsingSubcommand() {
|
func Example_helpTextWhenUsingSubcommand() {
|
||||||
// These are the args you would pass in on the command line
|
// These are the args you would pass in on the command line
|
||||||
|
|
87
parse.go
87
parse.go
|
@ -63,11 +63,23 @@ type command struct {
|
||||||
name string
|
name string
|
||||||
help string
|
help string
|
||||||
dest path
|
dest path
|
||||||
specs []*spec
|
options []*spec
|
||||||
subcommands []*command
|
subcommands []*command
|
||||||
|
groups []*command
|
||||||
parent *command
|
parent *command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// specs gets all the specs from this command plus all nested option groups,
|
||||||
|
// recursively through descendants
|
||||||
|
func (cmd command) specs() []*spec {
|
||||||
|
var specs []*spec
|
||||||
|
specs = append(specs, cmd.options...)
|
||||||
|
for _, grpcmd := range cmd.groups {
|
||||||
|
specs = append(specs, grpcmd.specs()...)
|
||||||
|
}
|
||||||
|
return specs
|
||||||
|
}
|
||||||
|
|
||||||
// ErrHelp indicates that -h or --help were provided
|
// ErrHelp indicates that -h or --help were provided
|
||||||
var ErrHelp = errors.New("help requested by user")
|
var ErrHelp = errors.New("help requested by user")
|
||||||
|
|
||||||
|
@ -206,7 +218,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := p.cmd.parseFieldsFromStructPointer(t)
|
err := p.cmd.parseFieldsFromStructPointer(t, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -214,7 +226,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
// for backwards compatibility, add nonzero field values as defaults
|
// for backwards compatibility, add nonzero field values as defaults
|
||||||
// this applies only to the top-level command, not to subcommands (this inconsistency
|
// this applies only to the top-level command, not to subcommands (this inconsistency
|
||||||
// is the reason that this method for setting default values was deprecated)
|
// is the reason that this method for setting default values was deprecated)
|
||||||
for _, spec := range p.cmd.specs {
|
for _, spec := range p.cmd.specs() {
|
||||||
// get the value
|
// get the value
|
||||||
defaultString, defaultValue, err := p.defaultVal(spec.dest)
|
defaultString, defaultValue, err := p.defaultVal(spec.dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -248,7 +260,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
// parseFieldsFromStructPointer ensures the destination structure is a pointer
|
// parseFieldsFromStructPointer ensures the destination structure is a pointer
|
||||||
// to a struct. This function should be called when parsing commands or
|
// to a struct. This function should be called when parsing commands or
|
||||||
// subcommands as they can only be a struct pointer.
|
// subcommands as they can only be a struct pointer.
|
||||||
func (cmd *command) parseFieldsFromStructPointer(t reflect.Type) error {
|
func (cmd *command) parseFieldsFromStructPointer(t reflect.Type, insideGroup bool) error {
|
||||||
// commands can only be created from pointers to structs
|
// commands can only be created from pointers to structs
|
||||||
if t.Kind() != reflect.Ptr {
|
if t.Kind() != reflect.Ptr {
|
||||||
return fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
return fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
||||||
|
@ -260,7 +272,33 @@ func (cmd *command) parseFieldsFromStructPointer(t reflect.Type) error {
|
||||||
return fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
return fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
||||||
cmd.dest, t.Kind())
|
cmd.dest, t.Kind())
|
||||||
}
|
}
|
||||||
|
return cmd.parseStruct(t, insideGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFieldsFromStructOrStructPointer ensures the destination structure is
|
||||||
|
// either a pointer to a struct, or a struct. This function should be called
|
||||||
|
// when parsing option groups as they can only be a struct, or a pointer to one.
|
||||||
|
func (cmd *command) parseFieldsFromStructOrStructPointer(t reflect.Type, insideGroup bool) error {
|
||||||
|
// option groups can only be created from structs or pointers to structs
|
||||||
|
typeHint := ""
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
typeHint = "a pointer to "
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("option groups must be structs or pointers to structs, but %s is %s%s",
|
||||||
|
cmd.dest, typeHint, t.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.parseStruct(t, insideGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseStruct populates the command instance based on the type and annotations
|
||||||
|
// of the target struct. As these command instances are used for either (sub)
|
||||||
|
// commands or option groups, please refer to the parseFieldsFromStructPointer
|
||||||
|
// or parseFieldsFromStructOrStructPointer respectively.
|
||||||
|
func (cmd *command) parseStruct(t reflect.Type, insideGroup bool) error {
|
||||||
var errs []string
|
var errs []string
|
||||||
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||||
// check for the ignore switch in the tag
|
// check for the ignore switch in the tag
|
||||||
|
@ -342,19 +380,47 @@ func (cmd *command) parseFieldsFromStructPointer(t reflect.Type) error {
|
||||||
}
|
}
|
||||||
cmd.subcommands = append(cmd.subcommands, &subCmd)
|
cmd.subcommands = append(cmd.subcommands, &subCmd)
|
||||||
|
|
||||||
|
if insideGroup {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: %s subcommands cannot be part of option groups",
|
||||||
|
t.Name(), field.Name, field.Type.String()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// decide on a name for the subcommand
|
// decide on a name for the subcommand
|
||||||
if subCmd.name == "" {
|
if subCmd.name == "" {
|
||||||
subCmd.name = strings.ToLower(field.Name)
|
subCmd.name = strings.ToLower(field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the subcommand recursively
|
// parse the subcommand recursively
|
||||||
err := subCmd.parseFieldsFromStructPointer(field.Type)
|
err := subCmd.parseFieldsFromStructPointer(field.Type, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err.Error())
|
errs = append(errs, err.Error())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
case key == "group":
|
||||||
|
// parse the option group recursively
|
||||||
|
optGrp := command{
|
||||||
|
name: value,
|
||||||
|
dest: subdest,
|
||||||
|
parent: cmd,
|
||||||
|
help: field.Tag.Get("help"),
|
||||||
|
}
|
||||||
|
cmd.groups = append(cmd.groups, &optGrp)
|
||||||
|
|
||||||
|
// decide on a name for the group
|
||||||
|
if optGrp.name == "" {
|
||||||
|
optGrp.name = strings.Title(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := optGrp.parseFieldsFromStructOrStructPointer(field.Type, true)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
default:
|
default:
|
||||||
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
||||||
return false
|
return false
|
||||||
|
@ -417,7 +483,7 @@ func (cmd *command) parseFieldsFromStructPointer(t reflect.Type) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the spec to the list of specs
|
// add the spec to the list of specs
|
||||||
cmd.specs = append(cmd.specs, &spec)
|
cmd.options = append(cmd.options, &spec)
|
||||||
|
|
||||||
// if this was an embedded field then we already returned true up above
|
// if this was an embedded field then we already returned true up above
|
||||||
return false
|
return false
|
||||||
|
@ -429,7 +495,7 @@ func (cmd *command) parseFieldsFromStructPointer(t reflect.Type) error {
|
||||||
|
|
||||||
// check that we don't have both positionals and subcommands
|
// check that we don't have both positionals and subcommands
|
||||||
var hasPositional bool
|
var hasPositional bool
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.options {
|
||||||
if spec.positional {
|
if spec.positional {
|
||||||
hasPositional = true
|
hasPositional = true
|
||||||
}
|
}
|
||||||
|
@ -531,8 +597,7 @@ func (p *Parser) process(args []string) error {
|
||||||
p.lastCmd = curCmd
|
p.lastCmd = curCmd
|
||||||
|
|
||||||
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
||||||
specs := make([]*spec, len(curCmd.specs))
|
specs := curCmd.specs()
|
||||||
copy(specs, curCmd.specs)
|
|
||||||
|
|
||||||
// deal with environment vars
|
// deal with environment vars
|
||||||
if !p.config.IgnoreEnv {
|
if !p.config.IgnoreEnv {
|
||||||
|
@ -571,11 +636,11 @@ func (p *Parser) process(args []string) error {
|
||||||
p.val(subcmd.dest)
|
p.val(subcmd.dest)
|
||||||
|
|
||||||
// add the new options to the set of allowed options
|
// add the new options to the set of allowed options
|
||||||
specs = append(specs, subcmd.specs...)
|
specs = append(specs, subcmd.specs()...)
|
||||||
|
|
||||||
// capture environment vars for these new options
|
// capture environment vars for these new options
|
||||||
if !p.config.IgnoreEnv {
|
if !p.config.IgnoreEnv {
|
||||||
err := p.captureEnvVars(subcmd.specs, wasPresent)
|
err := p.captureEnvVars(subcmd.specs(), wasPresent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
292
parse_test.go
292
parse_test.go
|
@ -288,6 +288,220 @@ func TestLongFlag(t *testing.T) {
|
||||||
assert.Equal(t, "xyz", args.Foo)
|
assert.Equal(t, "xyz", args.Foo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupRequired(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Foo *struct {
|
||||||
|
Bar string `arg:"required"`
|
||||||
|
} `arg:"group"`
|
||||||
|
}
|
||||||
|
err := parse("", &args)
|
||||||
|
require.Error(t, err, "--bar is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonPointerGroup(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Group struct {
|
||||||
|
Foo string
|
||||||
|
} `arg:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewParser(Config{IgnoreEnv: true}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonStructGroup(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
NotGroup int `arg:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewParser(Config{IgnoreEnv: true}, &args)
|
||||||
|
require.Error(t, err, "option groups must be structs or pointers to structs, but args.NotGroup is int")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonStructPtrGroup(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
NotGroup *int `arg:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewParser(Config{IgnoreEnv: true}, &args)
|
||||||
|
require.Error(t, err, "option groups must be structs or pointers to structs, but args.NotGroup is a pointer to int")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoSubcommandInGroup(t *testing.T) {
|
||||||
|
var args struct {
|
||||||
|
Group struct {
|
||||||
|
Sub *struct{} `arg:"subcommand"`
|
||||||
|
} `arg:"group"`
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewParser(Config{IgnoreEnv: true}, &args)
|
||||||
|
require.Error(t, err, "subcommands cannot be part of option groups")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupParsingWithoutArguments(t *testing.T) {
|
||||||
|
type perm struct {
|
||||||
|
Anent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type opt struct {
|
||||||
|
Ional string
|
||||||
|
}
|
||||||
|
|
||||||
|
type def struct {
|
||||||
|
Ault string `default:"permanent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
PermanentGroup perm `arg:"group:Permanent"`
|
||||||
|
OptionalGroup *opt `arg:"group:Optional"`
|
||||||
|
OptionalWithDefault *def `arg:"group:With default"`
|
||||||
|
Input string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts args
|
||||||
|
err := parse("", &opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, args{
|
||||||
|
OptionalWithDefault: &def{
|
||||||
|
Ault: "permanent",
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupParsingWithPositional(t *testing.T) {
|
||||||
|
type perm struct {
|
||||||
|
Anent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type opt struct {
|
||||||
|
Ional string
|
||||||
|
}
|
||||||
|
|
||||||
|
type def struct {
|
||||||
|
Ault string `default:"permanent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
PermanentGroup perm `arg:"group:Permanent"`
|
||||||
|
OptionalGroup *opt `arg:"group:Optional"`
|
||||||
|
OptionalWithDefault *def `arg:"group:With default"`
|
||||||
|
Input string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts args
|
||||||
|
err := parse("input", &opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, args{
|
||||||
|
OptionalWithDefault: &def{
|
||||||
|
Ault: "permanent",
|
||||||
|
},
|
||||||
|
Input: "input",
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupParsingOfGroupPointer(t *testing.T) {
|
||||||
|
type perm struct {
|
||||||
|
Anent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type opt struct {
|
||||||
|
Ional string
|
||||||
|
}
|
||||||
|
|
||||||
|
type def struct {
|
||||||
|
Ault string `default:"permanent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
PermanentGroup perm `arg:"group:Permanent"`
|
||||||
|
OptionalGroup *opt `arg:"group:Optional"`
|
||||||
|
OptionalWithDefault *def `arg:"group:With default"`
|
||||||
|
Input string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts args
|
||||||
|
err := parse("--ional pointer", &opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, args{
|
||||||
|
OptionalGroup: &opt{
|
||||||
|
Ional: "pointer",
|
||||||
|
},
|
||||||
|
OptionalWithDefault: &def{
|
||||||
|
Ault: "permanent",
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupParsingOfGroupStruct(t *testing.T) {
|
||||||
|
type perm struct {
|
||||||
|
Anent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type opt struct {
|
||||||
|
Ional string
|
||||||
|
}
|
||||||
|
|
||||||
|
type def struct {
|
||||||
|
Ault string `default:"permanent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
PermanentGroup perm `arg:"group:Permanent"`
|
||||||
|
OptionalGroup *opt `arg:"group:Optional"`
|
||||||
|
OptionalWithDefault *def `arg:"group:With default"`
|
||||||
|
Input string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts args
|
||||||
|
err := parse("--anent struct", &opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, args{
|
||||||
|
PermanentGroup: perm{
|
||||||
|
Anent: "struct",
|
||||||
|
},
|
||||||
|
OptionalWithDefault: &def{
|
||||||
|
Ault: "permanent",
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupParsingWithMixedTypes(t *testing.T) {
|
||||||
|
type perm struct {
|
||||||
|
Anent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type opt struct {
|
||||||
|
Ional string
|
||||||
|
}
|
||||||
|
|
||||||
|
type def struct {
|
||||||
|
Ault string `default:"permanent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
PermanentGroup perm `arg:"group:Permanent"`
|
||||||
|
OptionalGroup *opt `arg:"group:Optional"`
|
||||||
|
OptionalWithDefault *def `arg:"group:With default"`
|
||||||
|
Input string `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts args
|
||||||
|
err := parse("--foo bar -ional pointer --anent struct last", &opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, args{
|
||||||
|
Foo: "bar",
|
||||||
|
OptionalGroup: &opt{Ional: "pointer"},
|
||||||
|
PermanentGroup: perm{Anent: "struct"},
|
||||||
|
OptionalWithDefault: &def{Ault: "permanent"},
|
||||||
|
Input: "last",
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSlice(t *testing.T) {
|
func TestSlice(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Strings []string
|
Strings []string
|
||||||
|
@ -705,6 +919,84 @@ func TestMustParse(t *testing.T) {
|
||||||
assert.NotNil(t, parser)
|
assert.NotNil(t, parser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewParserWithEnv(t *testing.T) {
|
||||||
|
type simpleEnv struct {
|
||||||
|
Foo string `arg:"env"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// No env, no command line: value is empty
|
||||||
|
dest := &simpleEnv{}
|
||||||
|
_, err := parseWithEnv("", []string{}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &simpleEnv{Foo: ""}, dest)
|
||||||
|
|
||||||
|
// With env, no command line: value takes environment
|
||||||
|
dest = &simpleEnv{}
|
||||||
|
_, err = parseWithEnv("", []string{"FOO=env"}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &simpleEnv{Foo: "env"}, dest)
|
||||||
|
|
||||||
|
// No env, with command line: value takes argument
|
||||||
|
dest = &simpleEnv{}
|
||||||
|
_, err = parseWithEnv("--foo=cmd", []string{}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &simpleEnv{Foo: "cmd"}, dest)
|
||||||
|
|
||||||
|
// With env, with command line: value takes argument
|
||||||
|
dest = &simpleEnv{}
|
||||||
|
_, err = parseWithEnv("--foo=cmd", []string{"FOO=env"}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &simpleEnv{Foo: "cmd"}, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewParserWithEnvAndDefault(t *testing.T) {
|
||||||
|
type envWithDefault struct {
|
||||||
|
Foo string `arg:"env" default:"def"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// No env, no command line: value takes default
|
||||||
|
dest := &envWithDefault{}
|
||||||
|
_, err := parseWithEnv("", []string{}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &envWithDefault{Foo: "def"}, dest)
|
||||||
|
|
||||||
|
// With env, no command line: value takes environment
|
||||||
|
dest = &envWithDefault{}
|
||||||
|
_, err = parseWithEnv("", []string{"FOO=env"}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &envWithDefault{Foo: "env"}, dest)
|
||||||
|
|
||||||
|
// No env, with command line: value takes argument
|
||||||
|
dest = &envWithDefault{}
|
||||||
|
_, err = parseWithEnv("--foo=cmd", []string{}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &envWithDefault{Foo: "cmd"}, dest)
|
||||||
|
|
||||||
|
// With env, with command line: value takes argument
|
||||||
|
dest = &envWithDefault{}
|
||||||
|
_, err = parseWithEnv("--foo=cmd", []string{"FOO=env"}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &envWithDefault{Foo: "cmd"}, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewParserWithoutArgumentWithEnvAndDefault(t *testing.T) {
|
||||||
|
type noArgWithEnvAndWithDefault struct {
|
||||||
|
Foo string `arg:"--,env" default:"def"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// No env, no command line: value takes default
|
||||||
|
dest := &noArgWithEnvAndWithDefault{}
|
||||||
|
_, err := parseWithEnv("", []string{}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &noArgWithEnvAndWithDefault{Foo: "def"}, dest)
|
||||||
|
|
||||||
|
// With env, no command line: value takes environment
|
||||||
|
dest = &noArgWithEnvAndWithDefault{}
|
||||||
|
_, err = parseWithEnv("", []string{"FOO=env"}, dest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &noArgWithEnvAndWithDefault{Foo: "env"}, dest)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnvironmentVariable(t *testing.T) {
|
func TestEnvironmentVariable(t *testing.T) {
|
||||||
var args struct {
|
var args struct {
|
||||||
Foo string `arg:"env"`
|
Foo string `arg:"env"`
|
||||||
|
|
76
usage.go
76
usage.go
|
@ -70,7 +70,7 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
|
||||||
// writeUsageForSubcommand writes usage information for the given subcommand
|
// writeUsageForSubcommand writes usage information for the given subcommand
|
||||||
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
||||||
var positionals, longOptions, shortOptions []*spec
|
var positionals, longOptions, shortOptions []*spec
|
||||||
for _, spec := range cmd.specs {
|
for _, spec := range cmd.specs() {
|
||||||
switch {
|
switch {
|
||||||
case spec.positional:
|
case spec.positional:
|
||||||
positionals = append(positionals, spec)
|
positionals = append(positionals, spec)
|
||||||
|
@ -216,24 +216,18 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
|
||||||
|
|
||||||
// writeHelp writes the usage string for the given subcommand
|
// writeHelp writes the usage string for the given subcommand
|
||||||
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
var positionals, longOptions, shortOptions []*spec
|
|
||||||
for _, spec := range cmd.specs {
|
|
||||||
switch {
|
|
||||||
case spec.positional:
|
|
||||||
positionals = append(positionals, spec)
|
|
||||||
case spec.long != "":
|
|
||||||
longOptions = append(longOptions, spec)
|
|
||||||
case spec.short != "":
|
|
||||||
shortOptions = append(shortOptions, spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.description != "" {
|
if p.description != "" {
|
||||||
fmt.Fprintln(w, p.description)
|
fmt.Fprintln(w, p.description)
|
||||||
}
|
}
|
||||||
p.writeUsageForSubcommand(w, cmd)
|
p.writeUsageForSubcommand(w, cmd)
|
||||||
|
|
||||||
// write the list of positionals
|
// write the list of positionals
|
||||||
|
var positionals []*spec
|
||||||
|
for _, spec := range cmd.options {
|
||||||
|
if spec.positional {
|
||||||
|
positionals = append(positionals, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(positionals) > 0 {
|
if len(positionals) > 0 {
|
||||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||||
for _, spec := range positionals {
|
for _, spec := range positionals {
|
||||||
|
@ -242,26 +236,18 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the list of options with the short-only ones first to match the usage string
|
// write the list of options with the short-only ones first to match the usage string
|
||||||
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
|
p.writeHelpForArguments(w, cmd, "Options", "")
|
||||||
fmt.Fprint(w, "\nOptions:\n")
|
|
||||||
for _, spec := range shortOptions {
|
|
||||||
p.printOption(w, spec)
|
|
||||||
}
|
|
||||||
for _, spec := range longOptions {
|
|
||||||
p.printOption(w, spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// obtain a flattened list of options from all ancestors
|
// obtain a flattened list of options from all ancestors
|
||||||
var globals []*spec
|
var globals []*spec
|
||||||
ancestor := cmd.parent
|
ancestor := cmd.parent
|
||||||
for ancestor != nil {
|
for ancestor != nil {
|
||||||
globals = append(globals, ancestor.specs...)
|
globals = append(globals, ancestor.specs()...)
|
||||||
ancestor = ancestor.parent
|
ancestor = ancestor.parent
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the list of global options
|
// write the list of global options
|
||||||
if len(globals) > 0 {
|
if len(globals) > 0 || len(cmd.groups) > 0 {
|
||||||
fmt.Fprint(w, "\nGlobal options:\n")
|
fmt.Fprint(w, "\nGlobal options:\n")
|
||||||
for _, spec := range globals {
|
for _, spec := range globals {
|
||||||
p.printOption(w, spec)
|
p.printOption(w, spec)
|
||||||
|
@ -296,6 +282,45 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeHelpForArguments writes the list of short, long, and environment-only
|
||||||
|
// options in order.
|
||||||
|
func (p *Parser) writeHelpForArguments(w io.Writer, cmd *command, header, help string) {
|
||||||
|
var positionals, longOptions, shortOptions []*spec
|
||||||
|
for _, spec := range cmd.options {
|
||||||
|
switch {
|
||||||
|
case spec.positional:
|
||||||
|
positionals = append(positionals, spec)
|
||||||
|
case spec.long != "":
|
||||||
|
longOptions = append(longOptions, spec)
|
||||||
|
case spec.short != "":
|
||||||
|
shortOptions = append(shortOptions, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.parent != nil && len(shortOptions)+len(longOptions) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of options with the short-only ones first to match the usage string
|
||||||
|
fmt.Fprintf(w, "\n%v:\n", header)
|
||||||
|
if help != "" {
|
||||||
|
fmt.Fprintf(w, "\n%v\n\n", help)
|
||||||
|
}
|
||||||
|
for _, spec := range shortOptions {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
for _, spec := range longOptions {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of argument groups
|
||||||
|
if len(cmd.groups) > 0 {
|
||||||
|
for _, grpCmd := range cmd.groups {
|
||||||
|
p.writeHelpForArguments(w, grpCmd, fmt.Sprintf("%s options", grpCmd.name), grpCmd.help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||||
ways := make([]string, 0, 2)
|
ways := make([]string, 0, 2)
|
||||||
if spec.long != "" {
|
if spec.long != "" {
|
||||||
|
@ -304,6 +329,9 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||||
if spec.short != "" {
|
if spec.short != "" {
|
||||||
ways = append(ways, synopsis(spec, "-"+spec.short))
|
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||||
}
|
}
|
||||||
|
if spec.env != "" && len(ways) == 0 {
|
||||||
|
ways = append(ways, "(environment only)")
|
||||||
|
}
|
||||||
if len(ways) > 0 {
|
if len(ways) > 0 {
|
||||||
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultString, spec.env)
|
printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultString, spec.env)
|
||||||
}
|
}
|
||||||
|
|
199
usage_test.go
199
usage_test.go
|
@ -50,7 +50,7 @@ Options:
|
||||||
--optimize OPTIMIZE, -O OPTIMIZE
|
--optimize OPTIMIZE, -O OPTIMIZE
|
||||||
optimization level
|
optimization level
|
||||||
--ids IDS Ids
|
--ids IDS Ids
|
||||||
--values VALUES Values
|
--values VALUES Values [default: [3.14 42 256]]
|
||||||
--workers WORKERS, -w WORKERS
|
--workers WORKERS, -w WORKERS
|
||||||
number of workers to start [default: 10, env: WORKERS]
|
number of workers to start [default: 10, env: WORKERS]
|
||||||
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
||||||
|
@ -74,6 +74,7 @@ Options:
|
||||||
}
|
}
|
||||||
args.Name = "Foo Bar"
|
args.Name = "Foo Bar"
|
||||||
args.Value = 42
|
args.Value = 42
|
||||||
|
args.Values = []float64{3.14, 42, 256}
|
||||||
args.File = &NameDotName{"scratch", "txt"}
|
args.File = &NameDotName{"scratch", "txt"}
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -489,6 +490,200 @@ func TestNonexistentSubcommand(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUsageWithOptionGroup(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [--verbose] [--insecure] [--host HOST] [--port PORT] [--user USER] OUTPUT"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [--verbose] [--insecure] [--host HOST] [--port PORT] [--user USER] OUTPUT
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--verbose, -v verbosity level
|
||||||
|
|
||||||
|
Database options:
|
||||||
|
|
||||||
|
This block represents related arguments.
|
||||||
|
|
||||||
|
--insecure, -i disable tls
|
||||||
|
--host HOST hostname to connect to [default: localhost, env: DB_HOST]
|
||||||
|
--port PORT port to connect to [default: 3306, env: DB_PORT]
|
||||||
|
--user USER username to connect as [env: DB_USERNAME]
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
Insecure bool `arg:"-i,--insecure" help:"disable tls"`
|
||||||
|
Host string `arg:"--host,env:DB_HOST" help:"hostname to connect to" default:"localhost"`
|
||||||
|
Port string `arg:"--port,env:DB_PORT" help:"port to connect to" default:"3306"`
|
||||||
|
User string `arg:"--user,env:DB_USERNAME" help:"username to connect as"`
|
||||||
|
Password string `arg:"--,env:DB_PASSWORD" help:"password to connect with"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Verbose bool `arg:"-v" help:"verbosity level"`
|
||||||
|
Database *database `arg:"group" help:"This block represents related arguments."`
|
||||||
|
Output string `arg:"positional,required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{})
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithoutSubcommandAndOptionGroup(t *testing.T) {
|
||||||
|
expectedUsage := "Usage: example [-s] [--global] <command> [<args>]"
|
||||||
|
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example [-s] [--global] <command> [<args>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--global, -g global option
|
||||||
|
|
||||||
|
Global group options:
|
||||||
|
|
||||||
|
This block represents related arguments.
|
||||||
|
|
||||||
|
-s global something
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
foo Command A
|
||||||
|
bar Command B
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Global bool `arg:"-g" help:"global option"`
|
||||||
|
GlobalGroup *struct {
|
||||||
|
Something bool `arg:"-s,--" help:"global something"`
|
||||||
|
} `arg:"group:Global group" help:"This block represents related arguments."`
|
||||||
|
CommandA *struct {
|
||||||
|
OptionA bool `arg:"-a,--" help:"option for sub A"`
|
||||||
|
GroupA *struct {
|
||||||
|
GroupA bool `arg:"--group-a" help:"group belonging to cmd A"`
|
||||||
|
} `arg:"group:Group A" help:"This block belongs to command A."`
|
||||||
|
} `arg:"subcommand:foo" help:"Command A"`
|
||||||
|
CommandB *struct {
|
||||||
|
OptionB bool `arg:"-b,--" help:"option for sub B"`
|
||||||
|
GroupB *struct {
|
||||||
|
GroupB bool `arg:"--group-b" help:"group belonging to cmd B"`
|
||||||
|
NestedGroup *struct {
|
||||||
|
NestedGroup bool `arg:"--nested-group" help:"nested group belonging to group B of cmd B"`
|
||||||
|
} `arg:"group:Nested Group" help:"This block belongs to group B of command B."`
|
||||||
|
} `arg:"group:Group B" help:"This block belongs to command B."`
|
||||||
|
} `arg:"subcommand:bar" help:"Command B"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{})
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var help2 bytes.Buffer
|
||||||
|
p.WriteHelpForSubcommand(&help2)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
|
var usage2 bytes.Buffer
|
||||||
|
p.WriteUsageForSubcommand(&usage2)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsageWithSubcommandAndOptionGroup(t *testing.T) {
|
||||||
|
|
||||||
|
expectedUsage := "Usage: example bar [-b] [--group-b] [--nested-group]"
|
||||||
|
expectedHelp := `
|
||||||
|
Usage: example bar [-b] [--group-b] [--nested-group]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-b option for sub B
|
||||||
|
|
||||||
|
Group B options:
|
||||||
|
|
||||||
|
This block belongs to command B.
|
||||||
|
|
||||||
|
--group-b group belonging to cmd B
|
||||||
|
|
||||||
|
Nested Group options:
|
||||||
|
|
||||||
|
This block belongs to group B of command B.
|
||||||
|
|
||||||
|
--nested-group nested group belonging to group B of cmd B
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--global, -g global option
|
||||||
|
-s global something
|
||||||
|
--help, -h display this help and exit
|
||||||
|
`
|
||||||
|
|
||||||
|
var args struct {
|
||||||
|
Global bool `arg:"-g" help:"global option"`
|
||||||
|
GlobalGroup *struct {
|
||||||
|
Something bool `arg:"-s,--" help:"global something"`
|
||||||
|
} `arg:"group:Global group" help:"This block represents related arguments."`
|
||||||
|
CommandA *struct {
|
||||||
|
OptionA bool `arg:"-a,--" help:"option for sub A"`
|
||||||
|
GroupA *struct {
|
||||||
|
GroupA bool `arg:"--group-a" help:"group belonging to cmd A"`
|
||||||
|
} `arg:"group:Group A" help:"This block belongs to command A."`
|
||||||
|
} `arg:"subcommand:foo" help:"Command A"`
|
||||||
|
CommandB *struct {
|
||||||
|
OptionB bool `arg:"-b,--" help:"option for sub B"`
|
||||||
|
GroupB *struct {
|
||||||
|
GroupB bool `arg:"--group-b" help:"group belonging to cmd B"`
|
||||||
|
NestedGroup *struct {
|
||||||
|
NestedGroup bool `arg:"--nested-group" help:"nested group belonging to group B of cmd B"`
|
||||||
|
} `arg:"group:Nested Group" help:"This block belongs to group B of command B."`
|
||||||
|
} `arg:"group:Group B" help:"This block belongs to command B."`
|
||||||
|
} `arg:"subcommand:bar" help:"Command B"`
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Args[0] = "example"
|
||||||
|
p, err := NewParser(Config{}, &args)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_ = p.Parse([]string{"bar"})
|
||||||
|
|
||||||
|
var help bytes.Buffer
|
||||||
|
p.WriteHelp(&help)
|
||||||
|
assert.Equal(t, expectedHelp[1:], help.String())
|
||||||
|
|
||||||
|
var help2 bytes.Buffer
|
||||||
|
p.WriteHelpForSubcommand(&help2, "bar")
|
||||||
|
assert.Equal(t, expectedHelp[1:], help2.String())
|
||||||
|
|
||||||
|
var usage bytes.Buffer
|
||||||
|
p.WriteUsage(&usage)
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
|
||||||
|
|
||||||
|
var usage2 bytes.Buffer
|
||||||
|
p.WriteUsageForSubcommand(&usage2, "bar")
|
||||||
|
assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
|
||||||
|
}
|
||||||
|
|
||||||
func TestUsageWithoutLongNames(t *testing.T) {
|
func TestUsageWithoutLongNames(t *testing.T) {
|
||||||
expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
|
expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
|
||||||
|
|
||||||
|
@ -505,7 +700,7 @@ Options:
|
||||||
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
||||||
}
|
}
|
||||||
p, err := NewParser(Config{Program: "example"}, &args)
|
p, err := NewParser(Config{Program: "example"}, &args)
|
||||||
require.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var help bytes.Buffer
|
var help bytes.Buffer
|
||||||
p.WriteHelp(&help)
|
p.WriteHelp(&help)
|
||||||
|
|
Loading…
Reference in New Issue