Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,43 @@ if p.Subcommand() == nil {
}
```

### Reclaiming `-h`

If you need `-h` for an option (such as hostname), you can supply the config option `Help`
(a slice of strings) and only options matching those will trigger the help functionality.
Supplying an empty slice will prevent any options from triggering help. A `nil` slice
(ie. not setting `Help` explicitly) will default to `[]string{"-h", "--help"}` to emulate
the original behaviour.

At most one long and one short option should be supplied in `config.Help`.

```go
var args struct {
Hostname string `arg:"-h" default:"127.0.0.1"`
}

p, err := arg.NewParser(arg.Config{Help:[]string{}}, &args)
if err != nil {
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
}

err = p.Parse(os.Args[1:])
if err != nil {
fmt.Printf("error: %v\n", err)
p.WriteUsage(os.Stdout)
os.Exit(1)
}
fmt.Println(args.Hostname)
```

```shell
$ go run ./example -h 10.0.0.2
10.0.0.2

$ go run ./example --help
127.0.0.1
```

### Custom handling of --help and --version

The following reproduces the internal logic of `MustParse` for the simple case where
Expand Down
29 changes: 24 additions & 5 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ type Config struct {
// default values, including pointers to sub commands
IgnoreDefault bool

// Help instructs the library to use only these strings as help triggers.
Help []string

// StrictSubcommands intructs the library not to allow global commands after
// subcommand
StrictSubcommands bool
Expand Down Expand Up @@ -208,6 +211,10 @@ func NewParser(config Config, dests ...interface{}) (*Parser, error) {
if config.Out == nil {
config.Out = os.Stdout
}
// default to the usual help opts
if config.Help == nil {
config.Help = []string{"-h", "--help"}
}

// first pick a name for the command for use in the usage text
var name string
Expand Down Expand Up @@ -517,7 +524,7 @@ func (p *Parser) Parse(args []string) error {
if err != nil {
// If -h or --help were specified then make sure help text supercedes other errors
for _, arg := range args {
if arg == "-h" || arg == "--help" {
if p.argIsHelp(arg) {
return ErrHelp
}
if arg == "--" {
Expand Down Expand Up @@ -670,11 +677,11 @@ func (p *Parser) process(args []string) error {
continue
}

// check for special --help and --version flags
switch arg {
case "-h", "--help":
if p.argIsHelp(arg) {
return ErrHelp
case "--version":
}

if arg == "--version" {
if !hasVersionOption && p.version != "" {
return ErrVersion
}
Expand Down Expand Up @@ -863,3 +870,15 @@ func findSubcommand(cmds []*command, name string) *command {
}
return nil
}

// argIsHelp is a helper function to check if the supplied string is
// present in the configuration's list of allowed help triggers.
// From Go 1.21, this can be `return slices.Contains(p.config.Help, arg)`
func (p *Parser) argIsHelp(arg string) bool {
for _, v := range p.config.Help {
if arg == v {
return true
}
}
return false
}
46 changes: 46 additions & 0 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1779,3 +1779,49 @@ func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) {
assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison
assert.Equal(t, p.config.Out, os.Stdout)
}

type configHelpTest struct {
args string
helpopts []string
err error
name string
}

func TestConfigHelp(t *testing.T) {
var args struct {
Host string `arg:"-h" default:"127.0.0.1"`
Queue string `arg:"-q"`
S string
}
var argsLong struct {
Help bool `arg:"required,--help"`
}
var err error

tests := []configHelpTest{
{"-h 10.0.0.1", nil, ErrHelp, "nil config is default"},
{"-h 10.0.0.1", []string{}, nil, "empty config no help"},
{"-h 10.0.0.1", []string{"--help"}, nil, "long opt"},
{"--aidezmoi", []string{"--aidezmoi"}, ErrHelp, "custom long"},
{"-h 1.2.3.4 --help", []string{"--help"}, ErrHelp, "help wins"},
{"-q priority", nil, nil, "normal usage"},
{"-q priority -h 10.0.0.1", nil, ErrHelp, "normal usage"},
{"--s --help", nil, ErrHelp, "help should win"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err = parseWithEnv(Config{Help: test.helpopts}, test.args, nil, &args)
if test.err == nil {
require.NoError(t, err)
} else {
require.Equal(t, test.err.Error(), err.Error())
}
})
}

// Check that you can use `--help` as a long option if you want
_, err = parseWithEnv(Config{Help: []string{}}, "--help", nil, &argsLong)
require.NoError(t, err)
require.Equal(t, true, argsLong.Help)
}
15 changes: 13 additions & 2 deletions usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,21 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
}

// write the list of built in options
var short, long string

for _, v := range p.config.Help {
switch {
case strings.HasPrefix(v, "--"):
long = v[2:]
case strings.HasPrefix(v, "-"):
short = v[1:]
}
}

p.printOption(w, &spec{
cardinality: zero,
long: "help",
short: "h",
long: long,
short: short,
help: "display this help and exit",
})
if !hasVersionOption && p.version != "" {
Expand Down
87 changes: 87 additions & 0 deletions usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1084,3 +1084,90 @@ Options:
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}

func TestWriteUsageCustomHelp(t *testing.T) {
expectedUsage := "Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]"

expectedHelp := `
Usage: example [--name NAME] [--value VALUE] [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--ids IDS] [--values VALUES] [--workers WORKERS] [--testenv TESTENV] [--file FILE] INPUT [OUTPUT [OUTPUT ...]]

Positional arguments:
INPUT
OUTPUT list of outputs

Options:
--name NAME name to use [default: Foo Bar]
--value VALUE secret value [default: 42]
--verbose, -v verbosity level
--dataset DATASET dataset to use
--optimize OPTIMIZE, -O OPTIMIZE
optimization level
--ids IDS Ids
--values VALUES Values
--workers WORKERS, -w WORKERS
number of workers to start [default: 10, env: WORKERS]
--testenv TESTENV, -a TESTENV [env: TEST_ENV]
--file FILE, -f FILE File with mandatory extension [default: scratch.txt]
--capybara 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 {
Input string `arg:"positional,required"`
Output []string `arg:"positional" help:"list of outputs"`
Name string `help:"name to use"`
Value int `help:"secret value"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
Optimize int `arg:"-O" help:"optimization level"`
Ids []int64 `help:"Ids"`
Values []float64 `help:"Values"`
Workers int `arg:"-w,env:WORKERS" help:"number of workers to start" default:"10"`
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"`
}
args.Name = "Foo Bar"
args.Value = 42
args.File = &NameDotName{"scratch", "txt"}
p, err := NewParser(Config{Program: "example", Help: []string{"--capybara"}}, &args)
require.NoError(t, err)

os.Args[0] = "example"

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 TestUsageWithMultipleHelpOptions(t *testing.T) {
expectedUsage := "Usage: example"

expectedHelp := `
example 3.2.1
Usage: example

Options:
--aidezmoi, -a display this help and exit
--version display version and exit
`
os.Args[0] = "example"
p, err := NewParser(Config{Help: []string{"-a", "--aidezmoi"}}, &versioned{})
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()))
}