help,usage and error messages and tests

This commit is contained in:
Ilja Neumann 2023-06-03 12:47:47 +02:00
parent b928a1839a
commit 18623d869b
4 changed files with 88 additions and 33 deletions

View File

@ -418,10 +418,14 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
spec.placeholder = strings.ToUpper(spec.field.Name) spec.placeholder = strings.ToUpper(spec.field.Name)
} }
if spec.env == "" && emptyLongAndShort(kvPairs) { noFormSpecs := emptyLongAndShort(kvPairs)
if spec.env == "" && noFormSpecs {
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only", errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
t.Name(), field.Name)) t.Name(), field.Name))
return false return false
} else if spec.env != "" && noFormSpecs {
spec.envOnly = true
} }
// if this is a subcommand then we've done everything we need to do // if this is a subcommand then we've done everything we need to do
@ -759,10 +763,16 @@ func (p *Parser) process(args []string) error {
} }
if spec.required { if spec.required {
if spec.envOnly {
msg := fmt.Sprintf("environment variable %s is required", spec.env)
return errors.New(msg)
}
msg := fmt.Sprintf("%s is required", name) msg := fmt.Sprintf("%s is required", name)
if spec.env != "" { if spec.env != "" {
msg += " (or environment variable " + spec.env + ")" msg += " (or environment variable " + spec.env + ")"
} }
return errors.New(msg) return errors.New(msg)
} }

View File

@ -227,6 +227,14 @@ func TestRequiredWithEnv(t *testing.T) {
require.Error(t, err, "--foo is required (or environment variable FOO)") require.Error(t, err, "--foo is required (or environment variable FOO)")
} }
func TestRequiredWithEnvOnly(t *testing.T) {
var args struct {
Foo string `arg:"required,--,-,env:FOO"`
}
_, err := parseWithEnv("", []string{}, &args)
require.Error(t, err, "environment variable FOO is required")
}
func TestShortFlag(t *testing.T) { func TestShortFlag(t *testing.T) {
var args struct { var args struct {
Foo string `arg:"-f"` Foo string `arg:"-f"`
@ -845,6 +853,24 @@ func TestDefaultValuesIgnored(t *testing.T) {
assert.Equal(t, "", args.Foo) assert.Equal(t, "", args.Foo)
} }
func TestRequiredEnvironmentOnlyVariableIsMissing(t *testing.T) {
var args struct {
Foo string `arg:"required,--,env:FOO"`
}
_, err := parseWithEnv("", []string{""}, &args)
assert.Error(t, err)
}
func TestOptionalEnvironmentOnlyVariable(t *testing.T) {
var args struct {
Foo string `arg:"env:FOO"`
}
_, err := parseWithEnv("", []string{}, &args)
assert.NoError(t, err)
}
func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) { func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
var args struct { var args struct {
Sub *struct { Sub *struct {

View File

@ -312,7 +312,18 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
} }
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) { func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
printTwoCols(w, spec.env, "", "", "") ways := make([]string, 0, 2)
if spec.required {
ways = append(ways, "Required.")
} else {
ways = append(ways, "Optional.")
}
if spec.help != "" {
ways = append(ways, spec.help)
}
printTwoCols(w, spec.env, strings.Join(ways, " "), spec.defaultString, "")
} }
// lookupCommand finds a subcommand based on a sequence of subcommand names. The // lookupCommand finds a subcommand based on a sequence of subcommand names. The

View File

@ -56,6 +56,10 @@ Options:
--testenv TESTENV, -a TESTENV [env: TEST_ENV] --testenv TESTENV, -a TESTENV [env: TEST_ENV]
--file FILE, -f FILE File with mandatory extension [default: scratch.txt] --file FILE, -f FILE File with mandatory extension [default: scratch.txt]
--help, -h display this help and exit --help, -h display this help and exit
Environment variables:
API_KEY Required. Only via env-var for security reasons
TRACE Optional. Record low-level trace
` `
var args struct { var args struct {
@ -70,6 +74,8 @@ Options:
Values []float64 `help:"Values"` Values []float64 `help:"Values"`
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"` Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
TestEnv string `arg:"-a,env:TEST_ENV"` TestEnv string `arg:"-a,env:TEST_ENV"`
ApiKey string `arg:"required,-,--,env:API_KEY" help:"Only via env-var for security reasons"`
Trace bool `arg:"-,--,env" help:"Record low-level trace"`
File *NameDotName `arg:"-f" help:"File with mandatory extension"` File *NameDotName `arg:"-f" help:"File with mandatory extension"`
} }
args.Name = "Foo Bar" args.Name = "Foo Bar"
@ -554,12 +560,14 @@ Options:
--help, -h display this help and exit --help, -h display this help and exit
Environment variables: Environment variables:
ENVONLY ENVONLY Optional.
CUSTOM ENVONLY2 Optional.
CUSTOM Optional.
` `
var args struct { var args struct {
Short string `arg:"--,-s,env"` Short string `arg:"--,-s,env"`
EnvOnly string `arg:"--,env"` EnvOnly string `arg:"--,env"`
EnvOnly2 string `arg:"--,-,env"`
EnvOnlyOverriden string `arg:"--,env:CUSTOM"` EnvOnlyOverriden string `arg:"--,env:CUSTOM"`
} }
@ -575,6 +583,35 @@ Environment variables:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String())) assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
} }
func TestEnvOnlyArgs(t *testing.T) {
expectedUsage := "Usage: example [--arg ARG]"
expectedHelp := `
Usage: example [--arg ARG]
Options:
--arg ARG, -a ARG [env: MY_ARG]
--help, -h display this help and exit
Environment variables:
AUTH_KEY Required.
`
var args struct {
ArgParam string `arg:"-a,--arg,env:MY_ARG"`
AuthKey string `arg:"required,--,env:AUTH_KEY"`
}
p, err := NewParser(Config{Program: "example"}, &args)
assert.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 TestFail(t *testing.T) { func TestFail(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var exitCode int var exitCode int
@ -650,32 +687,3 @@ Options:
p.WriteHelp(&help) p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String()) assert.Equal(t, expectedHelp[1:], help.String())
} }
func TestFailEnvOnly(t *testing.T) {
expectedUsage := "Usage: example [--arg ARG]"
expectedHelp := `
Usage: example [--arg ARG]
Options:
--arg ARG, -a ARG [env: MY_ARG]
--help, -h display this help and exit
Environment variables:
AUTH_KEY
`
var args struct {
ArgParam string `arg:"-a,--arg,env:MY_ARG"`
AuthKey string `arg:"--,env:AUTH_KEY"`
}
p, err := NewParser(Config{Program: "example"}, &args)
assert.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()))
}