summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md17
-rw-r--r--example_test.go7
-rw-r--r--parse.go22
-rw-r--r--parse_test.go27
-rw-r--r--usage.go13
-rw-r--r--usage_test.go110
6 files changed, 173 insertions, 23 deletions
diff --git a/README.md b/README.md
index 7b3d148..cac383f 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/parse.go b/parse.go
index 169e8cc..2bed8bf 100644
--- a/parse.go
+++ b/parse.go
@@ -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)
+}
diff --git a/usage.go b/usage.go
index 0c68ee7..f5e4b38 100644
--- a/usage.go
+++ b/usage.go
@@ -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 {