Skip to content

Commit

Permalink
Reintroduce (much better) help and --help.
Browse files Browse the repository at this point in the history
  • Loading branch information
alecthomas committed Sep 17, 2014
1 parent 4cac799 commit 4ed1198
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 102 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,15 @@ func main() {

## Reference Documentation

## Sub-commands
### Help

Second to parsing, providing the user with useful help is probably the most
important thing a command-line parser does.

Since 1.3.x, Kingpin uses a bunch of heuristics to display help. For example,
`--help` should generally "just work" without much thought from users.

### Sub-commands

Kingpin supports nested sub-commands, with separate flag and positional
arguments per sub-command. Note that positional arguments may only occur after
Expand Down
55 changes: 40 additions & 15 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import (
"strings"
)

type Dispatch func() error
type Dispatch func(*ParseContext) error

// An Application contains the definitions of flags, arguments and commands
// for an application.
Expand All @@ -43,7 +43,6 @@ type Application struct {
*argGroup
*cmdGroup
initialized bool
commandHelp *string
Name string
Help string
}
Expand All @@ -53,20 +52,14 @@ func New(name, help string) *Application {
a := &Application{
flagGroup: newFlagGroup(),
argGroup: newArgGroup(),
cmdGroup: newCmdGroup(),
Name: name,
Help: help,
}
a.Flag("help", "Show help.").Dispatch(a.onFlagHelp).Bool()
a.cmdGroup = newCmdGroup(a)
a.Flag("help", "Show help.").Dispatch(a.onHelp).Bool()
return a
}

func (a *Application) onFlagHelp() error {
a.Usage(os.Stderr)
os.Exit(0)
return nil
}

// Parse parses command-line arguments. It returns the selected command and an
// error. The selected command will be a space separated subcommand, if
// subcommands have been configured.
Expand All @@ -91,14 +84,19 @@ func (a *Application) Parse(args []string) (command string, err error) {

// Version adds a --version flag for displaying the application version.
func (a *Application) Version(version string) *Application {
a.Flag("version", "Show application version.").Dispatch(func() error {
a.Flag("version", "Show application version.").Dispatch(func(*ParseContext) error {
fmt.Println(version)
os.Exit(0)
return nil
}).Bool()
return a
}

// Command adds a new top-level command.
func (a *Application) Command(name, help string) *CmdClause {
return a.addCommand(name, help)
}

func (a *Application) init() error {
if a.initialized {
return nil
Expand All @@ -108,8 +106,8 @@ func (a *Application) init() error {
}

if len(a.commands) > 0 {
cmd := a.Command("help", "Show help for a command.")
a.commandHelp = cmd.Arg("command", "Command name.").Required().Dispatch(a.onCommandHelp).String()
cmd := a.Command("help", "Show help for a command.").Dispatch(a.onHelp)
cmd.Arg("command", "Command name.").String()
// Make "help" command first in order. Also, Go's slice operations are woeful.
l := len(a.commandOrder) - 1
a.commandOrder = append(a.commandOrder[l:], a.commandOrder[:l]...)
Expand All @@ -133,8 +131,30 @@ func (a *Application) init() error {
return nil
}

func (a *Application) onCommandHelp() error {
a.CommandUsage(os.Stderr, *a.commandHelp)
func (a *Application) onHelp(context *ParseContext) error {
candidates := []string{}
for {
token := context.Peek()
if token.Type == TokenArg {
candidates = append(candidates, token.String())
context.Next()
} else {
break
}
}

var cmd *CmdClause
for i := len(candidates); i > 0; i-- {
command := strings.Join(candidates[:i], " ")
cmd = a.findCommand(command)
if cmd != nil {
a.CommandUsage(os.Stderr, command)
break
}
}
if cmd == nil {
a.Usage(os.Stderr)
}
os.Exit(0)
return nil
}
Expand Down Expand Up @@ -165,6 +185,11 @@ func (a *Application) Errorf(w io.Writer, format string, args ...interface{}) {
fmt.Fprintf(w, a.Name+": error: "+format+"\n", args...)
}

func (a *Application) Fatalf(w io.Writer, format string, args ...interface{}) {
a.Errorf(w, format, args...)
os.Exit(1)
}

// UsageErrorf prints an error message followed by usage information, then
// exits with a non-zero status.
func (a *Application) UsageErrorf(w io.Writer, format string, args ...interface{}) {
Expand Down
2 changes: 1 addition & 1 deletion app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestArgsMultipleRequiredThenNonRequired(t *testing.T) {
func TestDispatchCallbackIsCalled(t *testing.T) {
dispatched := false
c := New("test", "")
c.Command("cmd", "").Dispatch(func() error {
c.Command("cmd", "").Dispatch(func(*ParseContext) error {
dispatched = true
return nil
})
Expand Down
11 changes: 5 additions & 6 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,18 +144,17 @@ func (a *ArgClause) init() error {
}

func (a *ArgClause) parse(context *ParseContext) error {
if token := context.Next(); token.Type == TokenArg {
token := context.Peek()
if token.Type == TokenArg {
if err := a.value.Set(token.Value); err != nil {
return err
}
if a.dispatch != nil {
if err := a.dispatch(); err != nil {
if err := a.dispatch(context); err != nil {
return err
}
}
return nil
} else {
context.Return(token)
return nil
context.Next()
}
return nil
}
59 changes: 50 additions & 9 deletions cmd.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
package kingpin

import "fmt"
import (
"fmt"
"os"
"strings"
)

type cmdGroup struct {
app *Application
parent *CmdClause
commands map[string]*CmdClause
commandOrder []*CmdClause
}

func newCmdGroup() *cmdGroup {
func newCmdGroup(app *Application) *cmdGroup {
return &cmdGroup{
app: app,
commands: make(map[string]*CmdClause),
}
}

// Command adds a new top-level command to the application.
func (c *cmdGroup) Command(name, help string) *CmdClause {
cmd := newCommand(name, help)
func (c *cmdGroup) flattenedCommands() (out []*CmdClause) {
for _, cmd := range c.commandOrder {
if len(cmd.commands) == 0 {
out = append(out, cmd)
}
out = append(out, cmd.flattenedCommands()...)
}
return
}

func (c *cmdGroup) addCommand(name, help string) *CmdClause {
cmd := newCommand(c.app, name, help)
c.commands[name] = cmd
c.commandOrder = append(c.commandOrder, cmd)
return cmd
Expand All @@ -33,14 +49,15 @@ func (c *cmdGroup) init() error {
}

func (c *cmdGroup) parse(context *ParseContext) (selected []string, _ error) {
token := context.Next()
token := context.Peek()
if token.Type != TokenArg {
return nil, fmt.Errorf("expected command but got '%s'", token)
}
cmd, ok := c.commands[token.String()]
if !ok {
return nil, fmt.Errorf("no such command '%s'", token)
}
context.Next()
context.SelectedCommand = cmd.name
selected, err := cmd.parse(context)
if err == nil {
Expand All @@ -59,22 +76,46 @@ type CmdClause struct {
*flagGroup
*argGroup
*cmdGroup
app *Application
name string
help string
dispatch Dispatch
}

func newCommand(name, help string) *CmdClause {
func newCommand(app *Application, name, help string) *CmdClause {
c := &CmdClause{
flagGroup: newFlagGroup(),
argGroup: newArgGroup(),
cmdGroup: newCmdGroup(),
cmdGroup: newCmdGroup(app),
app: app,
name: name,
help: help,
}
c.Flag("help", "Show help on this command.").Hidden().Dispatch(c.onHelp).Bool()
return c
}

func (c *CmdClause) fullCommand() string {
out := []string{c.name}
for p := c.parent; p != nil; p = p.parent {
out = append([]string{p.name}, out...)
}
return strings.Join(out, " ")
}

func (c *CmdClause) onHelp(context *ParseContext) error {
c.app.CommandUsage(os.Stderr, c.fullCommand())
os.Exit(0)
return nil
}

// Command adds a new sub-command.
func (c *CmdClause) Command(name, help string) *CmdClause {
cmd := c.addCommand(name, help)
cmd.parent = c
return cmd
}

func (c *CmdClause) Dispatch(dispatch Dispatch) *CmdClause {
c.dispatch = dispatch
return c
Expand Down Expand Up @@ -107,7 +148,7 @@ func (c *CmdClause) parse(context *ParseContext) (selected []string, _ error) {
err = c.argGroup.parse(context)
}
if err == nil && c.dispatch != nil {
err = c.dispatch()
err = c.dispatch(context)
}
return selected, err
}
26 changes: 15 additions & 11 deletions examples/curl/curl.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ import (

var (
timeout = kingpin.Flag("timeout", "Set connection timeout.").Short('t').Default("5s").Duration()
headers = HTTPHeader(kingpin.Flag("headers", "Add HTTP headers to the request.").Short('H').PlaceHolder("HEADER:VALUE"))
headers = HTTPHeader(kingpin.Flag("headers", "Add HTTP headers to the request.").Short('H').PlaceHolder("HEADER=VALUE"))

get = kingpin.Command("get", "GET a resource.")
getURL = get.Arg("url", "URL to GET.").Required().URL()
get = kingpin.Command("get", "GET a resource.")
getFlag = get.Flag("test", "Test flag").Bool()
getURL = get.Command("url", "Retrieve a URL.")
getURLURL = getURL.Arg("url", "URL to GET.").Required().URL()
getFile = get.Command("file", "Retrieve a file.")
getFileFile = getFile.Arg("file", "File to retrieve.").Required().ExistingFile()

post = kingpin.Command("post", "POST a resource.")
postData = post.Flag("data", "Key-value data to POST").Short('d').PlaceHolder("KEY:VALUE").StringMap()
Expand All @@ -27,21 +31,21 @@ var (

type HTTPHeaderValue http.Header

func (h *HTTPHeaderValue) Set(value string) error {
parts := strings.SplitN(value, ":", 2)
func (h HTTPHeaderValue) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("expected HEADER:VALUE got '%s'", value)
return fmt.Errorf("expected HEADER=VALUE got '%s'", value)
}
(*http.Header)(h).Add(parts[0], parts[1])
(http.Header)(h).Add(parts[0], parts[1])
return nil
}

func (h *HTTPHeaderValue) String() string {
func (h HTTPHeaderValue) String() string {
return ""
}

func HTTPHeader(s kingpin.Settings) (target *http.Header) {
target = new(http.Header)
target = &http.Header{}
s.SetValue((*HTTPHeaderValue)(target))
return
}
Expand Down Expand Up @@ -91,8 +95,8 @@ func applyPOST() error {
func main() {
kingpin.CommandLine.Help = "An example implementation of curl."
switch kingpin.Parse() {
case "get":
kingpin.FatalIfError(apply("GET", (*getURL).String()), "GET failed")
case "get url":
kingpin.FatalIfError(apply("GET", (*getURLURL).String()), "GET failed")

case "post":
kingpin.FatalIfError(applyPOST(), "POST failed")
Expand Down
12 changes: 7 additions & 5 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (f *flagGroup) parse(context *ParseContext, ignoreRequired bool) error {

loop:
for {
token = context.Next()
token = context.Peek()
switch token.Type {
case TokenEOL:
break loop
Expand Down Expand Up @@ -87,6 +87,8 @@ loop:
delete(required, flag.name)
delete(defaults, flag.name)

context.Next()

fb, ok := flag.value.(boolFlag)
if ok && fb.IsBoolFlag() {
if invert {
Expand All @@ -98,10 +100,11 @@ loop:
if invert {
return fmt.Errorf("unknown long flag '%s'", flagToken)
}
token = context.Next()
token = context.Peek()
if token.Type != TokenArg {
return fmt.Errorf("expected argument for flag '%s'", flagToken)
}
context.Next()
defaultValue = token.Value
}

Expand All @@ -110,13 +113,12 @@ loop:
}

if flag.dispatch != nil {
if err := flag.dispatch(); err != nil {
if err := flag.dispatch(context); err != nil {
return err
}
}

default:
context.Return(token)
break loop
}
}
Expand Down Expand Up @@ -199,7 +201,7 @@ func (f *FlagClause) init() error {
return fmt.Errorf("required flag '--%s' with default value that will never be used", f.name)
}
if f.value == nil {
return fmt.Errorf("no value defined for --%s", f.name)
return fmt.Errorf("no type defined for --%s (eg. .String())", f.name)
}
if f.envar != "" {
if v := os.Getenv(f.envar); v != "" {
Expand Down
Loading

0 comments on commit 4ed1198

Please sign in to comment.