Skip to content
Merged
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
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ func main() {
// except that when flag "--mcli-show-hidden" is given.
mcli.AddHiden("secret-cmd", secretCmd, "An secret command won't be showed in help")

// Enable shell auto-completion, see `program completion -h` for help.
mcli.AddCompletion()
// Enable shell auto-completion, see `program completion -h` for help.
mcli.AddCompletion()

mcli.Run()
}
Expand Down Expand Up @@ -156,6 +156,8 @@ Also, there are some sophisticated examples:

## API

Use the default App:

- `SetGlobalFlags` sets global flags, global flags are available to all commands.
- `Add` adds a command.
- `AddRoot` adds a root command. A root command is executed when no sub command is specified.
Expand All @@ -169,18 +171,32 @@ Also, there are some sophisticated examples:
- `Parse` parses the command line for flags and arguments.
- `Run` runs the program, it will parse the command line, search for a registered command and run it.
- `PrintHelp` prints usage doc of the current command to stderr.

Create a new App instance:

- `NewApp` creates a new cli applcation instance.

### Custom parsing options
### Custom options

App:

- `App.Options` specifies optional options for an application.

CmdOpt:

- `WithLongDesc` specifies a long description of a command, which will be showed in the command's help.
- `WithExamples` specifies examples for a command. Examples will be showed after flags in the command's help.

ParseOpt:

- `WithArgs` tells `Parse` to parse from the given args, instead of parsing from the command line arguments.
- `WithErrorHandling` tells `Parse` to use the given ErrorHandling.
By default, it exits the program when an error happens.
By default, the program exits when an error happens.
- `WithName` specifies the command name to use when printing usage doc.
- `DisableGlobalFlags` tells `Parse` to don't parse and print global flags in help.
- `ReplaceUsage` tells `Parse` to use a custom usage function instead of the default.
- `WithFooter` adds a footer message after the default help.
- `App.Options` specifies optional options for an application.
- `WithFooter` adds a footer message after the default help,
this option overrides the App's setting `Options.HelpFooter` for this parsing call.

## Tag syntax

Expand Down
22 changes: 16 additions & 6 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ type Options struct {
KeepCommandOrder bool

// AllowPosixSTMO enables using the posix-style single token to specify
// multiple boolean options. e.g. -abc is equivalent to -a -b -c.
// multiple boolean options. e.g. `-abc` is equivalent to `-a -b -c`.
AllowPosixSTMO bool

// HelpFooter optionally adds a footer message to help output.
// If Parse is called with option `WithFooter`, the option function's
// output overrides this setting.
HelpFooter string
}

// NewApp creates a new cli application instance.
Expand Down Expand Up @@ -308,21 +313,23 @@ func (p *App) printUsage() {

// Add adds a command.
// f must be a function of signature `func()` or `func(*Context)`, else it panics.
func (p *App) Add(name string, f interface{}, description string) {
func (p *App) Add(name string, f interface{}, description string, opts ...CmdOpt) {
ff := p.validateFunc(f)
p.addCommand(&Command{
Name: name,
Description: description,
f: ff,
opts: newCmdOptions(opts...),
})
}

// AddRoot adds a root command.
// When no sub command specified, a root command will be executed.
func (p *App) AddRoot(f interface{}) {
func (p *App) AddRoot(f interface{}, opts ...CmdOpt) {
ff := p.validateFunc(f)
p.rootCmd = &Command{
f: ff,
opts: newCmdOptions(opts...),
isRoot: true,
}
}
Expand All @@ -342,7 +349,7 @@ func (p *App) validateFunc(f interface{}) func() {
}

// AddAlias adds an alias name for a command.
func (p *App) AddAlias(aliasName, target string) {
func (p *App) AddAlias(aliasName, target string, opts ...CmdOpt) {
cmd := p.cmdMap[target]
if cmd == nil {
panic(fmt.Sprintf("alias command target %q does not exist", target))
Expand All @@ -354,6 +361,7 @@ func (p *App) AddAlias(aliasName, target string) {
Description: desc,
AliasOf: target,
f: cmd.f,
opts: newCmdOptions(opts...),
})
}

Expand All @@ -362,13 +370,14 @@ func (p *App) AddAlias(aliasName, target string) {
//
// A hidden command won't be showed in help, except that when a special flag
// "--mcli-show-hidden" is provided.
func (p *App) AddHidden(name string, f interface{}, description string) {
func (p *App) AddHidden(name string, f interface{}, description string, opts ...CmdOpt) {
ff := p.validateFunc(f)
p.addCommand(&Command{
Name: name,
Description: description,
Hidden: true,
f: ff,
opts: newCmdOptions(opts...),
})
}

Expand All @@ -377,11 +386,12 @@ func (p *App) AddHidden(name string, f interface{}, description string) {
// It's not required to add group before adding sub commands, but user
// can use this function to add a description to a group, which will be
// showed in help.
func (p *App) AddGroup(name string, description string) {
func (p *App) AddGroup(name string, description string, opts ...CmdOpt) {
p.addCommand(&Command{
Name: name,
Description: description,
f: p.groupCmd,
opts: newCmdOptions(opts...),
isGroup: true,
})
}
Expand Down
39 changes: 39 additions & 0 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ func dummyCmd() {
PrintHelp()
}

func (p *App) dummyCmd_flagContinueOnError() {
p.parseArgs(nil, WithErrorHandling(flag.ContinueOnError))
p.printUsage()
}

func dummyCmdWithContext(ctx *Context) {
ctx.Parse(nil)
ctx.PrintHelp()
Expand Down Expand Up @@ -814,3 +819,37 @@ Line 3 in Description.`
got4 := buf.String()
assert.NotContains(t, got4, app4.Description)
}

func TestAppOptions(t *testing.T) {
t.Run("HelpFooter", func(t *testing.T) {
app := NewApp()
app.HelpFooter = `
LEARN MORE:
Use 'program help <command> <subcommand>' for more information of a command.
`
cmd2 := func(ctx *Context) {
ctx.Parse(nil, WithErrorHandling(flag.ContinueOnError),
WithFooter(func() string {
return "Footer from parsing option."
}))
ctx.PrintHelp()
}
app.Add("cmd1", app.dummyCmd_flagContinueOnError, "test cmd1")
app.Add("cmd2", cmd2, "test cmd2")

var buf bytes.Buffer
app.Run("cmd1", "-h")
app.getFlagSet().SetOutput(&buf)
app.printUsage()
got1 := buf.String()
assert.Contains(t, got1, "LEARN MORE:\n Use 'program help <command> <subcommand>' for more information of a command.\n\n")

buf.Reset()
app.Run("cmd2", "-h")
app.getFlagSet().SetOutput(&buf)
app.printUsage()
got2 := buf.String()
assert.NotContains(t, got2, "LEARN MORE")
assert.Contains(t, got2, "Footer from parsing option.\n\n")
})
}
3 changes: 2 additions & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ type Command struct {

AliasOf string

f func()
f func()
opts cmdOptions

idx int
level int
Expand Down
20 changes: 10 additions & 10 deletions default.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,37 @@ func SetGlobalFlags(v interface{}) {

// Add adds a command.
// f must be a function of signature `func()` or `func(*Context)`, else it panics.
func Add(name string, f interface{}, description string) {
defaultApp.Add(name, f, description)
func Add(name string, f interface{}, description string, opts ...CmdOpt) {
defaultApp.Add(name, f, description, opts...)
}

// AddRoot adds a root command processor.
// When no sub command specified, a root command will be executed.
func AddRoot(f interface{}) {
defaultApp.AddRoot(f)
func AddRoot(f interface{}, opts ...CmdOpt) {
defaultApp.AddRoot(f, opts...)
}

// AddAlias adds an alias name for a command.
func AddAlias(aliasName, target string) {
defaultApp.AddAlias(aliasName, target)
func AddAlias(aliasName, target string, opts ...CmdOpt) {
defaultApp.AddAlias(aliasName, target, opts...)
}

// AddHidden adds a hidden command.
// f must be a function of signature `func()` or `func(*Context)`, else it panics.
//
// A hidden command won't be showed in help, except that when a special flag
// "--mcli-show-hidden" is provided.
func AddHidden(name string, f interface{}, description string) {
defaultApp.AddHidden(name, f, description)
func AddHidden(name string, f interface{}, description string, opts ...CmdOpt) {
defaultApp.AddHidden(name, f, description, opts...)
}

// AddGroup adds a group explicitly.
// A group is a common prefix for some commands.
// It's not required to add group before adding sub commands, but user
// can use this function to add a description to a group, which will be
// showed in help.
func AddGroup(name string, description string) {
defaultApp.AddGroup(name, description)
func AddGroup(name string, description string, opts ...CmdOpt) {
defaultApp.AddGroup(name, description, opts...)
}

// AddHelp enables the "help" command to print help about any command.
Expand Down
44 changes: 43 additions & 1 deletion option.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package mcli

import "flag"
import (
"flag"
"strings"
)

func newParseOptions(opts ...ParseOpt) *parseOptions {
out := &parseOptions{
Expand Down Expand Up @@ -72,8 +75,47 @@ func ReplaceUsage(f func() string) ParseOpt {

// WithFooter specifies a function to generate extra help text to print
// after the default help.
// If this option is provided, the option function's output overrides
// the App's optional help-footer setting.
func WithFooter(f func() string) ParseOpt {
return ParseOpt{f: func(options *parseOptions) {
options.helpFooter = f
}}
}

func newCmdOptions(opts ...CmdOpt) cmdOptions {
return *(new(cmdOptions).apply(opts...))
}

type cmdOptions struct {
longDesc string
examples string
}

func (p *cmdOptions) apply(opts ...CmdOpt) *cmdOptions {
for _, o := range opts {
o.f(p)
}
return p
}

// CmdOpt specifies options to customize the behavior of a Command.
type CmdOpt struct {
f func(*cmdOptions)
}

// WithLongDesc specifies a long description of a command,
// which will be showed in the command's help.
func WithLongDesc(long string) CmdOpt {
return CmdOpt{f: func(options *cmdOptions) {
options.longDesc = strings.TrimSpace(long)
}}
}

// WithExamples specifies examples for a command.
// Examples will be showed after flags in the command's help.
func WithExamples(examples string) CmdOpt {
return CmdOpt{f: func(options *cmdOptions) {
options.examples = strings.TrimSpace(examples)
}}
}
38 changes: 38 additions & 0 deletions option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,41 @@ func TestWithFooter(t *testing.T) {
assert.Contains(t, got, "--args-b")
assert.Contains(t, got, "test with footer custom footer text\nanother line")
}

func TestWithLongDesc(t *testing.T) {
app := NewApp()
app.Add("cmd1", app.dummyCmd_flagContinueOnError, "test cmd1", WithLongDesc(`
Adding an issue to projects requires authorization with the "project" scope.
To authorize, run "gh auth refresh -s project".`))

app.Run("cmd1", "-h")

var buf bytes.Buffer
fs := app.getFlagSet()
fs.SetOutput(&buf)
fs.Usage()

got := buf.String()
assert.Contains(t, got, "test cmd1\n\nAdding an issue to projects requires authorization with the \"project\" scope.\nTo authorize, run \"gh auth refresh -s project\".\n\n")
}

func TestWithExamples(t *testing.T) {
app := NewApp()
app.Add("cmd1", app.dummyCmd_flagContinueOnError, "test cmd1", WithExamples(`
$ gh issue create --title "I found a bug" --body "Nothing works"
$ gh issue create --label "bug,help wanted"
$ gh issue create --label bug --label "help wanted"
$ gh issue create --assignee monalisa,hubot
$ gh issue create --assignee "@me"
$ gh issue create --project "Roadmap"`))

app.Run("cmd1", "-h")

var buf bytes.Buffer
fs := app.getFlagSet()
fs.SetOutput(&buf)
fs.Usage()

got := buf.String()
assert.Contains(t, got, "EXAMPLES:\n $ gh issue create --title \"I found a bug\" --body \"Nothing works\"\n $ gh issue create --label \"bug,help wanted\"\n $ gh")
}
Loading