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

Required flags #819

Merged
merged 63 commits into from
Aug 2, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
7e05320
Implement required flags
Dec 2, 2014
73e64a1
Add (required) to help of flags that are required.
Dec 2, 2014
4b2fcdb
Add tests for required flags
Dec 2, 2014
8f1fb06
Merge pull request #1 from ivey/required_flags
jhowarth Dec 2, 2014
cbd9529
Remove debugging
Mar 2, 2015
e67e05f
DRY error handling
Mar 2, 2015
6023f37
dry error messages
Mar 2, 2015
145da32
don't require flags when the help flag is included
Mar 2, 2015
b5844af
Merge pull request #2 from ivey/requiredFlags
jhowarth Mar 2, 2015
a6482d2
Merge remote-tracking branch 'upstream/master'
Mar 2, 2015
aba73ce
Copy the writer of the App to the subcommand App
Mar 3, 2015
138dbaa
Merge branch 'master' into required_flags
Jul 12, 2019
ce16301
reduce diff???
Jul 12, 2019
fa8187f
reduce diff
Jul 12, 2019
e6842c0
merge in test file
Jul 12, 2019
f7d5e2c
reduce diff
Jul 12, 2019
30a71dc
update Run command
Jul 12, 2019
9c299e7
reduce diff
Jul 12, 2019
0608059
reduce diff
Jul 12, 2019
3d2d697
reduce diff
Jul 12, 2019
af627c7
update func name
Jul 12, 2019
310bfeb
add required attr to generator
Jul 12, 2019
62e99ad
add IsRequired to generator
Jul 12, 2019
8a58b7e
remove manual isRequired funcs
Jul 12, 2019
922d231
./generate-flag-types cli -i flag-types.json -o flag_generated.go
Jul 12, 2019
6a2ae78
backwards compatible RequiredFlag implementation
Jul 12, 2019
f6777bf
quote the flag name
Jul 13, 2019
550ed20
update tests
Jul 13, 2019
746866c
add update integration with the help output
Jul 13, 2019
80d7e91
fill out test cases
Jul 13, 2019
cf82480
update tests
Jul 13, 2019
17108e1
tabs
Jul 13, 2019
f00f35c
docs
Jul 13, 2019
9293f5b
visually shorten logic
Jul 15, 2019
cdc7af7
add handling for multiple required flags
Jul 17, 2019
01d5cfa
use strings.Join
Jul 17, 2019
32d84d8
copy update
coilysiren Jul 17, 2019
cc1cf8c
wording shift
Jul 18, 2019
3002886
add subcommand
Jul 18, 2019
2299852
cleanup subcommand and specs
Jul 18, 2019
19140e1
show errors
Jul 18, 2019
d8985dc
reduce diff
Jul 18, 2019
7ce0af1
remove unused code
Jul 18, 2019
23f09ac
cleanup tests, check required flags in more places
Jul 29, 2019
9ec594d
reset generated flags changes
Jul 29, 2019
386b379
Revert "reset generated flags changes"
Jul 29, 2019
9438aba
remove showFlagError, we can use the help printer assertion to accomp…
Aug 2, 2019
714a73f
remove unused thing
Aug 2, 2019
95d3a86
update test to reflect app flag usage
Aug 2, 2019
7b9e16b
update test names
Aug 2, 2019
3d6eec8
add test cases
Aug 2, 2019
595382c
expand test cases
Aug 2, 2019
d4740d1
more test cases
Aug 2, 2019
78db152
add typed error assertions
Aug 2, 2019
45f2b3d
more test cases
Aug 2, 2019
ef9acb4
rename cases
Aug 2, 2019
fdd4d10
update comments
Aug 2, 2019
f21b22d
cleanup some issues with error display
Aug 2, 2019
38f9e16
add environment variable support :tada:
Aug 2, 2019
f4128a0
Update command.go
coilysiren Aug 2, 2019
d7ec4e8
add env var tests
Aug 2, 2019
60fb297
remove help assertion stuff
Aug 2, 2019
f8ba505
Merge branch 'master' into required-flags-take-2
asahasrabuddhe Aug 2, 2019
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
11 changes: 2 additions & 9 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import (
"time"
)

// printerFunc is the function signature for fmt.Fprintln
type printerFunc func(io.Writer, ...interface{}) (int, error)

var (
changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md"
appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL)
Expand All @@ -23,8 +20,6 @@ var (
errInvalidActionType = NewExitError("ERROR invalid Action type. "+
fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+
fmt.Sprintf("See %s", appActionDeprecationURL), 2)

showFlagError printerFunc = fmt.Fprintln
)

// App is the main structure of a cli application. It is recommended that
Expand Down Expand Up @@ -233,9 +228,8 @@ func (a *App) Run(arguments []string) (err error) {
return nil
}

cerr := checkRequiredFlags(a.Flags, set)
cerr := checkRequiredFlags(a.Flags, context)
if cerr != nil {
showFlagError(a.Writer, cerr)
ShowAppHelp(context)
return cerr
}
Expand Down Expand Up @@ -364,9 +358,8 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) {
}
}

cerr := checkRequiredFlags(a.Flags, set)
cerr := checkRequiredFlags(a.Flags, context)
if cerr != nil {
showFlagError(a.Writer, cerr)
ShowSubcommandHelp(context)
return cerr
}
Expand Down
152 changes: 105 additions & 47 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -880,76 +880,134 @@ func TestAppNoHelpFlag(t *testing.T) {
func TestRequiredFlagAppRunBehavior(t *testing.T) {
tdata := []struct {
testCase string
flags []Flag
appFlags []Flag
appRunInput []string
appCommands []Command
expectedAnError bool
}{
// assertion: empty input, when a required flag is present, errors
{
// expectations:
// - empty input, when a required flag is present, shows the help message
// - empty input, when a required flag is present, errors and shows the flag error message
testCase: "empty_input_with_required_flag",
appRunInput: []string{"command"},
flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
testCase: "error_case_empty_input_with_required_flag_on_app",
appRunInput: []string{"myCLI"},
appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
expectedAnError: true,
},
{
// expectations:
// - inputing --help, when a required flag is present, shows the help message
// - inputing --help, when a required flag is present, does not error
testCase: "help_input_with_required_flag",
appRunInput: []string{"command", "--help"},
flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
testCase: "error_case_empty_input_with_required_flag_on_command",
appRunInput: []string{"myCLI", "myCommand"},
appCommands: []Command{Command{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
expectedAnError: true,
},
{
testCase: "error_case_empty_input_with_required_flag_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand"},
appCommands: []Command{Command{
Name: "myCommand",
Subcommands: []Command{Command{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
}},
expectedAnError: true,
},
// assertion: inputing --help, when a required flag is present, does not error
{
testCase: "valid_case_help_input_with_required_flag_on_app",
appRunInput: []string{"myCLI", "--help"},
appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
},
{
testCase: "valid_case_help_input_with_required_flag_on_command",
appRunInput: []string{"myCLI", "myCommand", "--help"},
appCommands: []Command{Command{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
},
{
testCase: "valid_case_help_input_with_required_flag_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--help"},
appCommands: []Command{Command{
Name: "myCommand",
Subcommands: []Command{Command{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
}},
},
// assertion: giving optional input, when a required flag is present, errors
{
testCase: "error_case_optional_input_with_required_flag_on_app",
appRunInput: []string{"myCLI", "--optional", "cats"},
appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}},
expectedAnError: true,
},
{
// expectations:
// - giving optional input, when a required flag is present, shows the help message
// - giving optional input, when a required flag is present, errors and shows the flag error message
testCase: "optional_input_with_required_flag",
appRunInput: []string{"command", "--optional", "cats"},
flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}},
testCase: "error_case_optional_input_with_required_flag_on_command",
appRunInput: []string{"myCLI", "myCommand", "--optional", "cats"},
appCommands: []Command{Command{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}},
}},
expectedAnError: true,
},
{
testCase: "error_case_optional_input_with_required_flag_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--optional", "cats"},
appCommands: []Command{Command{
Name: "myCommand",
Subcommands: []Command{Command{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}},
}},
}},
expectedAnError: true,
},
// assertion: when a required flag is present, inputting that required flag does not error
{
testCase: "valid_case_required_flag_input_on_app",
appRunInput: []string{"myCLI", "--requiredFlag", "cats"},
appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
},
{
testCase: "valid_case_required_flag_input_on_command",
appRunInput: []string{"myCLI", "myCommand", "--requiredFlag", "cats"},
appCommands: []Command{Command{
Name: "myCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
},
{
testCase: "valid_case_required_flag_input_on_subcommand",
appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--requiredFlag", "cats"},
appCommands: []Command{Command{
Name: "myCommand",
Subcommands: []Command{Command{
Name: "mySubCommand",
Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}},
}},
}},
},
}
for _, test := range tdata {
t.Run(test.testCase, func(t *testing.T) {
// setup - undo showFlagError mock when finished
oldShowError := showFlagError
defer func() {
showFlagError = oldShowError
}()
// setup - mock showFlagError
var showFlagErrorWasCalled = false
showFlagError = func(writer io.Writer, err ...interface{}) (int, error) {
showFlagErrorWasCalled = true
return 0, nil
}
// setup - undo HelpPrinter mock when finished
oldPrinter := HelpPrinter
defer func() {
HelpPrinter = oldPrinter
}()
// setup - mock HelpPrinter
var helpPrinterWasCalled = false
HelpPrinter = func(w io.Writer, template string, data interface{}) {
helpPrinterWasCalled = true
}
// setup - app
// setup
app := NewApp()
app.Flags = test.flags
app.Flags = test.appFlags
app.Commands = test.appCommands

// logic under test
err := app.Run(test.appRunInput)

// assertions
if helpPrinterWasCalled == false {
t.Errorf("HelpPrinter expected to be called, but was not")
}
if test.expectedAnError && err == nil {
t.Errorf("expected an error, but there was none")
}
if test.expectedAnError && showFlagErrorWasCalled == false {
t.Errorf("showFlagError expected to be called, but was not")
if _, ok := err.(requiredFlagsErr); test.expectedAnError && !ok {
t.Errorf("expected a requiredFlagsErr, but got: %s", err)
}
if !test.expectedAnError && err != nil {
t.Errorf("did not expected an error, but there was one: %s", err)
Expand Down
3 changes: 1 addition & 2 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,8 @@ func (c Command) Run(ctx *Context) (err error) {
return nil
}

cerr := checkRequiredFlags(c.Flags, set)
cerr := checkRequiredFlags(c.Flags, context)
if cerr != nil {
showFlagError(context.App.Writer, cerr)
ShowCommandHelp(context, c.Name)
return cerr
}
Expand Down
40 changes: 26 additions & 14 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,29 +287,41 @@ func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
return nil
}

func checkRequiredFlags(flags []Flag, set *flag.FlagSet) error {
visited := make(map[string]bool)
set.Visit(func(f *flag.Flag) {
visited[f.Name] = true
})
type requiredFlagsErr interface {
error
getMissingFlags() []string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function isn't used anywhere, but I was reading https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully and it said

Assert errors for behaviour, not type

So that's what I'm trying to do here? 🤷‍♀ I'm open to suggestions for improvements!

}

type errRequiredFlags struct {
missingFlags []string
}

func (e *errRequiredFlags) Error() string {
numberOfMissingFlags := len(e.missingFlags)
if numberOfMissingFlags == 1 {
return fmt.Sprintf("Required flag %q not set", e.missingFlags[0])
}
joinedMissingFlags := strings.Join(e.missingFlags, ", ")
return fmt.Sprintf("Required flags %q not set", joinedMissingFlags)
}

func (e *errRequiredFlags) getMissingFlags() []string {
return e.missingFlags
}

func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr {
var missingFlags []string
for _, f := range flags {
if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
key := strings.Split(f.GetName(), ",")[0]
if !visited[key] {
missingFlags = append(missingFlags, f.GetName())
if !context.IsSet(key) {
missingFlags = append(missingFlags, key)
}
}
}

numberOfMissingFlags := len(missingFlags)
if numberOfMissingFlags == 1 {
return fmt.Errorf("Required flag %q not set", missingFlags[0])
}
if numberOfMissingFlags >= 2 {
joinedMissingFlags := strings.Join(missingFlags, ", ")
return fmt.Errorf("Required flags %q not set", joinedMissingFlags)
if len(missingFlags) != 0 {
return &errRequiredFlags{missingFlags: missingFlags}
}

return nil
Expand Down
26 changes: 25 additions & 1 deletion context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ func TestCheckRequiredFlags(t *testing.T) {
tdata := []struct {
testCase string
parseInput []string
envVarInput [2]string
flags []Flag
expectedAnError bool
expectedErrorContents []string
Expand Down Expand Up @@ -435,6 +436,13 @@ func TestCheckRequiredFlags(t *testing.T) {
},
parseInput: []string{"--requiredFlag", "myinput"},
},
{
testCase: "required_and_present_via_env_var",
flags: []Flag{
StringFlag{Name: "requiredFlag", Required: true, EnvVar: "REQUIRED_FLAG"},
},
envVarInput: [2]string{"REQUIRED_FLAG", "true"},
},
{
testCase: "required_and_optional",
flags: []Flag{
Expand All @@ -452,6 +460,15 @@ func TestCheckRequiredFlags(t *testing.T) {
parseInput: []string{"--optionalFlag", "myinput"},
expectedAnError: true,
},
{
testCase: "required_and_optional_and_optional_present_via_env_var",
flags: []Flag{
StringFlag{Name: "requiredFlag", Required: true},
StringFlag{Name: "optionalFlag", EnvVar: "OPTIONAL_FLAG"},
},
envVarInput: [2]string{"OPTIONAL_FLAG", "true"},
expectedAnError: true,
},
{
testCase: "required_and_optional_and_required_present",
flags: []Flag{
Expand Down Expand Up @@ -495,9 +512,16 @@ func TestCheckRequiredFlags(t *testing.T) {
flags.Apply(set)
}
set.Parse(test.parseInput)
if test.envVarInput[0] != "" {
os.Clearenv()
os.Setenv(test.envVarInput[0], test.envVarInput[1])
}
ctx := &Context{}
context := NewContext(ctx.App, set, ctx)
context.Command.Flags = test.flags

// logic under test
err := checkRequiredFlags(test.flags, set)
err := checkRequiredFlags(test.flags, context)

// assertions
if test.expectedAnError && err == nil {
Expand Down