Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow conditionally required members #112

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Expand Up @@ -42,6 +42,23 @@ $ ./example
Usage: example --id ID [--timeout TIMEOUT]
error: --id is required
```
### Conditional required arguments

```go
var args struct {
UserName string `default:"abc"`
UserID string `default:"123"`
Password string `arg:"required-if:username|userid"`
}
arg.MustParse(&args)
```

```shell
$ ./example --username=happytimes
Usage: example --id ID [--timeout TIMEOUT]
error: --password is required, because:
username was set
```

### Positional arguments

Expand Down
43 changes: 43 additions & 0 deletions parse.go
Expand Up @@ -52,6 +52,7 @@ type spec struct {
short string
multiple bool
required bool
requiredIf []string // list of other spec names
positional bool
separate bool
help string
Expand Down Expand Up @@ -319,7 +320,26 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
t.Name(), field.Name))
return false
}
if len(spec.requiredIf) > 0 { // mutually exculisive from required-if
errs = append(errs, fmt.Sprintf("cannot be %s while also having conditional requirements", key))
return false
}
spec.required = true
case key == "required-if":
if spec.required { // mutually exclusive from required
errs = append(errs, fmt.Sprintf("cannot have %s while also being required", key))
return false
}
requiredIfNames := strings.Split(value, "|") // list of names which relate to another spec
for index, requiredNames := range requiredIfNames {
requiredIfNames[index] = strings.TrimSpace(requiredNames)
}
// only assign to spec if values != ""
for _, v := range requiredIfNames {
if v != "" {
spec.requiredIf = append(spec.requiredIf, v)
}
}
case key == "positional":
spec.positional = true
case key == "separate":
Expand Down Expand Up @@ -645,6 +665,19 @@ func (p *Parser) process(args []string) error {
if spec.required {
return fmt.Errorf("%s is required", name)
}
requiredIfErrors := ""
for _, requiredName := range spec.requiredIf { // check if conditionally required
foundSpec := findSpec(specs, requiredName)
if foundSpec == nil {
continue
}
if wasPresent[foundSpec] {
requiredIfErrors = requiredIfErrors + fmt.Sprintf("\t %s was set \n", requiredName)
}
}
if len(requiredIfErrors) > 0 {
return fmt.Errorf("%s is required, because: \n %s", name, requiredIfErrors)
}
if spec.defaultVal != "" {
err := scalar.ParseValue(p.val(spec.dest), spec.defaultVal)
if err != nil {
Expand Down Expand Up @@ -743,6 +776,16 @@ func findOption(specs []*spec, name string) *spec {
return nil
}

// findOption finds an option from its name not excluding positional, or returns null if no spec is found
func findSpec(specs []*spec, name string) *spec {
for _, spec := range specs {
if spec.long == name || spec.short == name {
return spec
}
}
return nil
}

// findSubcommand finds a subcommand using its name, or returns null if no subcommand is found
func findSubcommand(cmds []*command, name string) *command {
for _, cmd := range cmds {
Expand Down
83 changes: 83 additions & 0 deletions parse_test.go
Expand Up @@ -1213,3 +1213,86 @@ func TestDefaultValuesNotAllowedWithSlice(t *testing.T) {
err := parse("", &args)
assert.EqualError(t, err, ".A: default values are not supported for slice fields")
}

func TestRequiredIf(t *testing.T) {
var args struct {
Dummy string `default:"123"`
Foo string `arg:"required-if:dummy"`
}
err := parse("--dummy=3", &args)
require.Error(t, err, "--foo is required")
}

func TestRequiredIfPositional(t *testing.T) {
var args struct {
Input string `arg:"positional"`
Output string `arg:"required-if:input"`
}
err := parse("foo", &args)
assert.Error(t, err)
}

func TestRequiredIfPositionalMultiple(t *testing.T) {
var args struct {
Input string `arg:"positional"`
Multiple []string `arg:"positional,required-if:input"`
}
err := parse("foo", &args)
assert.Error(t, err)
}

func TestMissingRequiredIf(t *testing.T) {
var args struct {
Foo string `arg:"required-if:x"`
X []string `arg:"positional"`
}
err := parse("x", &args)
assert.Error(t, err)
}

func TestDefaultPositionalRequiredIfValues(t *testing.T) {
var args struct {
A int `arg:"positional" default:"123"`
B *int `arg:"positional" default:"123"`
C string `arg:"positional" default:"abc"`
D *string `arg:"positional" default:"abc"`
E float64 `arg:"positional" default:"1.23"`
F *float64 `arg:"positional" default:"1.23"`
G bool `arg:"positional" default:"true"`
H *bool `arg:"required-if:a|b" default:"true"`
}
err := parse("456 789", &args)
require.Error(t, err)
assert.Equal(t, 456, args.A)
assert.Equal(t, 789, *args.B)
assert.Equal(t, "abc", args.C)
assert.Equal(t, "abc", *args.D)
assert.Equal(t, 1.23, args.E)
assert.Equal(t, 1.23, *args.F)
assert.True(t, args.G)
assert.Nil(t, args.H)
}

func TestRequiredIfValues(t *testing.T) {
var args struct {
A int `arg:"positional" default:"123"`
B *int `arg:"positional" default:"123"`
C string `arg:"positional" default:"abc"`
D *string `arg:"positional" default:"abc"`
E float64 `arg:"positional" default:"1.23"`
F *float64 `arg:"positional" default:"1.23"`
G bool `arg:"positional" default:"true"`
H *bool `arg:"required-if:a|b" default:"true"`
}
err := parse("456 789 -h=true", &args)
require.NoError(t, err)
assert.Equal(t, 456, args.A)
assert.Equal(t, 789, *args.B)
assert.Equal(t, "abc", args.C)
assert.Equal(t, "abc", *args.D)
assert.Equal(t, 1.23, args.E)
assert.Equal(t, 1.23, *args.F)
assert.True(t, args.G)
assert.NotNil(t, args.H)
assert.True(t, *args.H)
}
32 changes: 31 additions & 1 deletion usage.go
Expand Up @@ -129,8 +129,13 @@ func (p *Parser) WriteHelp(w io.Writer) {

// writeHelp writes the usage string for the given subcommand
func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
var positionals, options []*spec
var positionals, options, requireds, requiredIf []*spec
for _, spec := range cmd.specs {
if spec.required {
requireds = append(requireds, spec)
} else if len(spec.requiredIf) > 0 {
requiredIf = append(requiredIf, spec)
}
if spec.positional {
positionals = append(positionals, spec)
} else {
Expand All @@ -143,6 +148,31 @@ func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
}
p.writeUsageForCommand(w, cmd)

//write the list of required arguments
if len(requireds) > 0 {
fmt.Fprint(w, "\nRequired arguments:\n")
for _, spec := range requireds {
printTwoCols(w, spec.placeholder, spec.help, "")
}
}

//write the list of required-if list arguments
if len(requiredIf) > 0 {
fmt.Fprint(w, "\nConditionally required arguments:\n")
for _, spec := range requiredIf {
requiredString := "required if: "
for index, v := range spec.requiredIf {
requiredString += v
if index+1 < len(spec.requiredIf) {
requiredString += " ,"
} else {
requiredString += " has be set"
}
}
printTwoCols(w, spec.placeholder, requiredString, "")
}
}

// write the list of positionals
if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n")
Expand Down
17 changes: 15 additions & 2 deletions usage_test.go
Expand Up @@ -251,16 +251,26 @@ Options:
}

func TestRequiredMultiplePositionals(t *testing.T) {
expectedHelp := `Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
expectedHelp := `Usage: example [--makerequired MAKEREQUIRED] [--requiredvariable REQUIREDVARIABLE] REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]

Required arguments:
REQUIREDMULTIPLE required multiple positional

Conditionally required arguments:
REQUIREDVARIABLE required if: makerequired has be set

Positional arguments:
REQUIREDMULTIPLE required multiple positional

Options:
--makerequired MAKEREQUIRED [default: dog]
--requiredvariable REQUIREDVARIABLE
--help, -h display this help and exit
`
var args struct {
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
MakeRequired string `default:"dog"`
RequiredVariable string `arg:"required-if:makerequired|"`
}

p, err := NewParser(Config{}, &args)
Expand All @@ -271,9 +281,12 @@ Options:
assert.Equal(t, expectedHelp, help.String())
}

func TestUsageWithNestedSubcommands(t *testing.T) {
func TestUsagWithNestedSubcommands(t *testing.T) {
expectedHelp := `Usage: example child nested [--enable] OUTPUT

Required arguments:
OUTPUT

Positional arguments:
OUTPUT

Expand Down