diff options
| -rw-r--r-- | README.md | 17 | ||||
| -rw-r--r-- | example_test.go | 7 | ||||
| -rw-r--r-- | parse.go | 22 | ||||
| -rw-r--r-- | parse_test.go | 27 | ||||
| -rw-r--r-- | usage.go | 13 | ||||
| -rw-r--r-- | usage_test.go | 110 |
6 files changed, 173 insertions, 23 deletions
@@ -120,6 +120,23 @@ $ WORKERS='1,99' ./example Workers: [1 99] ``` +You can also have an environment variable that doesn't match the arg name: + +```go +var args struct { + Workers int `arg:"--count,env:NUM_WORKERS"` +} +arg.MustParse(&args) +fmt.Println("Workers:", args.Workers) +``` + +``` +$ NUM_WORKERS=6 ./example +Workers: 6 +$ NUM_WORKERS=6 ./example --count 4 +Workers: 4 +``` + ### Usage strings ```go var args struct { diff --git a/example_test.go b/example_test.go index 4bd7632..d3622bf 100644 --- a/example_test.go +++ b/example_test.go @@ -163,6 +163,7 @@ func Example_helpText() { // This is only necessary when running inside golang's runnable example harness mustParseExit = func(int) {} + mustParseOut = os.Stdout MustParse(&args) @@ -195,6 +196,7 @@ func Example_helpPlaceholder() { // This is only necessary when running inside golang's runnable example harness mustParseExit = func(int) {} + mustParseOut = os.Stdout MustParse(&args) @@ -235,6 +237,7 @@ func Example_helpTextWithSubcommand() { // This is only necessary when running inside golang's runnable example harness mustParseExit = func(int) {} + mustParseOut = os.Stdout MustParse(&args) @@ -272,6 +275,7 @@ func Example_helpTextWhenUsingSubcommand() { // This is only necessary when running inside golang's runnable example harness mustParseExit = func(int) {} + mustParseOut = os.Stdout MustParse(&args) @@ -392,6 +396,7 @@ func Example_errorText() { // This is only necessary when running inside golang's runnable example harness mustParseExit = func(int) {} + mustParseOut = os.Stdout MustParse(&args) @@ -415,6 +420,7 @@ func Example_errorTextForSubcommand() { // This is only necessary when running inside golang's runnable example harness mustParseExit = func(int) {} + mustParseOut = os.Stdout MustParse(&args) @@ -450,6 +456,7 @@ func Example_subcommand() { // This is only necessary when running inside golang's runnable example harness mustParseExit = func(int) {} + mustParseOut = os.Stdout MustParse(&args) @@ -56,7 +56,7 @@ type spec struct { env string // the name of the environment variable for this option, or empty for none defaultValue reflect.Value // default value for this option defaultString string // default value for this option, in string form to be displayed in help text - placeholder string // name of the data in help + placeholder string // placeholder string in help } // command represents a named subcommand, or the top-level command @@ -76,27 +76,21 @@ var ErrHelp = errors.New("help requested by user") // ErrVersion indicates that the builtin --version was provided var ErrVersion = errors.New("version requested by user") -// for monkey patching in example code +// for monkey patching in example and test code var mustParseExit = os.Exit +var mustParseOut io.Writer = os.Stdout // MustParse processes command line arguments and exits upon failure func MustParse(dest ...interface{}) *Parser { - return mustParse(Config{Exit: mustParseExit}, dest...) + return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, dest...) } // mustParse is a helper that facilitates testing func mustParse(config Config, dest ...interface{}) *Parser { - if config.Exit == nil { - config.Exit = os.Exit - } - if config.Out == nil { - config.Out = os.Stdout - } - p, err := NewParser(config, dest...) if err != nil { fmt.Fprintln(config.Out, err) - config.Exit(-1) + config.Exit(2) return nil } @@ -342,9 +336,8 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { spec.help = help } - // Look at the tag - var isSubcommand bool // tracks whether this field is a subcommand - + // process each comma-separated part of the tag + var isSubcommand bool for _, key := range strings.Split(tag, ",") { if key == "" { continue @@ -414,6 +407,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { } } + // placeholder is the string used in the help text like this: "--somearg PLACEHOLDER" placeholder, hasPlaceholder := field.Tag.Lookup("placeholder") if hasPlaceholder { spec.placeholder = placeholder diff --git a/parse_test.go b/parse_test.go index d53b483..5bc781c 100644 --- a/parse_test.go +++ b/parse_test.go @@ -692,6 +692,21 @@ func TestMustParse(t *testing.T) { assert.NotNil(t, parser) } +func TestMustParseError(t *testing.T) { + var args struct { + Foo []string `default:""` + } + var exitCode int + var stdout bytes.Buffer + mustParseExit = func(code int) { exitCode = code } + mustParseOut = &stdout + os.Args = []string{"example"} + parser := MustParse(&args) + assert.Nil(t, parser) + assert.Equal(t, 2, exitCode) + assert.Contains(t, stdout.String(), "default values are not supported for slice or map fields") +} + func TestEnvironmentVariable(t *testing.T) { var args struct { Foo string `arg:"env"` @@ -906,7 +921,7 @@ func TestParserMustParse(t *testing.T) { }{ {name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"}, {name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"}, - {name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: -1, output: ""}, + {name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: 2, output: ""}, } for _, tt := range tests { @@ -1556,7 +1571,7 @@ func TestMustParseInvalidParser(t *testing.T) { } parser := mustParse(Config{Out: &stdout, Exit: exit}, &args) assert.Nil(t, parser) - assert.Equal(t, -1, exitCode) + assert.Equal(t, 2, exitCode) } func TestMustParsePrintsHelp(t *testing.T) { @@ -1737,3 +1752,11 @@ func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) { require.NotNil(t, args.Sub) assert.True(t, args.Sub.Guard) } + +func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) { + var args struct{} + p, err := NewParser(Config{}, &args) + require.NoError(t, err) + assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison + assert.Equal(t, p.config.Out, os.Stdout) +} @@ -9,13 +9,13 @@ import ( // the width of the left column const colWidth = 25 -// Fail prints usage information to stderr and exits with non-zero status +// Fail prints usage information to p.Config.Out and exits with status code 2. func (p *Parser) Fail(msg string) { p.FailSubcommand(msg) } -// FailSubcommand prints usage information for a specified subcommand to stderr, -// then exits with non-zero status. To write usage information for a top-level +// FailSubcommand prints usage information for a specified subcommand to p.Config.Out, +// then exits with status code 2. To write usage information for a top-level // subcommand, provide just the name of that subcommand. To write usage // information for a subcommand that is nested under another subcommand, provide // a sequence of subcommand names starting with the top-level subcommand and so @@ -27,7 +27,7 @@ func (p *Parser) FailSubcommand(msg string, subcommand ...string) error { } fmt.Fprintln(p.config.Out, "error:", msg) - p.config.Exit(-1) + p.config.Exit(2) return nil } @@ -328,7 +328,10 @@ func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) { } func synopsis(spec *spec, form string) string { - if spec.cardinality == zero { + // if the user omits the placeholder tag then we pick one automatically, + // but if the user explicitly specifies an empty placeholder then we + // leave out the placeholder in the help message + if spec.cardinality == zero || spec.placeholder == "" { return form } return form + " " + spec.placeholder diff --git a/usage_test.go b/usage_test.go index b1693a9..71324eb 100644 --- a/usage_test.go +++ b/usage_test.go @@ -260,6 +260,39 @@ Options: assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String())) } +type userDefinedVersionFlag struct { + ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"` +} + +// Version returns the version for this program +func (userDefinedVersionFlag) Version() string { + return "example 3.2.1" +} + +func TestUsageWithUserDefinedVersionFlag(t *testing.T) { + expectedUsage := "example 3.2.1\nUsage: example [--version]" + + expectedHelp := ` +example 3.2.1 +Usage: example [--version] + +Options: + --version this is a user-defined version flag + --help, -h display this help and exit +` + os.Args[0] = "example" + p, err := NewParser(Config{}, &userDefinedVersionFlag{}) + require.NoError(t, err) + + 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())) +} + type described struct{} // Described returns the description for this program @@ -415,6 +448,50 @@ Options: assert.Equal(t, expectedUsage, usage.String()) } +func TestUsageWithSubcommands(t *testing.T) { + expectedUsage := "Usage: example child [--values VALUES]" + + expectedHelp := ` +Usage: example child [--values VALUES] + +Options: + --values VALUES Values + +Global options: + --verbose, -v verbosity level + --help, -h display this help and exit +` + + var args struct { + Verbose bool `arg:"-v" help:"verbosity level"` + Child *struct { + Values []float64 `help:"Values"` + } `arg:"subcommand:child"` + } + + os.Args[0] = "example" + p, err := NewParser(Config{}, &args) + require.NoError(t, err) + + _ = p.Parse([]string{"child"}) + + var help bytes.Buffer + p.WriteHelp(&help) + assert.Equal(t, expectedHelp[1:], help.String()) + + var help2 bytes.Buffer + p.WriteHelpForSubcommand(&help2, "child") + 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, "child") + assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String())) +} + func TestUsageWithNestedSubcommands(t *testing.T) { expectedUsage := "Usage: example child nested [--enable] OUTPUT" @@ -524,6 +601,35 @@ Options: assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String())) } +func TestUsageWithEmptyPlaceholder(t *testing.T) { + expectedUsage := "Usage: example [-a] [--b] [--c]" + + expectedHelp := ` +Usage: example [-a] [--b] [--c] + +Options: + -a some help for a + --b some help for b + --c, -c some help for c + --help, -h display this help and exit +` + var args struct { + ShortOnly string `arg:"-a,--" placeholder:"" help:"some help for a"` + LongOnly string `arg:"--b" placeholder:"" help:"some help for b"` + Both string `arg:"-c,--c" placeholder:"" help:"some help for c"` + } + p, err := NewParser(Config{Program: "example"}, &args) + require.NoError(t, err) + + 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 TestUsageWithShortFirst(t *testing.T) { expectedUsage := "Usage: example [-c CAT] [--dog DOG]" @@ -632,7 +738,7 @@ error: something went wrong p.Fail("something went wrong") assert.Equal(t, expectedStdout[1:], stdout.String()) - assert.Equal(t, -1, exitCode) + assert.Equal(t, 2, exitCode) } func TestFailSubcommand(t *testing.T) { @@ -655,7 +761,7 @@ error: something went wrong require.NoError(t, err) assert.Equal(t, expectedStdout[1:], stdout.String()) - assert.Equal(t, -1, exitCode) + assert.Equal(t, 2, exitCode) } type lengthOf struct { |
