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
|
||||
|
||||
https://godoc.org/github.com/alexflint/go-arg
|
||||
|
|
|
@ -253,6 +253,55 @@ func Example_helpTextWithSubcommand() {
|
|||
// 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
|
||||
func Example_helpTextWhenUsingSubcommand() {
|
||||
// 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
|
||||
help string
|
||||
dest path
|
||||
specs []*spec
|
||||
options []*spec
|
||||
subcommands []*command
|
||||
groups []*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
|
||||
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))
|
||||
}
|
||||
|
||||
err := p.cmd.parseFieldsFromStructPointer(t)
|
||||
err := p.cmd.parseFieldsFromStructPointer(t, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -214,7 +226,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
|||
// for backwards compatibility, add nonzero field values as defaults
|
||||
// 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)
|
||||
for _, spec := range p.cmd.specs {
|
||||
for _, spec := range p.cmd.specs() {
|
||||
// get the value
|
||||
defaultString, defaultValue, err := p.defaultVal(spec.dest)
|
||||
if err != nil {
|
||||
|
@ -248,7 +260,7 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
|||
// parseFieldsFromStructPointer ensures the destination structure is a pointer
|
||||
// to a struct. This function should be called when parsing commands or
|
||||
// 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
|
||||
if t.Kind() != reflect.Ptr {
|
||||
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",
|
||||
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
|
||||
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||
// 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)
|
||||
|
||||
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
|
||||
if subCmd.name == "" {
|
||||
subCmd.name = strings.ToLower(field.Name)
|
||||
}
|
||||
|
||||
// parse the subcommand recursively
|
||||
err := subCmd.parseFieldsFromStructPointer(field.Type)
|
||||
err := subCmd.parseFieldsFromStructPointer(field.Type, false)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
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:
|
||||
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
||||
return false
|
||||
|
@ -417,7 +483,7 @@ func (cmd *command) parseFieldsFromStructPointer(t reflect.Type) error {
|
|||
}
|
||||
|
||||
// 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
|
||||
return false
|
||||
|
@ -429,7 +495,7 @@ func (cmd *command) parseFieldsFromStructPointer(t reflect.Type) error {
|
|||
|
||||
// check that we don't have both positionals and subcommands
|
||||
var hasPositional bool
|
||||
for _, spec := range cmd.specs {
|
||||
for _, spec := range cmd.options {
|
||||
if spec.positional {
|
||||
hasPositional = true
|
||||
}
|
||||
|
@ -531,8 +597,7 @@ func (p *Parser) process(args []string) error {
|
|||
p.lastCmd = curCmd
|
||||
|
||||
// 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))
|
||||
copy(specs, curCmd.specs)
|
||||
specs := curCmd.specs()
|
||||
|
||||
// deal with environment vars
|
||||
if !p.config.IgnoreEnv {
|
||||
|
@ -571,11 +636,11 @@ func (p *Parser) process(args []string) error {
|
|||
p.val(subcmd.dest)
|
||||
|
||||
// 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
|
||||
if !p.config.IgnoreEnv {
|
||||
err := p.captureEnvVars(subcmd.specs, wasPresent)
|
||||
err := p.captureEnvVars(subcmd.specs(), wasPresent)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
var args struct {
|
||||
Strings []string
|
||||
|
@ -705,6 +919,84 @@ func TestMustParse(t *testing.T) {
|
|||
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) {
|
||||
var args struct {
|
||||
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
|
||||
func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
|
||||
var positionals, longOptions, shortOptions []*spec
|
||||
for _, spec := range cmd.specs {
|
||||
for _, spec := range cmd.specs() {
|
||||
switch {
|
||||
case spec.positional:
|
||||
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
|
||||
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 != "" {
|
||||
fmt.Fprintln(w, p.description)
|
||||
}
|
||||
p.writeUsageForSubcommand(w, cmd)
|
||||
|
||||
// write the list of positionals
|
||||
var positionals []*spec
|
||||
for _, spec := range cmd.options {
|
||||
if spec.positional {
|
||||
positionals = append(positionals, spec)
|
||||
}
|
||||
}
|
||||
if len(positionals) > 0 {
|
||||
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||
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
|
||||
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
|
||||
fmt.Fprint(w, "\nOptions:\n")
|
||||
for _, spec := range shortOptions {
|
||||
p.printOption(w, spec)
|
||||
}
|
||||
for _, spec := range longOptions {
|
||||
p.printOption(w, spec)
|
||||
}
|
||||
}
|
||||
p.writeHelpForArguments(w, cmd, "Options", "")
|
||||
|
||||
// obtain a flattened list of options from all ancestors
|
||||
var globals []*spec
|
||||
ancestor := cmd.parent
|
||||
for ancestor != nil {
|
||||
globals = append(globals, ancestor.specs...)
|
||||
globals = append(globals, ancestor.specs()...)
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
|
||||
// write the list of global options
|
||||
if len(globals) > 0 {
|
||||
if len(globals) > 0 || len(cmd.groups) > 0 {
|
||||
fmt.Fprint(w, "\nGlobal options:\n")
|
||||
for _, spec := range globals {
|
||||
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) {
|
||||
ways := make([]string, 0, 2)
|
||||
if spec.long != "" {
|
||||
|
@ -304,6 +329,9 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
|
|||
if spec.short != "" {
|
||||
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||
}
|
||||
if spec.env != "" && len(ways) == 0 {
|
||||
ways = append(ways, "(environment only)")
|
||||
}
|
||||
if len(ways) > 0 {
|
||||
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
|
||||
optimization level
|
||||
--ids IDS Ids
|
||||
--values VALUES Values
|
||||
--values VALUES Values [default: [3.14 42 256]]
|
||||
--workers WORKERS, -w WORKERS
|
||||
number of workers to start [default: 10, env: WORKERS]
|
||||
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
|
||||
|
@ -74,6 +74,7 @@ Options:
|
|||
}
|
||||
args.Name = "Foo Bar"
|
||||
args.Value = 42
|
||||
args.Values = []float64{3.14, 42, 256}
|
||||
args.File = &NameDotName{"scratch", "txt"}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
|
@ -489,6 +490,200 @@ func TestNonexistentSubcommand(t *testing.T) {
|
|||
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) {
|
||||
expectedUsage := "Usage: example [-a PLACEHOLDER] -b SHORTONLY2"
|
||||
|
||||
|
@ -505,7 +700,7 @@ Options:
|
|||
ShortOnly2 string `arg:"-b,--,required" help:"some help2"`
|
||||
}
|
||||
p, err := NewParser(Config{Program: "example"}, &args)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var help bytes.Buffer
|
||||
p.WriteHelp(&help)
|
||||
|
|
Loading…
Reference in New Issue