├── .travis.yml ├── guesswidth.go ├── guesswidth_ae.go ├── examples ├── ping │ └── main.go ├── chat1 │ └── main.go ├── modular │ └── main.go ├── chat2 │ └── main.go └── curl │ └── main.go ├── guesswidth_unix.go ├── parser_test.go ├── COPYING ├── actions.go ├── examples_test.go ├── args_test.go ├── values_test.go ├── values.json ├── usage_test.go ├── doc.go ├── parsers_test.go ├── args.go ├── cmd └── genvalues │ └── main.go ├── global.go ├── flags_test.go ├── cmd.go ├── model.go ├── cmd_test.go ├── app_test.go ├── usage.go ├── parsers.go ├── flags.go ├── templates.go ├── parser.go ├── values.go ├── app.go ├── values_generated.go └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | install: go get -t -v ./... 4 | go: 1.2 5 | -------------------------------------------------------------------------------- /guesswidth.go: -------------------------------------------------------------------------------- 1 | // +build !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd 2 | 3 | package kingpin 4 | 5 | import "io" 6 | 7 | func guessWidth(w io.Writer) int { 8 | return 80 9 | } 10 | -------------------------------------------------------------------------------- /guesswidth_ae.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package kingpin 4 | 5 | import "io" 6 | 7 | func guessWidth(w io.Writer) int { 8 | // No need to guess for appengine... 9 | return 80 10 | } 11 | -------------------------------------------------------------------------------- /examples/ping/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/alecthomas/kingpin.v2" 7 | ) 8 | 9 | var ( 10 | debug = kingpin.Flag("debug", "Enable debug mode.").Bool() 11 | timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").OverrideDefaultFromEnvar("PING_TIMEOUT").Required().Short('t').Duration() 12 | ip = kingpin.Arg("ip", "IP address to ping.").Required().IP() 13 | count = kingpin.Arg("count", "Number of packets to send").Int() 14 | ) 15 | 16 | func main() { 17 | kingpin.Version("0.0.1") 18 | kingpin.Parse() 19 | fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count) 20 | } 21 | -------------------------------------------------------------------------------- /examples/chat1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/alecthomas/kingpin.v2" 7 | ) 8 | 9 | var ( 10 | debug = kingpin.Flag("debug", "Enable debug mode.").Bool() 11 | timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration() 12 | ip = kingpin.Arg("ip", "IP address to ping.").Required().IP() 13 | count = kingpin.Arg("count", "Number of packets to send").Int() 14 | ) 15 | 16 | func main() { 17 | kingpin.Version("0.0.1") 18 | kingpin.Parse() 19 | fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count) 20 | } 21 | -------------------------------------------------------------------------------- /examples/modular/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/alecthomas/kingpin.v2" 8 | ) 9 | 10 | // Context for "ls" command 11 | type LsCommand struct { 12 | All bool 13 | } 14 | 15 | func (l *LsCommand) run(c *kingpin.ParseContext) error { 16 | fmt.Printf("all=%v\n", l.All) 17 | return nil 18 | } 19 | 20 | func configureLsCommand(app *kingpin.Application) { 21 | c := &LsCommand{} 22 | ls := app.Command("ls", "List files.").Action(c.run) 23 | ls.Flag("all", "List all files.").Short('a').BoolVar(&c.All) 24 | } 25 | 26 | func main() { 27 | app := kingpin.New("modular", "My modular application.") 28 | configureLsCommand(app) 29 | kingpin.MustParse(app.Parse(os.Args[1:])) 30 | } 31 | -------------------------------------------------------------------------------- /guesswidth_unix.go: -------------------------------------------------------------------------------- 1 | // +build !appengine,linux freebsd darwin dragonfly netbsd openbsd 2 | 3 | package kingpin 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "strconv" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | func guessWidth(w io.Writer) int { 14 | // check if COLUMNS env is set to comply with 15 | // http://pubs.opengroup.org/onlinepubs/009604499/basedefs/xbd_chap08.html 16 | colsStr := os.Getenv("COLUMNS") 17 | if colsStr != "" { 18 | if cols, err := strconv.Atoi(colsStr); err == nil { 19 | return cols 20 | } 21 | } 22 | 23 | if t, ok := w.(*os.File); ok { 24 | fd := t.Fd() 25 | var dimensions [4]uint16 26 | 27 | if _, _, err := syscall.Syscall6( 28 | syscall.SYS_IOCTL, 29 | uintptr(fd), 30 | uintptr(syscall.TIOCGWINSZ), 31 | uintptr(unsafe.Pointer(&dimensions)), 32 | 0, 0, 0, 33 | ); err == 0 { 34 | return int(dimensions[1]) 35 | } 36 | } 37 | return 80 38 | } 39 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestParserExpandFromFile(t *testing.T) { 12 | f, err := ioutil.TempFile("", "") 13 | assert.NoError(t, err) 14 | defer os.Remove(f.Name()) 15 | f.WriteString("hello\nworld\n") 16 | f.Close() 17 | 18 | app := New("test", "") 19 | arg0 := app.Arg("arg0", "").String() 20 | arg1 := app.Arg("arg1", "").String() 21 | 22 | _, err = app.Parse([]string{"@" + f.Name()}) 23 | assert.NoError(t, err) 24 | assert.Equal(t, "hello", *arg0) 25 | assert.Equal(t, "world", *arg1) 26 | } 27 | 28 | func TestParseContextPush(t *testing.T) { 29 | app := New("test", "") 30 | app.Command("foo", "").Command("bar", "") 31 | c := tokenize([]string{"foo", "bar"}, false) 32 | a := c.Next() 33 | assert.Equal(t, TokenArg, a.Type) 34 | b := c.Next() 35 | assert.Equal(t, TokenArg, b.Type) 36 | c.Push(b) 37 | c.Push(a) 38 | a = c.Next() 39 | assert.Equal(t, "foo", a.Value) 40 | b = c.Next() 41 | assert.Equal(t, "bar", b.Value) 42 | } 43 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Alec Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | // Action callback executed at various stages after all values are populated. 4 | // The application, commands, arguments and flags all have corresponding 5 | // actions. 6 | type Action func(*ParseContext) error 7 | 8 | type actionMixin struct { 9 | actions []Action 10 | preActions []Action 11 | } 12 | 13 | type actionApplier interface { 14 | applyActions(*ParseContext) error 15 | applyPreActions(*ParseContext) error 16 | } 17 | 18 | func (a *actionMixin) addAction(action Action) { 19 | a.actions = append(a.actions, action) 20 | } 21 | 22 | func (a *actionMixin) addPreAction(action Action) { 23 | a.preActions = append(a.preActions, action) 24 | } 25 | 26 | func (a *actionMixin) applyActions(context *ParseContext) error { 27 | for _, action := range a.actions { 28 | if err := action(context); err != nil { 29 | return err 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | func (a *actionMixin) applyPreActions(context *ParseContext) error { 36 | for _, preAction := range a.preActions { 37 | if err := preAction(context); err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /examples/chat2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "gopkg.in/alecthomas/kingpin.v2" 8 | ) 9 | 10 | var ( 11 | app = kingpin.New("chat", "A command-line chat application.") 12 | debug = app.Flag("debug", "Enable debug mode.").Bool() 13 | serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP() 14 | 15 | register = app.Command("register", "Register a new user.") 16 | registerNick = register.Arg("nick", "Nickname for user.").Required().String() 17 | registerName = register.Arg("name", "Name of user.").Required().String() 18 | 19 | post = app.Command("post", "Post a message to a channel.") 20 | postImage = post.Flag("image", "Image to post.").File() 21 | postChannel = post.Arg("channel", "Channel to post to.").Required().String() 22 | postText = post.Arg("text", "Text to post.").Strings() 23 | ) 24 | 25 | func main() { 26 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 27 | // Register user 28 | case register.FullCommand(): 29 | println(*registerNick) 30 | 31 | // Post message 32 | case post.FullCommand(): 33 | if *postImage != nil { 34 | } 35 | text := strings.Join(*postText, " ") 36 | println("Post:", text) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | type HTTPHeaderValue http.Header 10 | 11 | func (h *HTTPHeaderValue) Set(value string) error { 12 | parts := strings.SplitN(value, ":", 2) 13 | if len(parts) != 2 { 14 | return fmt.Errorf("expected HEADER:VALUE got '%s'", value) 15 | } 16 | (*http.Header)(h).Add(parts[0], parts[1]) 17 | return nil 18 | } 19 | 20 | func (h *HTTPHeaderValue) Get() interface{} { 21 | return (http.Header)(*h) 22 | } 23 | 24 | func (h *HTTPHeaderValue) String() string { 25 | return "" 26 | } 27 | 28 | func HTTPHeader(s Settings) (target *http.Header) { 29 | target = new(http.Header) 30 | s.SetValue((*HTTPHeaderValue)(target)) 31 | return 32 | } 33 | 34 | // This example ilustrates how to define custom parsers. HTTPHeader 35 | // cumulatively parses each encountered --header flag into a http.Header struct. 36 | func ExampleValue() { 37 | var ( 38 | curl = New("curl", "transfer a URL") 39 | headers = HTTPHeader(curl.Flag("headers", "Add HTTP headers to the request.").Short('H').PlaceHolder("HEADER:VALUE")) 40 | ) 41 | 42 | curl.Parse([]string{"-H Content-Type:application/octet-stream"}) 43 | for key, value := range *headers { 44 | fmt.Printf("%s = %s\n", key, value) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /args_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestArgRemainder(t *testing.T) { 11 | app := New("test", "") 12 | v := app.Arg("test", "").Strings() 13 | args := []string{"hello", "world"} 14 | _, err := app.Parse(args) 15 | assert.NoError(t, err) 16 | assert.Equal(t, args, *v) 17 | } 18 | 19 | func TestArgRemainderErrorsWhenNotLast(t *testing.T) { 20 | a := newArgGroup() 21 | a.Arg("test", "").Strings() 22 | a.Arg("test2", "").String() 23 | assert.Error(t, a.init()) 24 | } 25 | 26 | func TestArgMultipleRequired(t *testing.T) { 27 | terminated := false 28 | app := New("test", "") 29 | app.Version("0.0.0").Writer(ioutil.Discard) 30 | app.Arg("a", "").Required().String() 31 | app.Arg("b", "").Required().String() 32 | app.Terminate(func(int) { terminated = true }) 33 | 34 | _, err := app.Parse([]string{}) 35 | assert.Error(t, err) 36 | _, err = app.Parse([]string{"A"}) 37 | assert.Error(t, err) 38 | _, err = app.Parse([]string{"A", "B"}) 39 | assert.NoError(t, err) 40 | _, err = app.Parse([]string{"--version"}) 41 | assert.True(t, terminated) 42 | } 43 | 44 | func TestInvalidArgsDefaultCanBeOverridden(t *testing.T) { 45 | app := New("test", "") 46 | app.Arg("a", "").Default("invalid").Bool() 47 | _, err := app.Parse([]string{}) 48 | assert.Error(t, err) 49 | } 50 | -------------------------------------------------------------------------------- /values_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | 6 | "testing" 7 | ) 8 | 9 | func TestAccumulatorStrings(t *testing.T) { 10 | target := []string{} 11 | acc := newAccumulator(&target, func(v interface{}) Value { return newStringValue(v.(*string)) }) 12 | acc.Set("a") 13 | assert.Equal(t, []string{"a"}, target) 14 | acc.Set("b") 15 | assert.Equal(t, []string{"a", "b"}, target) 16 | } 17 | 18 | func TestStrings(t *testing.T) { 19 | app := New("", "") 20 | app.Arg("a", "").Required().String() 21 | app.Arg("b", "").Required().String() 22 | c := app.Arg("c", "").Required().Strings() 23 | app.Parse([]string{"a", "b", "a", "b"}) 24 | assert.Equal(t, []string{"a", "b"}, *c) 25 | } 26 | 27 | func TestEnum(t *testing.T) { 28 | app := New("", "") 29 | a := app.Arg("a", "").Enum("one", "two", "three") 30 | _, err := app.Parse([]string{"moo"}) 31 | assert.Error(t, err) 32 | _, err = app.Parse([]string{"one"}) 33 | assert.NoError(t, err) 34 | assert.Equal(t, "one", *a) 35 | } 36 | 37 | func TestEnumVar(t *testing.T) { 38 | app := New("", "") 39 | var a string 40 | app.Arg("a", "").EnumVar(&a, "one", "two", "three") 41 | _, err := app.Parse([]string{"moo"}) 42 | assert.Error(t, err) 43 | _, err = app.Parse([]string{"one"}) 44 | assert.NoError(t, err) 45 | assert.Equal(t, "one", a) 46 | } 47 | 48 | func TestCounter(t *testing.T) { 49 | app := New("", "") 50 | c := app.Flag("f", "").Counter() 51 | _, err := app.Parse([]string{"--f", "--f", "--f"}) 52 | assert.NoError(t, err) 53 | assert.Equal(t, 3, *c) 54 | } 55 | -------------------------------------------------------------------------------- /values.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"type": "bool", "parser": "strconv.ParseBool(s)"}, 3 | {"type": "string", "parser": "s, error(nil)", "format": "string(*f)", "plural": "Strings"}, 4 | {"type": "uint", "parser": "strconv.ParseUint(s, 0, 64)", "plural": "Uints"}, 5 | {"type": "uint8", "parser": "strconv.ParseUint(s, 0, 8)"}, 6 | {"type": "uint16", "parser": "strconv.ParseUint(s, 0, 16)"}, 7 | {"type": "uint32", "parser": "strconv.ParseUint(s, 0, 32)"}, 8 | {"type": "uint64", "parser": "strconv.ParseUint(s, 0, 64)"}, 9 | {"type": "int", "parser": "strconv.ParseFloat(s, 64)", "plural": "Ints"}, 10 | {"type": "int8", "parser": "strconv.ParseInt(s, 0, 8)"}, 11 | {"type": "int16", "parser": "strconv.ParseInt(s, 0, 16)"}, 12 | {"type": "int32", "parser": "strconv.ParseInt(s, 0, 32)"}, 13 | {"type": "int64", "parser": "strconv.ParseInt(s, 0, 64)"}, 14 | {"type": "float64", "parser": "strconv.ParseFloat(s, 64)"}, 15 | {"type": "float32", "parser": "strconv.ParseFloat(s, 32)"}, 16 | {"name": "Duration", "type": "time.Duration", "no_value_parser": true}, 17 | {"name": "IP", "type": "net.IP", "no_value_parser": true}, 18 | {"name": "TCPAddr", "Type": "*net.TCPAddr", "plural": "TCPList", "no_value_parser": true}, 19 | {"name": "ExistingFile", "Type": "string", "plural": "ExistingFiles", "no_value_parser": true}, 20 | {"name": "ExistingDir", "Type": "string", "plural": "ExistingDirs", "no_value_parser": true}, 21 | {"name": "ExistingFileOrDir", "Type": "string", "plural": "ExistingFilesOrDirs", "no_value_parser": true}, 22 | {"name": "Regexp", "Type": "*regexp.Regexp", "plural": "RegexpList", "no_value_parser": true} 23 | ] 24 | -------------------------------------------------------------------------------- /usage_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestFormatTwoColumns(t *testing.T) { 12 | buf := bytes.NewBuffer(nil) 13 | formatTwoColumns(buf, 2, 2, 20, [][2]string{ 14 | {"--hello", "Hello world help with something that is cool."}, 15 | }) 16 | expected := ` --hello Hello 17 | world 18 | help with 19 | something 20 | that is 21 | cool. 22 | ` 23 | assert.Equal(t, expected, buf.String()) 24 | } 25 | 26 | func TestFormatTwoColumnsWide(t *testing.T) { 27 | samples := [][2]string{ 28 | {strings.Repeat("x", 29), "29 chars"}, 29 | {strings.Repeat("x", 30), "30 chars"}} 30 | buf := bytes.NewBuffer(nil) 31 | formatTwoColumns(buf, 0, 0, 200, samples) 32 | expected := `xxxxxxxxxxxxxxxxxxxxxxxxxxxxx29 chars 33 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 34 | 30 chars 35 | ` 36 | assert.Equal(t, expected, buf.String()) 37 | } 38 | 39 | func TestHiddenCommand(t *testing.T) { 40 | templates := []struct{ name, template string }{ 41 | {"default", DefaultUsageTemplate}, 42 | {"Compact", CompactUsageTemplate}, 43 | {"Long", LongHelpTemplate}, 44 | {"Man", ManPageTemplate}, 45 | } 46 | 47 | var buf bytes.Buffer 48 | t.Log("1") 49 | 50 | a := New("test", "Test").Writer(&buf).Terminate(nil) 51 | a.Command("visible", "visible") 52 | a.Command("hidden", "hidden").Hidden() 53 | 54 | for _, tp := range templates { 55 | buf.Reset() 56 | a.UsageTemplate(tp.template) 57 | a.Parse(nil) 58 | // a.Parse([]string{"--help"}) 59 | usage := buf.String() 60 | t.Logf("Usage for %s is:\n%s\n", tp.name, usage) 61 | 62 | assert.NotContains(t, usage, "hidden") 63 | assert.Contains(t, usage, "visible") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package kingpin provides command line interfaces like this: 2 | // 3 | // $ chat 4 | // usage: chat [] [] [ ...] 5 | // 6 | // Flags: 7 | // --debug enable debug mode 8 | // --help Show help. 9 | // --server=127.0.0.1 server address 10 | // 11 | // Commands: 12 | // help 13 | // Show help for a command. 14 | // 15 | // post [] 16 | // Post a message to a channel. 17 | // 18 | // register 19 | // Register a new user. 20 | // 21 | // $ chat help post 22 | // usage: chat [] post [] [] 23 | // 24 | // Post a message to a channel. 25 | // 26 | // Flags: 27 | // --image=IMAGE image to post 28 | // 29 | // Args: 30 | // channel to post to 31 | // [] text to post 32 | // $ chat post --image=~/Downloads/owls.jpg pics 33 | // 34 | // From code like this: 35 | // 36 | // package main 37 | // 38 | // import "gopkg.in/alecthomas/kingpin.v1" 39 | // 40 | // var ( 41 | // debug = kingpin.Flag("debug", "enable debug mode").Default("false").Bool() 42 | // serverIP = kingpin.Flag("server", "server address").Default("127.0.0.1").IP() 43 | // 44 | // register = kingpin.Command("register", "Register a new user.") 45 | // registerNick = register.Arg("nick", "nickname for user").Required().String() 46 | // registerName = register.Arg("name", "name of user").Required().String() 47 | // 48 | // post = kingpin.Command("post", "Post a message to a channel.") 49 | // postImage = post.Flag("image", "image to post").ExistingFile() 50 | // postChannel = post.Arg("channel", "channel to post to").Required().String() 51 | // postText = post.Arg("text", "text to post").String() 52 | // ) 53 | // 54 | // func main() { 55 | // switch kingpin.Parse() { 56 | // // Register user 57 | // case "register": 58 | // println(*registerNick) 59 | // 60 | // // Post message 61 | // case "post": 62 | // if *postImage != nil { 63 | // } 64 | // if *postText != "" { 65 | // } 66 | // } 67 | // } 68 | package kingpin 69 | -------------------------------------------------------------------------------- /parsers_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "net/url" 7 | "os" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "testing" 12 | ) 13 | 14 | func TestParseStrings(t *testing.T) { 15 | p := parserMixin{} 16 | v := p.Strings() 17 | p.value.Set("a") 18 | p.value.Set("b") 19 | assert.Equal(t, []string{"a", "b"}, *v) 20 | } 21 | 22 | func TestStringsStringer(t *testing.T) { 23 | target := []string{} 24 | v := newAccumulator(&target, func(v interface{}) Value { return newStringValue(v.(*string)) }) 25 | v.Set("hello") 26 | v.Set("world") 27 | assert.Equal(t, "hello,world", v.String()) 28 | } 29 | 30 | func TestParseStringMap(t *testing.T) { 31 | p := parserMixin{} 32 | v := p.StringMap() 33 | p.value.Set("a:b") 34 | p.value.Set("b:c") 35 | assert.Equal(t, map[string]string{"a": "b", "b": "c"}, *v) 36 | } 37 | 38 | func TestParseIP(t *testing.T) { 39 | p := parserMixin{} 40 | v := p.IP() 41 | p.value.Set("10.1.1.2") 42 | ip := net.ParseIP("10.1.1.2") 43 | assert.Equal(t, ip, *v) 44 | } 45 | 46 | func TestParseURL(t *testing.T) { 47 | p := parserMixin{} 48 | v := p.URL() 49 | p.value.Set("http://w3.org") 50 | u, err := url.Parse("http://w3.org") 51 | assert.NoError(t, err) 52 | assert.Equal(t, *u, **v) 53 | } 54 | 55 | func TestParseExistingFile(t *testing.T) { 56 | f, err := ioutil.TempFile("", "") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer f.Close() 61 | defer os.Remove(f.Name()) 62 | 63 | p := parserMixin{} 64 | v := p.ExistingFile() 65 | err = p.value.Set(f.Name()) 66 | assert.NoError(t, err) 67 | assert.Equal(t, f.Name(), *v) 68 | err = p.value.Set("/etc/hostsDEFINITELYMISSING") 69 | assert.Error(t, err) 70 | } 71 | 72 | func TestParseTCPAddr(t *testing.T) { 73 | p := parserMixin{} 74 | v := p.TCP() 75 | err := p.value.Set("127.0.0.1:1234") 76 | assert.NoError(t, err) 77 | expected, err := net.ResolveTCPAddr("tcp", "127.0.0.1:1234") 78 | assert.NoError(t, err) 79 | assert.Equal(t, *expected, **v) 80 | } 81 | 82 | func TestParseTCPAddrList(t *testing.T) { 83 | p := parserMixin{} 84 | _ = p.TCPList() 85 | err := p.value.Set("127.0.0.1:1234") 86 | assert.NoError(t, err) 87 | err = p.value.Set("127.0.0.1:1235") 88 | assert.NoError(t, err) 89 | assert.Equal(t, "127.0.0.1:1234,127.0.0.1:1235", p.value.String()) 90 | } 91 | 92 | func TestFloat32(t *testing.T) { 93 | p := parserMixin{} 94 | v := p.Float32() 95 | err := p.value.Set("123.45") 96 | assert.NoError(t, err) 97 | assert.InEpsilon(t, 123.45, *v, 0.001) 98 | } 99 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import "fmt" 4 | 5 | type argGroup struct { 6 | args []*ArgClause 7 | } 8 | 9 | func newArgGroup() *argGroup { 10 | return &argGroup{} 11 | } 12 | 13 | func (a *argGroup) have() bool { 14 | return len(a.args) > 0 15 | } 16 | 17 | func (a *argGroup) Arg(name, help string) *ArgClause { 18 | arg := newArg(name, help) 19 | a.args = append(a.args, arg) 20 | return arg 21 | } 22 | 23 | func (a *argGroup) init() error { 24 | required := 0 25 | seen := map[string]struct{}{} 26 | previousArgMustBeLast := false 27 | for i, arg := range a.args { 28 | if previousArgMustBeLast { 29 | return fmt.Errorf("Args() can't be followed by another argument '%s'", arg.name) 30 | } 31 | if arg.consumesRemainder() { 32 | previousArgMustBeLast = true 33 | } 34 | if _, ok := seen[arg.name]; ok { 35 | return fmt.Errorf("duplicate argument '%s'", arg.name) 36 | } 37 | seen[arg.name] = struct{}{} 38 | if arg.required && required != i { 39 | return fmt.Errorf("required arguments found after non-required") 40 | } 41 | if arg.required { 42 | required++ 43 | } 44 | if err := arg.init(); err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | type ArgClause struct { 52 | actionMixin 53 | parserMixin 54 | name string 55 | help string 56 | defaultValue string 57 | required bool 58 | } 59 | 60 | func newArg(name, help string) *ArgClause { 61 | a := &ArgClause{ 62 | name: name, 63 | help: help, 64 | } 65 | return a 66 | } 67 | 68 | func (a *ArgClause) consumesRemainder() bool { 69 | if r, ok := a.value.(remainderArg); ok { 70 | return r.IsCumulative() 71 | } 72 | return false 73 | } 74 | 75 | // Required arguments must be input by the user. They can not have a Default() value provided. 76 | func (a *ArgClause) Required() *ArgClause { 77 | a.required = true 78 | return a 79 | } 80 | 81 | // Default value for this argument. It *must* be parseable by the value of the argument. 82 | func (a *ArgClause) Default(value string) *ArgClause { 83 | a.defaultValue = value 84 | return a 85 | } 86 | 87 | func (a *ArgClause) Action(action Action) *ArgClause { 88 | a.addAction(action) 89 | return a 90 | } 91 | 92 | func (a *ArgClause) PreAction(action Action) *ArgClause { 93 | a.addPreAction(action) 94 | return a 95 | } 96 | 97 | func (a *ArgClause) init() error { 98 | if a.required && a.defaultValue != "" { 99 | return fmt.Errorf("required argument '%s' with unusable default value", a.name) 100 | } 101 | if a.value == nil { 102 | return fmt.Errorf("no parser defined for arg '%s'", a.name) 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /cmd/genvalues/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os/exec" 6 | "strings" 7 | "text/template" 8 | 9 | "os" 10 | ) 11 | 12 | const ( 13 | tmpl = `package kingpin 14 | 15 | // This file is autogenerated by "go generate .". Do not modify. 16 | 17 | {{range .}} 18 | {{if not .NoValueParser}} 19 | // -- {{.Type}} Value 20 | type {{.Type}}Value {{.Type}} 21 | 22 | func new{{.|Name}}Value(p *{{.Type}}) *{{.Type}}Value { 23 | return (*{{.Type}}Value)(p) 24 | } 25 | 26 | func (f *{{.Type}}Value) Set(s string) error { 27 | v, err := {{.Parser}} 28 | *f = {{.Type}}Value(v) 29 | return err 30 | } 31 | 32 | func (f *{{.Type}}Value) Get() interface{} { return {{.Type}}(*f) } 33 | 34 | func (f *{{.Type}}Value) String() string { return {{.|Format}} } 35 | 36 | // {{.|Name}} parses the next command-line value as {{.Type}}. 37 | func (p *parserMixin) {{.|Name}}() (target *{{.Type}}) { 38 | target = new({{.Type}}) 39 | p.{{.|Name}}Var(target) 40 | return 41 | } 42 | 43 | func (p *parserMixin) {{.|Name}}Var(target *{{.Type}}) { 44 | p.SetValue(new{{.|Name}}Value(target)) 45 | } 46 | 47 | {{end}} 48 | // {{.|Plural}} accumulates {{.Type}} values into a slice. 49 | func (p *parserMixin) {{.|Plural}}() (target *[]{{.Type}}) { 50 | target = new([]{{.Type}}) 51 | p.{{.|Plural}}Var(target) 52 | return 53 | } 54 | 55 | func (p *parserMixin) {{.|Plural}}Var(target *[]{{.Type}}) { 56 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return new{{.|Name}}Value(v.(*{{.Type}})) })) 57 | } 58 | 59 | {{end}} 60 | ` 61 | ) 62 | 63 | type Value struct { 64 | Name string `json:"name"` 65 | NoValueParser bool `json:"no_value_parser"` 66 | Type string `json:"type"` 67 | Parser string `json:"parser"` 68 | Format string `json:"format"` 69 | Plural string `json:"plural"` 70 | } 71 | 72 | func fatalIfError(err error) { 73 | if err != nil { 74 | panic(err) 75 | } 76 | } 77 | 78 | func main() { 79 | r, err := os.Open("values.json") 80 | fatalIfError(err) 81 | defer r.Close() 82 | 83 | v := []Value{} 84 | err = json.NewDecoder(r).Decode(&v) 85 | fatalIfError(err) 86 | 87 | valueName := func(v *Value) string { 88 | if v.Name != "" { 89 | return v.Name 90 | } 91 | return strings.Title(v.Type) 92 | } 93 | 94 | t, err := template.New("genvalues").Funcs(template.FuncMap{ 95 | "Lower": strings.ToLower, 96 | "Format": func(v *Value) string { 97 | if v.Format != "" { 98 | return v.Format 99 | } 100 | return "fmt.Sprintf(\"%v\", *f)" 101 | }, 102 | "Name": valueName, 103 | "Plural": func(v *Value) string { 104 | if v.Plural != "" { 105 | return v.Plural 106 | } 107 | return valueName(v) + "List" 108 | }, 109 | }).Parse(tmpl) 110 | fatalIfError(err) 111 | 112 | w, err := os.Create("values_generated.go") 113 | fatalIfError(err) 114 | defer w.Close() 115 | 116 | err = t.Execute(w, v) 117 | fatalIfError(err) 118 | 119 | err = exec.Command("goimports", "-w", "values_generated.go").Run() 120 | fatalIfError(err) 121 | } 122 | -------------------------------------------------------------------------------- /global.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var ( 9 | // CommandLine is the default Kingpin parser. 10 | CommandLine = New(filepath.Base(os.Args[0]), "") 11 | // Global help flag. Exposed for user customisation. 12 | HelpFlag = CommandLine.HelpFlag 13 | // Top-level help command. Exposed for user customisation. May be nil. 14 | HelpCommand = CommandLine.HelpCommand 15 | // Global version flag. Exposed for user customisation. May be nil. 16 | VersionFlag = CommandLine.VersionFlag 17 | ) 18 | 19 | // Command adds a new command to the default parser. 20 | func Command(name, help string) *CmdClause { 21 | return CommandLine.Command(name, help) 22 | } 23 | 24 | // Flag adds a new flag to the default parser. 25 | func Flag(name, help string) *FlagClause { 26 | return CommandLine.Flag(name, help) 27 | } 28 | 29 | // Arg adds a new argument to the top-level of the default parser. 30 | func Arg(name, help string) *ArgClause { 31 | return CommandLine.Arg(name, help) 32 | } 33 | 34 | // Parse and return the selected command. Will call the termination handler if 35 | // an error is encountered. 36 | func Parse() string { 37 | selected := MustParse(CommandLine.Parse(os.Args[1:])) 38 | if selected == "" && CommandLine.cmdGroup.have() { 39 | Usage() 40 | CommandLine.terminate(0) 41 | } 42 | return selected 43 | } 44 | 45 | // Errorf prints an error message to stderr. 46 | func Errorf(format string, args ...interface{}) { 47 | CommandLine.Errorf(format, args...) 48 | } 49 | 50 | // Fatalf prints an error message to stderr and exits. 51 | func Fatalf(format string, args ...interface{}) { 52 | CommandLine.Fatalf(format, args...) 53 | } 54 | 55 | // FatalIfError prints an error and exits if err is not nil. The error is printed 56 | // with the given prefix. 57 | func FatalIfError(err error, format string, args ...interface{}) { 58 | CommandLine.FatalIfError(err, format, args...) 59 | } 60 | 61 | // FatalUsage prints an error message followed by usage information, then 62 | // exits with a non-zero status. 63 | func FatalUsage(format string, args ...interface{}) { 64 | CommandLine.FatalUsage(format, args...) 65 | } 66 | 67 | // FatalUsageContext writes a printf formatted error message to stderr, then 68 | // usage information for the given ParseContext, before exiting. 69 | func FatalUsageContext(context *ParseContext, format string, args ...interface{}) { 70 | CommandLine.FatalUsageContext(context, format, args...) 71 | } 72 | 73 | // Usage prints usage to stderr. 74 | func Usage() { 75 | CommandLine.Usage(os.Args[1:]) 76 | } 77 | 78 | // Set global usage template to use (defaults to DefaultUsageTemplate). 79 | func UsageTemplate(template string) *Application { 80 | return CommandLine.UsageTemplate(template) 81 | } 82 | 83 | // MustParse can be used with app.Parse(args) to exit with an error if parsing fails. 84 | func MustParse(command string, err error) string { 85 | if err != nil { 86 | Fatalf("%s, try --help", err) 87 | } 88 | return command 89 | } 90 | 91 | // Version adds a flag for displaying the application version number. 92 | func Version(version string) *Application { 93 | return CommandLine.Version(version) 94 | } 95 | -------------------------------------------------------------------------------- /examples/curl/main.go: -------------------------------------------------------------------------------- 1 | // A curl-like HTTP command-line client. 2 | package main 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "gopkg.in/alecthomas/kingpin.v2" 13 | ) 14 | 15 | var ( 16 | timeout = kingpin.Flag("timeout", "Set connection timeout.").Short('t').Default("5s").Duration() 17 | headers = HTTPHeader(kingpin.Flag("headers", "Add HTTP headers to the request.").Short('H').PlaceHolder("HEADER=VALUE")) 18 | 19 | get = kingpin.Command("get", "GET a resource.").Default() 20 | getFlag = get.Flag("test", "Test flag").Bool() 21 | getURL = get.Command("url", "Retrieve a URL.").Default() 22 | getURLURL = getURL.Arg("url", "URL to GET.").Required().URL() 23 | getFile = get.Command("file", "Retrieve a file.") 24 | getFileFile = getFile.Arg("file", "File to retrieve.").Required().ExistingFile() 25 | 26 | post = kingpin.Command("post", "POST a resource.") 27 | postData = post.Flag("data", "Key-value data to POST").Short('d').PlaceHolder("KEY:VALUE").StringMap() 28 | postBinaryFile = post.Flag("data-binary", "File with binary data to POST.").File() 29 | postURL = post.Arg("url", "URL to POST to.").Required().URL() 30 | ) 31 | 32 | type HTTPHeaderValue http.Header 33 | 34 | func (h HTTPHeaderValue) Set(value string) error { 35 | parts := strings.SplitN(value, "=", 2) 36 | if len(parts) != 2 { 37 | return fmt.Errorf("expected HEADER=VALUE got '%s'", value) 38 | } 39 | (http.Header)(h).Add(parts[0], parts[1]) 40 | return nil 41 | } 42 | 43 | func (h HTTPHeaderValue) String() string { 44 | return "" 45 | } 46 | 47 | func HTTPHeader(s kingpin.Settings) (target *http.Header) { 48 | target = &http.Header{} 49 | s.SetValue((*HTTPHeaderValue)(target)) 50 | return 51 | } 52 | 53 | func applyRequest(req *http.Request) error { 54 | req.Header = *headers 55 | resp, err := http.DefaultClient.Do(req) 56 | if err != nil { 57 | return err 58 | } 59 | defer resp.Body.Close() 60 | if resp.StatusCode < 200 || resp.StatusCode > 299 { 61 | return fmt.Errorf("HTTP request failed: %s", resp.Status) 62 | } 63 | _, err = io.Copy(os.Stdout, resp.Body) 64 | return err 65 | } 66 | 67 | func apply(method string, url string) error { 68 | req, err := http.NewRequest(method, url, nil) 69 | if err != nil { 70 | return err 71 | } 72 | return applyRequest(req) 73 | } 74 | 75 | func applyPOST() error { 76 | req, err := http.NewRequest("POST", (*postURL).String(), nil) 77 | if err != nil { 78 | return err 79 | } 80 | if len(*postData) > 0 { 81 | for key, value := range *postData { 82 | req.Form.Set(key, value) 83 | } 84 | } else if postBinaryFile != nil { 85 | if headers.Get("Content-Type") != "" { 86 | headers.Set("Content-Type", "application/octet-stream") 87 | } 88 | req.Body = *postBinaryFile 89 | } else { 90 | return errors.New("--data or --data-binary must be provided to POST") 91 | } 92 | return applyRequest(req) 93 | } 94 | 95 | func main() { 96 | kingpin.UsageTemplate(kingpin.CompactUsageTemplate).Version("1.0").Author("Alec Thomas") 97 | kingpin.CommandLine.Help = "An example implementation of curl." 98 | switch kingpin.Parse() { 99 | case "get url": 100 | kingpin.FatalIfError(apply("GET", (*getURLURL).String()), "GET failed") 101 | 102 | case "post": 103 | kingpin.FatalIfError(applyPOST(), "POST failed") 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /flags_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "testing" 10 | ) 11 | 12 | func TestBool(t *testing.T) { 13 | app := New("test", "") 14 | b := app.Flag("b", "").Bool() 15 | _, err := app.Parse([]string{"--b"}) 16 | assert.NoError(t, err) 17 | assert.True(t, *b) 18 | } 19 | 20 | func TestNoBool(t *testing.T) { 21 | fg := newFlagGroup() 22 | f := fg.Flag("b", "").Default("true") 23 | b := f.Bool() 24 | fg.init("") 25 | tokens := tokenize([]string{"--no-b"}, false) 26 | _, err := fg.parse(tokens) 27 | assert.NoError(t, err) 28 | assert.False(t, *b) 29 | } 30 | 31 | func TestNegateNonBool(t *testing.T) { 32 | fg := newFlagGroup() 33 | f := fg.Flag("b", "") 34 | f.Int() 35 | fg.init("") 36 | tokens := tokenize([]string{"--no-b"}, false) 37 | _, err := fg.parse(tokens) 38 | assert.Error(t, err) 39 | } 40 | 41 | func TestInvalidFlagDefaultCanBeOverridden(t *testing.T) { 42 | app := New("test", "") 43 | app.Flag("a", "").Default("invalid").Bool() 44 | _, err := app.Parse([]string{}) 45 | assert.Error(t, err) 46 | } 47 | 48 | func TestRequiredFlag(t *testing.T) { 49 | app := New("test", "") 50 | app.Version("0.0.0").Writer(ioutil.Discard) 51 | exits := 0 52 | app.Terminate(func(int) { exits++ }) 53 | app.Flag("a", "").Required().Bool() 54 | _, err := app.Parse([]string{"--a"}) 55 | assert.NoError(t, err) 56 | _, err = app.Parse([]string{}) 57 | assert.Error(t, err) 58 | _, err = app.Parse([]string{"--version"}) 59 | assert.Equal(t, 1, exits) 60 | } 61 | 62 | func TestShortFlag(t *testing.T) { 63 | app := New("test", "") 64 | f := app.Flag("long", "").Short('s').Bool() 65 | _, err := app.Parse([]string{"-s"}) 66 | assert.NoError(t, err) 67 | assert.True(t, *f) 68 | } 69 | 70 | func TestCombinedShortFlags(t *testing.T) { 71 | app := New("test", "") 72 | a := app.Flag("short0", "").Short('0').Bool() 73 | b := app.Flag("short1", "").Short('1').Bool() 74 | c := app.Flag("short2", "").Short('2').Bool() 75 | _, err := app.Parse([]string{"-01"}) 76 | assert.NoError(t, err) 77 | assert.True(t, *a) 78 | assert.True(t, *b) 79 | assert.False(t, *c) 80 | } 81 | 82 | func TestCombinedShortFlagArg(t *testing.T) { 83 | a := New("test", "") 84 | n := a.Flag("short", "").Short('s').Int() 85 | _, err := a.Parse([]string{"-s10"}) 86 | assert.NoError(t, err) 87 | assert.Equal(t, 10, *n) 88 | } 89 | 90 | func TestEmptyShortFlagIsAnError(t *testing.T) { 91 | _, err := New("test", "").Parse([]string{"-"}) 92 | assert.Error(t, err) 93 | } 94 | 95 | func TestRequiredWithEnvarMissingErrors(t *testing.T) { 96 | app := New("test", "") 97 | app.Flag("t", "").OverrideDefaultFromEnvar("TEST_ENVAR").Required().Int() 98 | _, err := app.Parse([]string{}) 99 | assert.Error(t, err) 100 | } 101 | 102 | func TestRequiredWithEnvar(t *testing.T) { 103 | os.Setenv("TEST_ENVAR", "123") 104 | app := New("test", "") 105 | flag := app.Flag("t", "").Envar("TEST_ENVAR").Required().Int() 106 | _, err := app.Parse([]string{}) 107 | assert.NoError(t, err) 108 | assert.Equal(t, 123, *flag) 109 | } 110 | 111 | func TestRegexp(t *testing.T) { 112 | app := New("test", "") 113 | flag := app.Flag("reg", "").Regexp() 114 | _, err := app.Parse([]string{"--reg", "^abc$"}) 115 | assert.NoError(t, err) 116 | assert.NotNil(t, *flag) 117 | assert.Equal(t, "^abc$", (*flag).String()) 118 | assert.Regexp(t, *flag, "abc") 119 | assert.NotRegexp(t, *flag, "abcd") 120 | } 121 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type cmdGroup struct { 9 | app *Application 10 | parent *CmdClause 11 | commands map[string]*CmdClause 12 | commandOrder []*CmdClause 13 | } 14 | 15 | func (c *cmdGroup) defaultSubcommand() *CmdClause { 16 | for _, cmd := range c.commandOrder { 17 | if cmd.isDefault { 18 | return cmd 19 | } 20 | } 21 | return nil 22 | } 23 | 24 | func newCmdGroup(app *Application) *cmdGroup { 25 | return &cmdGroup{ 26 | app: app, 27 | commands: make(map[string]*CmdClause), 28 | } 29 | } 30 | 31 | func (c *cmdGroup) flattenedCommands() (out []*CmdClause) { 32 | for _, cmd := range c.commandOrder { 33 | if len(cmd.commands) == 0 { 34 | out = append(out, cmd) 35 | } 36 | out = append(out, cmd.flattenedCommands()...) 37 | } 38 | return 39 | } 40 | 41 | func (c *cmdGroup) addCommand(name, help string) *CmdClause { 42 | cmd := newCommand(c.app, name, help) 43 | c.commands[name] = cmd 44 | c.commandOrder = append(c.commandOrder, cmd) 45 | return cmd 46 | } 47 | 48 | func (c *cmdGroup) init() error { 49 | seen := map[string]bool{} 50 | if c.defaultSubcommand() != nil && !c.have() { 51 | return fmt.Errorf("default subcommand %q provided but no subcommands defined", c.defaultSubcommand().name) 52 | } 53 | defaults := []string{} 54 | for _, cmd := range c.commandOrder { 55 | if cmd.isDefault { 56 | defaults = append(defaults, cmd.name) 57 | } 58 | if seen[cmd.name] { 59 | return fmt.Errorf("duplicate command %q", cmd.name) 60 | } 61 | seen[cmd.name] = true 62 | for _, alias := range cmd.aliases { 63 | if seen[alias] { 64 | return fmt.Errorf("alias duplicates existing command %q", alias) 65 | } 66 | c.commands[alias] = cmd 67 | } 68 | if err := cmd.init(); err != nil { 69 | return err 70 | } 71 | } 72 | if len(defaults) > 1 { 73 | return fmt.Errorf("more than one default subcommand exists: %s", strings.Join(defaults, ", ")) 74 | } 75 | return nil 76 | } 77 | 78 | func (c *cmdGroup) have() bool { 79 | return len(c.commands) > 0 80 | } 81 | 82 | type CmdClauseValidator func(*CmdClause) error 83 | 84 | // A CmdClause is a single top-level command. It encapsulates a set of flags 85 | // and either subcommands or positional arguments. 86 | type CmdClause struct { 87 | actionMixin 88 | *flagGroup 89 | *argGroup 90 | *cmdGroup 91 | app *Application 92 | name string 93 | aliases []string 94 | help string 95 | isDefault bool 96 | validator CmdClauseValidator 97 | hidden bool 98 | } 99 | 100 | func newCommand(app *Application, name, help string) *CmdClause { 101 | c := &CmdClause{ 102 | flagGroup: newFlagGroup(), 103 | argGroup: newArgGroup(), 104 | cmdGroup: newCmdGroup(app), 105 | app: app, 106 | name: name, 107 | help: help, 108 | } 109 | return c 110 | } 111 | 112 | // Add an Alias for this command. 113 | func (c *CmdClause) Alias(name string) *CmdClause { 114 | c.aliases = append(c.aliases, name) 115 | return c 116 | } 117 | 118 | // Validate sets a validation function to run when parsing. 119 | func (c *CmdClause) Validate(validator CmdClauseValidator) *CmdClause { 120 | c.validator = validator 121 | return c 122 | } 123 | 124 | func (c *CmdClause) FullCommand() string { 125 | out := []string{c.name} 126 | for p := c.parent; p != nil; p = p.parent { 127 | out = append([]string{p.name}, out...) 128 | } 129 | return strings.Join(out, " ") 130 | } 131 | 132 | // Command adds a new sub-command. 133 | func (c *CmdClause) Command(name, help string) *CmdClause { 134 | cmd := c.addCommand(name, help) 135 | cmd.parent = c 136 | return cmd 137 | } 138 | 139 | // Default makes this command the default if commands don't match. 140 | func (c *CmdClause) Default() *CmdClause { 141 | c.isDefault = true 142 | return c 143 | } 144 | 145 | func (c *CmdClause) Action(action Action) *CmdClause { 146 | c.addAction(action) 147 | return c 148 | } 149 | 150 | func (c *CmdClause) PreAction(action Action) *CmdClause { 151 | c.addPreAction(action) 152 | return c 153 | } 154 | 155 | func (c *CmdClause) init() error { 156 | if err := c.flagGroup.init(c.app.defaultEnvarPrefix()); err != nil { 157 | return err 158 | } 159 | if c.argGroup.have() && c.cmdGroup.have() { 160 | return fmt.Errorf("can't mix Arg()s with Command()s") 161 | } 162 | if err := c.argGroup.init(); err != nil { 163 | return err 164 | } 165 | if err := c.cmdGroup.init(); err != nil { 166 | return err 167 | } 168 | return nil 169 | } 170 | 171 | func (c *CmdClause) Hidden() *CmdClause { 172 | c.hidden = true 173 | return c 174 | } 175 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Data model for Kingpin command-line structure. 10 | 11 | type FlagGroupModel struct { 12 | Flags []*FlagModel 13 | } 14 | 15 | func (f *FlagGroupModel) FlagSummary() string { 16 | out := []string{} 17 | count := 0 18 | for _, flag := range f.Flags { 19 | if flag.Name != "help" { 20 | count++ 21 | } 22 | if flag.Required { 23 | if flag.IsBoolFlag() { 24 | out = append(out, fmt.Sprintf("--[no-]%s", flag.Name)) 25 | } else { 26 | out = append(out, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder())) 27 | } 28 | } 29 | } 30 | if count != len(out) { 31 | out = append(out, "[]") 32 | } 33 | return strings.Join(out, " ") 34 | } 35 | 36 | type FlagModel struct { 37 | Name string 38 | Help string 39 | Short rune 40 | Default string 41 | Envar string 42 | PlaceHolder string 43 | Required bool 44 | Hidden bool 45 | Value Value 46 | } 47 | 48 | func (f *FlagModel) String() string { 49 | return f.Value.String() 50 | } 51 | 52 | func (f *FlagModel) IsBoolFlag() bool { 53 | if fl, ok := f.Value.(boolFlag); ok { 54 | return fl.IsBoolFlag() 55 | } 56 | return false 57 | } 58 | 59 | func (f *FlagModel) FormatPlaceHolder() string { 60 | if f.PlaceHolder != "" { 61 | return f.PlaceHolder 62 | } 63 | if f.Default != "" { 64 | if _, ok := f.Value.(*stringValue); ok { 65 | return strconv.Quote(f.Default) 66 | } 67 | return f.Default 68 | } 69 | return strings.ToUpper(f.Name) 70 | } 71 | 72 | type ArgGroupModel struct { 73 | Args []*ArgModel 74 | } 75 | 76 | func (a *ArgGroupModel) ArgSummary() string { 77 | depth := 0 78 | out := []string{} 79 | for _, arg := range a.Args { 80 | h := "<" + arg.Name + ">" 81 | if !arg.Required { 82 | h = "[" + h 83 | depth++ 84 | } 85 | out = append(out, h) 86 | } 87 | out[len(out)-1] = out[len(out)-1] + strings.Repeat("]", depth) 88 | return strings.Join(out, " ") 89 | } 90 | 91 | type ArgModel struct { 92 | Name string 93 | Help string 94 | Default string 95 | Required bool 96 | Value Value 97 | } 98 | 99 | func (a *ArgModel) String() string { 100 | return a.Value.String() 101 | } 102 | 103 | type CmdGroupModel struct { 104 | Commands []*CmdModel 105 | } 106 | 107 | func (c *CmdGroupModel) FlattenedCommands() (out []*CmdModel) { 108 | for _, cmd := range c.Commands { 109 | if len(cmd.Commands) == 0 { 110 | out = append(out, cmd) 111 | } 112 | out = append(out, cmd.FlattenedCommands()...) 113 | } 114 | return 115 | } 116 | 117 | type CmdModel struct { 118 | Name string 119 | Aliases []string 120 | Help string 121 | FullCommand string 122 | Depth int 123 | Hidden bool 124 | Default bool 125 | *FlagGroupModel 126 | *ArgGroupModel 127 | *CmdGroupModel 128 | } 129 | 130 | func (c *CmdModel) String() string { 131 | return c.FullCommand 132 | } 133 | 134 | type ApplicationModel struct { 135 | Name string 136 | Help string 137 | Version string 138 | Author string 139 | *ArgGroupModel 140 | *CmdGroupModel 141 | *FlagGroupModel 142 | } 143 | 144 | func (a *Application) Model() *ApplicationModel { 145 | return &ApplicationModel{ 146 | Name: a.Name, 147 | Help: a.Help, 148 | Version: a.version, 149 | Author: a.author, 150 | FlagGroupModel: a.flagGroup.Model(), 151 | ArgGroupModel: a.argGroup.Model(), 152 | CmdGroupModel: a.cmdGroup.Model(), 153 | } 154 | } 155 | 156 | func (a *argGroup) Model() *ArgGroupModel { 157 | m := &ArgGroupModel{} 158 | for _, arg := range a.args { 159 | m.Args = append(m.Args, arg.Model()) 160 | } 161 | return m 162 | } 163 | 164 | func (a *ArgClause) Model() *ArgModel { 165 | return &ArgModel{ 166 | Name: a.name, 167 | Help: a.help, 168 | Default: a.defaultValue, 169 | Required: a.required, 170 | Value: a.value, 171 | } 172 | } 173 | 174 | func (f *flagGroup) Model() *FlagGroupModel { 175 | m := &FlagGroupModel{} 176 | for _, fl := range f.flagOrder { 177 | m.Flags = append(m.Flags, fl.Model()) 178 | } 179 | return m 180 | } 181 | 182 | func (f *FlagClause) Model() *FlagModel { 183 | return &FlagModel{ 184 | Name: f.name, 185 | Help: f.help, 186 | Short: rune(f.shorthand), 187 | Default: f.defaultValue, 188 | Envar: f.envar, 189 | PlaceHolder: f.placeholder, 190 | Required: f.required, 191 | Hidden: f.hidden, 192 | Value: f.value, 193 | } 194 | } 195 | 196 | func (c *cmdGroup) Model() *CmdGroupModel { 197 | m := &CmdGroupModel{} 198 | for _, cm := range c.commandOrder { 199 | m.Commands = append(m.Commands, cm.Model()) 200 | } 201 | return m 202 | } 203 | 204 | func (c *CmdClause) Model() *CmdModel { 205 | depth := 0 206 | for i := c; i != nil; i = i.parent { 207 | depth++ 208 | } 209 | return &CmdModel{ 210 | Name: c.name, 211 | Aliases: c.aliases, 212 | Help: c.help, 213 | Depth: depth, 214 | Hidden: c.hidden, 215 | Default: c.isDefault, 216 | FullCommand: c.FullCommand(), 217 | FlagGroupModel: c.flagGroup.Model(), 218 | ArgGroupModel: c.argGroup.Model(), 219 | CmdGroupModel: c.cmdGroup.Model(), 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /cmd_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "testing" 9 | ) 10 | 11 | func parseAndExecute(app *Application, context *ParseContext) (string, error) { 12 | if err := parse(context, app); err != nil { 13 | return "", err 14 | } 15 | return app.execute(context) 16 | } 17 | 18 | func TestNestedCommands(t *testing.T) { 19 | app := New("app", "") 20 | sub1 := app.Command("sub1", "") 21 | sub1.Flag("sub1", "") 22 | subsub1 := sub1.Command("sub1sub1", "") 23 | subsub1.Command("sub1sub1end", "") 24 | 25 | sub2 := app.Command("sub2", "") 26 | sub2.Flag("sub2", "") 27 | sub2.Command("sub2sub1", "") 28 | 29 | context := tokenize([]string{"sub1", "sub1sub1", "sub1sub1end"}, false) 30 | selected, err := parseAndExecute(app, context) 31 | assert.NoError(t, err) 32 | assert.True(t, context.EOL()) 33 | assert.Equal(t, "sub1 sub1sub1 sub1sub1end", selected) 34 | } 35 | 36 | func TestNestedCommandsWithArgs(t *testing.T) { 37 | app := New("app", "") 38 | cmd := app.Command("a", "").Command("b", "") 39 | a := cmd.Arg("a", "").String() 40 | b := cmd.Arg("b", "").String() 41 | context := tokenize([]string{"a", "b", "c", "d"}, false) 42 | selected, err := parseAndExecute(app, context) 43 | assert.NoError(t, err) 44 | assert.True(t, context.EOL()) 45 | assert.Equal(t, "a b", selected) 46 | assert.Equal(t, "c", *a) 47 | assert.Equal(t, "d", *b) 48 | } 49 | 50 | func TestNestedCommandsWithFlags(t *testing.T) { 51 | app := New("app", "") 52 | cmd := app.Command("a", "").Command("b", "") 53 | a := cmd.Flag("aaa", "").Short('a').String() 54 | b := cmd.Flag("bbb", "").Short('b').String() 55 | err := app.init() 56 | assert.NoError(t, err) 57 | context := tokenize(strings.Split("a b --aaa x -b x", " "), false) 58 | selected, err := parseAndExecute(app, context) 59 | assert.NoError(t, err) 60 | assert.True(t, context.EOL()) 61 | assert.Equal(t, "a b", selected) 62 | assert.Equal(t, "x", *a) 63 | assert.Equal(t, "x", *b) 64 | } 65 | 66 | func TestNestedCommandWithMergedFlags(t *testing.T) { 67 | app := New("app", "") 68 | cmd0 := app.Command("a", "") 69 | cmd0f0 := cmd0.Flag("aflag", "").Bool() 70 | // cmd1 := app.Command("b", "") 71 | // cmd1f0 := cmd0.Flag("bflag", "").Bool() 72 | cmd00 := cmd0.Command("aa", "") 73 | cmd00f0 := cmd00.Flag("aaflag", "").Bool() 74 | err := app.init() 75 | assert.NoError(t, err) 76 | context := tokenize(strings.Split("a aa --aflag --aaflag", " "), false) 77 | selected, err := parseAndExecute(app, context) 78 | assert.NoError(t, err) 79 | assert.True(t, *cmd0f0) 80 | assert.True(t, *cmd00f0) 81 | assert.Equal(t, "a aa", selected) 82 | } 83 | 84 | func TestNestedCommandWithDuplicateFlagErrors(t *testing.T) { 85 | app := New("app", "") 86 | app.Flag("test", "").Bool() 87 | app.Command("cmd0", "").Flag("test", "").Bool() 88 | err := app.init() 89 | assert.Error(t, err) 90 | } 91 | 92 | func TestNestedCommandWithArgAndMergedFlags(t *testing.T) { 93 | app := New("app", "") 94 | cmd0 := app.Command("a", "") 95 | cmd0f0 := cmd0.Flag("aflag", "").Bool() 96 | // cmd1 := app.Command("b", "") 97 | // cmd1f0 := cmd0.Flag("bflag", "").Bool() 98 | cmd00 := cmd0.Command("aa", "") 99 | cmd00a0 := cmd00.Arg("arg", "").String() 100 | cmd00f0 := cmd00.Flag("aaflag", "").Bool() 101 | err := app.init() 102 | assert.NoError(t, err) 103 | context := tokenize(strings.Split("a aa hello --aflag --aaflag", " "), false) 104 | selected, err := parseAndExecute(app, context) 105 | assert.NoError(t, err) 106 | assert.True(t, *cmd0f0) 107 | assert.True(t, *cmd00f0) 108 | assert.Equal(t, "a aa", selected) 109 | assert.Equal(t, "hello", *cmd00a0) 110 | } 111 | 112 | func TestDefaultSubcommandEOL(t *testing.T) { 113 | app := newTestApp() 114 | c0 := app.Command("c0", "").Default() 115 | c0.Command("c01", "").Default() 116 | c0.Command("c02", "") 117 | 118 | cmd, err := app.Parse([]string{"c0"}) 119 | assert.NoError(t, err) 120 | assert.Equal(t, "c0 c01", cmd) 121 | } 122 | 123 | func TestDefaultSubcommandWithArg(t *testing.T) { 124 | app := newTestApp() 125 | c0 := app.Command("c0", "").Default() 126 | c01 := c0.Command("c01", "").Default() 127 | c012 := c01.Command("c012", "").Default() 128 | a0 := c012.Arg("a0", "").String() 129 | c0.Command("c02", "") 130 | 131 | cmd, err := app.Parse([]string{"c0", "hello"}) 132 | assert.NoError(t, err) 133 | assert.Equal(t, "c0 c01 c012", cmd) 134 | assert.Equal(t, "hello", *a0) 135 | } 136 | 137 | func TestDefaultSubcommandWithFlags(t *testing.T) { 138 | app := newTestApp() 139 | c0 := app.Command("c0", "").Default() 140 | _ = c0.Flag("f0", "").Int() 141 | c0c1 := c0.Command("c1", "").Default() 142 | c0c1f1 := c0c1.Flag("f1", "").Int() 143 | selected, err := app.Parse([]string{"--f1=2"}) 144 | assert.NoError(t, err) 145 | assert.Equal(t, "c0 c1", selected) 146 | assert.Equal(t, 2, *c0c1f1) 147 | _, err = app.Parse([]string{"--f2"}) 148 | assert.Error(t, err) 149 | } 150 | 151 | func TestMultipleDefaultCommands(t *testing.T) { 152 | app := newTestApp() 153 | app.Command("c0", "").Default() 154 | app.Command("c1", "").Default() 155 | _, err := app.Parse([]string{}) 156 | assert.Error(t, err) 157 | } 158 | 159 | func TestAliasedCommand(t *testing.T) { 160 | app := newTestApp() 161 | app.Command("one", "").Alias("two") 162 | selected, _ := app.Parse([]string{"one"}) 163 | assert.Equal(t, "one", selected) 164 | selected, _ = app.Parse([]string{"two"}) 165 | assert.Equal(t, "one", selected) 166 | // 2 due to "help" and "one" 167 | assert.Equal(t, 2, len(app.Model().FlattenedCommands())) 168 | } 169 | 170 | func TestDuplicateAlias(t *testing.T) { 171 | app := newTestApp() 172 | app.Command("one", "") 173 | app.Command("two", "").Alias("one") 174 | _, err := app.Parse([]string{"one"}) 175 | assert.Error(t, err) 176 | } 177 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func newTestApp() *Application { 13 | return New("test", "").Terminate(nil) 14 | } 15 | 16 | func TestCommander(t *testing.T) { 17 | c := newTestApp() 18 | ping := c.Command("ping", "Ping an IP address.") 19 | pingTTL := ping.Flag("ttl", "TTL for ICMP packets").Short('t').Default("5s").Duration() 20 | 21 | selected, err := c.Parse([]string{"ping"}) 22 | assert.NoError(t, err) 23 | assert.Equal(t, "ping", selected) 24 | assert.Equal(t, 5*time.Second, *pingTTL) 25 | 26 | selected, err = c.Parse([]string{"ping", "--ttl=10s"}) 27 | assert.NoError(t, err) 28 | assert.Equal(t, "ping", selected) 29 | assert.Equal(t, 10*time.Second, *pingTTL) 30 | } 31 | 32 | func TestRequiredFlags(t *testing.T) { 33 | c := newTestApp() 34 | c.Flag("a", "a").String() 35 | c.Flag("b", "b").Required().String() 36 | 37 | _, err := c.Parse([]string{"--a=foo"}) 38 | assert.Error(t, err) 39 | _, err = c.Parse([]string{"--b=foo"}) 40 | assert.NoError(t, err) 41 | } 42 | 43 | func TestInvalidDefaultFlagValueErrors(t *testing.T) { 44 | c := newTestApp() 45 | c.Flag("foo", "foo").Default("a").Int() 46 | _, err := c.Parse([]string{}) 47 | assert.Error(t, err) 48 | } 49 | 50 | func TestInvalidDefaultArgValueErrors(t *testing.T) { 51 | c := newTestApp() 52 | cmd := c.Command("cmd", "cmd") 53 | cmd.Arg("arg", "arg").Default("one").Int() 54 | _, err := c.Parse([]string{"cmd"}) 55 | assert.Error(t, err) 56 | } 57 | 58 | func TestArgsRequiredAfterNonRequiredErrors(t *testing.T) { 59 | c := newTestApp() 60 | cmd := c.Command("cmd", "") 61 | cmd.Arg("a", "a").String() 62 | cmd.Arg("b", "b").Required().String() 63 | _, err := c.Parse([]string{"cmd"}) 64 | assert.Error(t, err) 65 | } 66 | 67 | func TestArgsMultipleRequiredThenNonRequired(t *testing.T) { 68 | c := newTestApp().Writer(ioutil.Discard) 69 | cmd := c.Command("cmd", "") 70 | cmd.Arg("a", "a").Required().String() 71 | cmd.Arg("b", "b").Required().String() 72 | cmd.Arg("c", "c").String() 73 | cmd.Arg("d", "d").String() 74 | _, err := c.Parse([]string{"cmd", "a", "b"}) 75 | assert.NoError(t, err) 76 | _, err = c.Parse([]string{}) 77 | assert.Error(t, err) 78 | } 79 | 80 | func TestDispatchCallbackIsCalled(t *testing.T) { 81 | dispatched := false 82 | c := New("test", "") 83 | c.Command("cmd", "").Action(func(*ParseContext) error { 84 | dispatched = true 85 | return nil 86 | }) 87 | 88 | _, err := c.Parse([]string{"cmd"}) 89 | assert.NoError(t, err) 90 | assert.True(t, dispatched) 91 | } 92 | 93 | func TestTopLevelArgWorks(t *testing.T) { 94 | c := newTestApp() 95 | s := c.Arg("arg", "help").String() 96 | _, err := c.Parse([]string{"foo"}) 97 | assert.NoError(t, err) 98 | assert.Equal(t, "foo", *s) 99 | } 100 | 101 | func TestTopLevelArgCantBeUsedWithCommands(t *testing.T) { 102 | c := newTestApp() 103 | c.Arg("arg", "help").String() 104 | c.Command("cmd", "help") 105 | _, err := c.Parse([]string{}) 106 | assert.Error(t, err) 107 | } 108 | 109 | func TestTooManyArgs(t *testing.T) { 110 | a := newTestApp() 111 | a.Arg("a", "").String() 112 | _, err := a.Parse([]string{"a", "b"}) 113 | assert.Error(t, err) 114 | } 115 | 116 | func TestTooManyArgsAfterCommand(t *testing.T) { 117 | a := newTestApp() 118 | a.Command("a", "") 119 | assert.NoError(t, a.init()) 120 | _, err := a.Parse([]string{"a", "b"}) 121 | assert.Error(t, err) 122 | } 123 | 124 | func TestArgsLooksLikeFlagsWithConsumeRemainder(t *testing.T) { 125 | a := New("test", "") 126 | a.Arg("opts", "").Required().Strings() 127 | _, err := a.Parse([]string{"hello", "-world"}) 128 | assert.Error(t, err) 129 | } 130 | 131 | func TestCommandParseDoesNotResetFlagsToDefault(t *testing.T) { 132 | app := New("test", "") 133 | flag := app.Flag("flag", "").Default("default").String() 134 | app.Command("cmd", "") 135 | 136 | _, err := app.Parse([]string{"--flag=123", "cmd"}) 137 | assert.NoError(t, err) 138 | assert.Equal(t, "123", *flag) 139 | } 140 | 141 | func TestCommandParseDoesNotFailRequired(t *testing.T) { 142 | app := New("test", "") 143 | flag := app.Flag("flag", "").Required().String() 144 | app.Command("cmd", "") 145 | 146 | _, err := app.Parse([]string{"cmd", "--flag=123"}) 147 | assert.NoError(t, err) 148 | assert.Equal(t, "123", *flag) 149 | } 150 | 151 | func TestSelectedCommand(t *testing.T) { 152 | app := New("test", "help") 153 | c0 := app.Command("c0", "") 154 | c0.Command("c1", "") 155 | s, err := app.Parse([]string{"c0", "c1"}) 156 | assert.NoError(t, err) 157 | assert.Equal(t, "c0 c1", s) 158 | } 159 | 160 | func TestSubCommandRequired(t *testing.T) { 161 | app := New("test", "help") 162 | c0 := app.Command("c0", "") 163 | c0.Command("c1", "") 164 | _, err := app.Parse([]string{"c0"}) 165 | assert.Error(t, err) 166 | } 167 | 168 | func TestInterspersedFalse(t *testing.T) { 169 | app := New("test", "help").Interspersed(false) 170 | a1 := app.Arg("a1", "").String() 171 | a2 := app.Arg("a2", "").String() 172 | f1 := app.Flag("flag", "").String() 173 | 174 | _, err := app.Parse([]string{"a1", "--flag=flag"}) 175 | assert.NoError(t, err) 176 | assert.Equal(t, "a1", *a1) 177 | assert.Equal(t, "--flag=flag", *a2) 178 | assert.Equal(t, "", *f1) 179 | } 180 | 181 | func TestInterspersedTrue(t *testing.T) { 182 | // test once with the default value and once with explicit true 183 | for i := 0; i < 2; i++ { 184 | app := New("test", "help") 185 | if i != 0 { 186 | t.Log("Setting explicit") 187 | app.Interspersed(true) 188 | } else { 189 | t.Log("Using default") 190 | } 191 | a1 := app.Arg("a1", "").String() 192 | a2 := app.Arg("a2", "").String() 193 | f1 := app.Flag("flag", "").String() 194 | 195 | _, err := app.Parse([]string{"a1", "--flag=flag"}) 196 | assert.NoError(t, err) 197 | assert.Equal(t, "a1", *a1) 198 | assert.Equal(t, "", *a2) 199 | assert.Equal(t, "flag", *f1) 200 | } 201 | } 202 | 203 | func TestDefaultEnvars(t *testing.T) { 204 | a := New("some-app", "").DefaultEnvars() 205 | f0 := a.Flag("some-flag", "") 206 | f0.Bool() 207 | f1 := a.Flag("some-other-flag", "").NoEnvar() 208 | f1.Bool() 209 | _, err := a.Parse([]string{}) 210 | assert.NoError(t, err) 211 | assert.Equal(t, "SOME_APP_SOME_FLAG", f0.envar) 212 | assert.Equal(t, "", f1.envar) 213 | } 214 | -------------------------------------------------------------------------------- /usage.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/doc" 7 | "io" 8 | "strings" 9 | 10 | "github.com/alecthomas/template" 11 | ) 12 | 13 | var ( 14 | preIndent = " " 15 | ) 16 | 17 | func formatTwoColumns(w io.Writer, indent, padding, width int, rows [][2]string) { 18 | // Find size of first column. 19 | s := 0 20 | for _, row := range rows { 21 | if c := len(row[0]); c > s && c < 30 { 22 | s = c 23 | } 24 | } 25 | 26 | indentStr := strings.Repeat(" ", indent) 27 | offsetStr := strings.Repeat(" ", s+padding) 28 | 29 | for _, row := range rows { 30 | buf := bytes.NewBuffer(nil) 31 | doc.ToText(buf, row[1], "", preIndent, width-s-padding-indent) 32 | lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") 33 | fmt.Fprintf(w, "%s%-*s%*s", indentStr, s, row[0], padding, "") 34 | if len(row[0]) >= 30 { 35 | fmt.Fprintf(w, "\n%s%s", indentStr, offsetStr) 36 | } 37 | fmt.Fprintf(w, "%s\n", lines[0]) 38 | for _, line := range lines[1:] { 39 | fmt.Fprintf(w, "%s%s%s\n", indentStr, offsetStr, line) 40 | } 41 | } 42 | } 43 | 44 | // Usage writes application usage to w. It parses args to determine 45 | // appropriate help context, such as which command to show help for. 46 | func (a *Application) Usage(args []string) { 47 | context, err := a.parseContext(true, args) 48 | a.FatalIfError(err, "") 49 | if err := a.UsageForContextWithTemplate(context, 2, a.usageTemplate); err != nil { 50 | panic(err) 51 | } 52 | } 53 | 54 | func formatAppUsage(app *ApplicationModel) string { 55 | s := []string{app.Name} 56 | if len(app.Flags) > 0 { 57 | s = append(s, app.FlagSummary()) 58 | } 59 | if len(app.Args) > 0 { 60 | s = append(s, app.ArgSummary()) 61 | } 62 | return strings.Join(s, " ") 63 | } 64 | 65 | func formatCmdUsage(app *ApplicationModel, cmd *CmdModel) string { 66 | s := []string{app.Name, cmd.String()} 67 | if len(app.Flags) > 0 { 68 | s = append(s, app.FlagSummary()) 69 | } 70 | if len(app.Args) > 0 { 71 | s = append(s, app.ArgSummary()) 72 | } 73 | return strings.Join(s, " ") 74 | } 75 | 76 | func formatFlag(haveShort bool, flag *FlagModel) string { 77 | flagString := "" 78 | if flag.Short != 0 { 79 | flagString += fmt.Sprintf("-%c, --%s", flag.Short, flag.Name) 80 | } else { 81 | if haveShort { 82 | flagString += fmt.Sprintf(" --%s", flag.Name) 83 | } else { 84 | flagString += fmt.Sprintf("--%s", flag.Name) 85 | } 86 | } 87 | if !flag.IsBoolFlag() { 88 | flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder()) 89 | } 90 | return flagString 91 | } 92 | 93 | type templateParseContext struct { 94 | SelectedCommand *CmdModel 95 | *FlagGroupModel 96 | *ArgGroupModel 97 | } 98 | 99 | type templateContext struct { 100 | App *ApplicationModel 101 | Width int 102 | Context *templateParseContext 103 | } 104 | 105 | // UsageForContext displays usage information from a ParseContext (obtained from 106 | // Application.ParseContext() or Action(f) callbacks). 107 | func (a *Application) UsageForContext(context *ParseContext) error { 108 | return a.UsageForContextWithTemplate(context, 2, a.usageTemplate) 109 | } 110 | 111 | // UsageForContextWithTemplate is the base usage function. You generally don't need to use this. 112 | func (a *Application) UsageForContextWithTemplate(context *ParseContext, indent int, tmpl string) error { 113 | width := guessWidth(a.writer) 114 | funcs := template.FuncMap{ 115 | "Indent": func(level int) string { 116 | return strings.Repeat(" ", level*indent) 117 | }, 118 | "Wrap": func(indent int, s string) string { 119 | buf := bytes.NewBuffer(nil) 120 | indentText := strings.Repeat(" ", indent) 121 | doc.ToText(buf, s, indentText, indentText, width-indent) 122 | return buf.String() 123 | }, 124 | "FormatFlag": formatFlag, 125 | "FlagsToTwoColumns": func(f []*FlagModel) [][2]string { 126 | rows := [][2]string{} 127 | haveShort := false 128 | for _, flag := range f { 129 | if flag.Short != 0 { 130 | haveShort = true 131 | break 132 | } 133 | } 134 | for _, flag := range f { 135 | if !flag.Hidden { 136 | rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help}) 137 | } 138 | } 139 | return rows 140 | }, 141 | "RequiredFlags": func(f []*FlagModel) []*FlagModel { 142 | requiredFlags := []*FlagModel{} 143 | for _, flag := range f { 144 | if flag.Required == true { 145 | requiredFlags = append(requiredFlags, flag) 146 | } 147 | } 148 | return requiredFlags 149 | }, 150 | "OptionalFlags": func(f []*FlagModel) []*FlagModel { 151 | optionalFlags := []*FlagModel{} 152 | for _, flag := range f { 153 | if flag.Required == false { 154 | optionalFlags = append(optionalFlags, flag) 155 | } 156 | } 157 | return optionalFlags 158 | }, 159 | "ArgsToTwoColumns": func(a []*ArgModel) [][2]string { 160 | rows := [][2]string{} 161 | for _, arg := range a { 162 | s := "<" + arg.Name + ">" 163 | if !arg.Required { 164 | s = "[" + s + "]" 165 | } 166 | rows = append(rows, [2]string{s, arg.Help}) 167 | } 168 | return rows 169 | }, 170 | "FormatTwoColumns": func(rows [][2]string) string { 171 | buf := bytes.NewBuffer(nil) 172 | formatTwoColumns(buf, indent, indent, width, rows) 173 | return buf.String() 174 | }, 175 | "FormatTwoColumnsWithIndent": func(rows [][2]string, indent, padding int) string { 176 | buf := bytes.NewBuffer(nil) 177 | formatTwoColumns(buf, indent, padding, width, rows) 178 | return buf.String() 179 | }, 180 | "FormatAppUsage": formatAppUsage, 181 | "FormatCommandUsage": formatCmdUsage, 182 | "IsCumulative": func(value Value) bool { 183 | _, ok := value.(remainderArg) 184 | return ok 185 | }, 186 | "Char": func(c rune) string { 187 | return string(c) 188 | }, 189 | } 190 | t, err := template.New("usage").Funcs(funcs).Parse(tmpl) 191 | if err != nil { 192 | return err 193 | } 194 | var selectedCommand *CmdModel 195 | if context.SelectedCommand != nil { 196 | selectedCommand = context.SelectedCommand.Model() 197 | } 198 | ctx := templateContext{ 199 | App: a.Model(), 200 | Width: width, 201 | Context: &templateParseContext{ 202 | SelectedCommand: selectedCommand, 203 | FlagGroupModel: context.flags.Model(), 204 | ArgGroupModel: context.arguments.Model(), 205 | }, 206 | } 207 | return t.Execute(a.writer, ctx) 208 | } 209 | -------------------------------------------------------------------------------- /parsers.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "os" 7 | "regexp" 8 | "time" 9 | 10 | "github.com/alecthomas/units" 11 | ) 12 | 13 | type Settings interface { 14 | SetValue(value Value) 15 | } 16 | 17 | type parserMixin struct { 18 | value Value 19 | required bool 20 | } 21 | 22 | func (p *parserMixin) SetValue(value Value) { 23 | p.value = value 24 | } 25 | 26 | // StringMap provides key=value parsing into a map. 27 | func (p *parserMixin) StringMap() (target *map[string]string) { 28 | target = &(map[string]string{}) 29 | p.StringMapVar(target) 30 | return 31 | } 32 | 33 | // Duration sets the parser to a time.Duration parser. 34 | func (p *parserMixin) Duration() (target *time.Duration) { 35 | target = new(time.Duration) 36 | p.DurationVar(target) 37 | return 38 | } 39 | 40 | // Bytes parses numeric byte units. eg. 1.5KB 41 | func (p *parserMixin) Bytes() (target *units.Base2Bytes) { 42 | target = new(units.Base2Bytes) 43 | p.BytesVar(target) 44 | return 45 | } 46 | 47 | // IP sets the parser to a net.IP parser. 48 | func (p *parserMixin) IP() (target *net.IP) { 49 | target = new(net.IP) 50 | p.IPVar(target) 51 | return 52 | } 53 | 54 | // TCP (host:port) address. 55 | func (p *parserMixin) TCP() (target **net.TCPAddr) { 56 | target = new(*net.TCPAddr) 57 | p.TCPVar(target) 58 | return 59 | } 60 | 61 | // TCPVar (host:port) address. 62 | func (p *parserMixin) TCPVar(target **net.TCPAddr) { 63 | p.SetValue(newTCPAddrValue(target)) 64 | } 65 | 66 | // ExistingFile sets the parser to one that requires and returns an existing file. 67 | func (p *parserMixin) ExistingFile() (target *string) { 68 | target = new(string) 69 | p.ExistingFileVar(target) 70 | return 71 | } 72 | 73 | // ExistingDir sets the parser to one that requires and returns an existing directory. 74 | func (p *parserMixin) ExistingDir() (target *string) { 75 | target = new(string) 76 | p.ExistingDirVar(target) 77 | return 78 | } 79 | 80 | // ExistingFileOrDir sets the parser to one that requires and returns an existing file OR directory. 81 | func (p *parserMixin) ExistingFileOrDir() (target *string) { 82 | target = new(string) 83 | p.ExistingFileOrDirVar(target) 84 | return 85 | } 86 | 87 | // File returns an os.File against an existing file. 88 | func (p *parserMixin) File() (target **os.File) { 89 | target = new(*os.File) 90 | p.FileVar(target) 91 | return 92 | } 93 | 94 | // File attempts to open a File with os.OpenFile(flag, perm). 95 | func (p *parserMixin) OpenFile(flag int, perm os.FileMode) (target **os.File) { 96 | target = new(*os.File) 97 | p.OpenFileVar(target, flag, perm) 98 | return 99 | } 100 | 101 | // URL provides a valid, parsed url.URL. 102 | func (p *parserMixin) URL() (target **url.URL) { 103 | target = new(*url.URL) 104 | p.URLVar(target) 105 | return 106 | } 107 | 108 | // StringMap provides key=value parsing into a map. 109 | func (p *parserMixin) StringMapVar(target *map[string]string) { 110 | p.SetValue(newStringMapValue(target)) 111 | } 112 | 113 | // Float sets the parser to a float64 parser. 114 | func (p *parserMixin) Float() (target *float64) { 115 | return p.Float64() 116 | } 117 | 118 | // Float sets the parser to a float64 parser. 119 | func (p *parserMixin) FloatVar(target *float64) { 120 | p.Float64Var(target) 121 | } 122 | 123 | // Duration sets the parser to a time.Duration parser. 124 | func (p *parserMixin) DurationVar(target *time.Duration) { 125 | p.SetValue(newDurationValue(target)) 126 | } 127 | 128 | // BytesVar parses numeric byte units. eg. 1.5KB 129 | func (p *parserMixin) BytesVar(target *units.Base2Bytes) { 130 | p.SetValue(newBytesValue(target)) 131 | } 132 | 133 | // IP sets the parser to a net.IP parser. 134 | func (p *parserMixin) IPVar(target *net.IP) { 135 | p.SetValue(newIPValue(target)) 136 | } 137 | 138 | // ExistingFile sets the parser to one that requires and returns an existing file. 139 | func (p *parserMixin) ExistingFileVar(target *string) { 140 | p.SetValue(newExistingFileValue(target)) 141 | } 142 | 143 | // ExistingDir sets the parser to one that requires and returns an existing directory. 144 | func (p *parserMixin) ExistingDirVar(target *string) { 145 | p.SetValue(newExistingDirValue(target)) 146 | } 147 | 148 | // ExistingDir sets the parser to one that requires and returns an existing directory. 149 | func (p *parserMixin) ExistingFileOrDirVar(target *string) { 150 | p.SetValue(newExistingFileOrDirValue(target)) 151 | } 152 | 153 | // FileVar opens an existing file. 154 | func (p *parserMixin) FileVar(target **os.File) { 155 | p.SetValue(newFileValue(target, os.O_RDONLY, 0)) 156 | } 157 | 158 | // OpenFileVar calls os.OpenFile(flag, perm) 159 | func (p *parserMixin) OpenFileVar(target **os.File, flag int, perm os.FileMode) { 160 | p.SetValue(newFileValue(target, flag, perm)) 161 | } 162 | 163 | // URL provides a valid, parsed url.URL. 164 | func (p *parserMixin) URLVar(target **url.URL) { 165 | p.SetValue(newURLValue(target)) 166 | } 167 | 168 | // URLList provides a parsed list of url.URL values. 169 | func (p *parserMixin) URLList() (target *[]*url.URL) { 170 | target = new([]*url.URL) 171 | p.URLListVar(target) 172 | return 173 | } 174 | 175 | // URLListVar provides a parsed list of url.URL values. 176 | func (p *parserMixin) URLListVar(target *[]*url.URL) { 177 | p.SetValue(newURLListValue(target)) 178 | } 179 | 180 | // Enum allows a value from a set of options. 181 | func (p *parserMixin) Enum(options ...string) (target *string) { 182 | target = new(string) 183 | p.EnumVar(target, options...) 184 | return 185 | } 186 | 187 | // EnumVar allows a value from a set of options. 188 | func (p *parserMixin) EnumVar(target *string, options ...string) { 189 | p.SetValue(newEnumFlag(target, options...)) 190 | } 191 | 192 | // Enums allows a set of values from a set of options. 193 | func (p *parserMixin) Enums(options ...string) (target *[]string) { 194 | target = new([]string) 195 | p.EnumsVar(target, options...) 196 | return 197 | } 198 | 199 | // EnumVar allows a value from a set of options. 200 | func (p *parserMixin) EnumsVar(target *[]string, options ...string) { 201 | p.SetValue(newEnumsFlag(target, options...)) 202 | } 203 | 204 | // A Counter increments a number each time it is encountered. 205 | func (p *parserMixin) Counter() (target *int) { 206 | target = new(int) 207 | p.CounterVar(target) 208 | return 209 | } 210 | 211 | func (p *parserMixin) CounterVar(target *int) { 212 | p.SetValue(newCounterValue(target)) 213 | } 214 | 215 | // Regexp 216 | func (p *parserMixin) Regexp() (target **regexp.Regexp) { 217 | target = new(*regexp.Regexp) 218 | p.RegexpVar(target) 219 | return 220 | } 221 | 222 | // Regexp 223 | func (p *parserMixin) RegexpVar(target **regexp.Regexp) { 224 | p.SetValue(newRegexpValue(target)) 225 | } 226 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | type flagGroup struct { 10 | short map[string]*FlagClause 11 | long map[string]*FlagClause 12 | flagOrder []*FlagClause 13 | } 14 | 15 | func newFlagGroup() *flagGroup { 16 | return &flagGroup{ 17 | short: make(map[string]*FlagClause), 18 | long: make(map[string]*FlagClause), 19 | } 20 | } 21 | 22 | func (f *flagGroup) merge(o *flagGroup) { 23 | for _, flag := range o.flagOrder { 24 | if flag.shorthand != 0 { 25 | f.short[string(flag.shorthand)] = flag 26 | } 27 | f.long[flag.name] = flag 28 | f.flagOrder = append(f.flagOrder, flag) 29 | } 30 | } 31 | 32 | // Flag defines a new flag with the given long name and help. 33 | func (f *flagGroup) Flag(name, help string) *FlagClause { 34 | flag := newFlag(name, help) 35 | f.long[name] = flag 36 | f.flagOrder = append(f.flagOrder, flag) 37 | return flag 38 | } 39 | 40 | func (f *flagGroup) init(defaultEnvarPrefix string) error { 41 | for _, flag := range f.long { 42 | if defaultEnvarPrefix != "" && !flag.noEnvar && flag.envar == "" { 43 | flag.envar = envarTransform(defaultEnvarPrefix + "_" + flag.name) 44 | } 45 | if err := flag.init(); err != nil { 46 | return err 47 | } 48 | if flag.shorthand != 0 { 49 | f.short[string(flag.shorthand)] = flag 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | func (f *flagGroup) parse(context *ParseContext) (*FlagClause, error) { 56 | var token *Token 57 | 58 | loop: 59 | for { 60 | token = context.Peek() 61 | switch token.Type { 62 | case TokenEOL: 63 | break loop 64 | 65 | case TokenLong, TokenShort: 66 | flagToken := token 67 | defaultValue := "" 68 | var flag *FlagClause 69 | var ok bool 70 | invert := false 71 | 72 | name := token.Value 73 | if token.Type == TokenLong { 74 | if strings.HasPrefix(name, "no-") { 75 | name = name[3:] 76 | invert = true 77 | } 78 | flag, ok = f.long[name] 79 | if !ok { 80 | return nil, fmt.Errorf("unknown long flag '%s'", flagToken) 81 | } 82 | } else { 83 | flag, ok = f.short[name] 84 | if !ok { 85 | return nil, fmt.Errorf("unknown short flag '%s'", flagToken) 86 | } 87 | } 88 | 89 | context.Next() 90 | 91 | fb, ok := flag.value.(boolFlag) 92 | if ok && fb.IsBoolFlag() { 93 | if invert { 94 | defaultValue = "false" 95 | } else { 96 | defaultValue = "true" 97 | } 98 | } else { 99 | if invert { 100 | context.Push(token) 101 | return nil, fmt.Errorf("unknown long flag '%s'", flagToken) 102 | } 103 | token = context.Peek() 104 | if token.Type != TokenArg { 105 | context.Push(token) 106 | return nil, fmt.Errorf("expected argument for flag '%s'", flagToken) 107 | } 108 | context.Next() 109 | defaultValue = token.Value 110 | } 111 | 112 | context.matchedFlag(flag, defaultValue) 113 | return flag, nil 114 | 115 | default: 116 | break loop 117 | } 118 | } 119 | return nil, nil 120 | } 121 | 122 | func (f *flagGroup) visibleFlags() int { 123 | count := 0 124 | for _, flag := range f.long { 125 | if !flag.hidden { 126 | count++ 127 | } 128 | } 129 | return count 130 | } 131 | 132 | // FlagClause is a fluid interface used to build flags. 133 | type FlagClause struct { 134 | parserMixin 135 | actionMixin 136 | name string 137 | shorthand byte 138 | help string 139 | envar string 140 | noEnvar bool 141 | defaultValue string 142 | placeholder string 143 | hidden bool 144 | } 145 | 146 | func newFlag(name, help string) *FlagClause { 147 | f := &FlagClause{ 148 | name: name, 149 | help: help, 150 | } 151 | return f 152 | } 153 | 154 | func (f *FlagClause) needsValue() bool { 155 | return f.required && f.defaultValue == "" 156 | } 157 | 158 | func (f *FlagClause) formatPlaceHolder() string { 159 | if f.placeholder != "" { 160 | return f.placeholder 161 | } 162 | if f.defaultValue != "" { 163 | if _, ok := f.value.(*stringValue); ok { 164 | return fmt.Sprintf("%q", f.defaultValue) 165 | } 166 | return f.defaultValue 167 | } 168 | return strings.ToUpper(f.name) 169 | } 170 | 171 | func (f *FlagClause) init() error { 172 | if f.required && f.defaultValue != "" { 173 | return fmt.Errorf("required flag '--%s' with default value that will never be used", f.name) 174 | } 175 | if f.value == nil { 176 | return fmt.Errorf("no type defined for --%s (eg. .String())", f.name) 177 | } 178 | if !f.noEnvar && f.envar != "" { 179 | if v := os.Getenv(f.envar); v != "" { 180 | f.defaultValue = v 181 | } 182 | } 183 | return nil 184 | } 185 | 186 | // Dispatch to the given function after the flag is parsed and validated. 187 | func (f *FlagClause) Action(action Action) *FlagClause { 188 | f.addAction(action) 189 | return f 190 | } 191 | 192 | func (f *FlagClause) PreAction(action Action) *FlagClause { 193 | f.addPreAction(action) 194 | return f 195 | } 196 | 197 | // Default value for this flag. It *must* be parseable by the value of the flag. 198 | func (f *FlagClause) Default(value string) *FlagClause { 199 | f.defaultValue = value 200 | return f 201 | } 202 | 203 | // DEPRECATED: Use Envar(name) instead. 204 | func (f *FlagClause) OverrideDefaultFromEnvar(envar string) *FlagClause { 205 | return f.Envar(envar) 206 | } 207 | 208 | // Envar overrides the default value for a flag from an environment variable, 209 | // if it is set. 210 | func (f *FlagClause) Envar(name string) *FlagClause { 211 | f.envar = name 212 | f.noEnvar = false 213 | return f 214 | } 215 | 216 | // NoEnvar forces environment variable defaults to be disabled for this flag. 217 | // Most useful in conjunction with app.DefaultEnvars(). 218 | func (f *FlagClause) NoEnvar() *FlagClause { 219 | f.envar = "" 220 | f.noEnvar = true 221 | return f 222 | } 223 | 224 | // PlaceHolder sets the place-holder string used for flag values in the help. The 225 | // default behaviour is to use the value provided by Default() if provided, 226 | // then fall back on the capitalized flag name. 227 | func (f *FlagClause) PlaceHolder(placeholder string) *FlagClause { 228 | f.placeholder = placeholder 229 | return f 230 | } 231 | 232 | // Hidden hides a flag from usage but still allows it to be used. 233 | func (f *FlagClause) Hidden() *FlagClause { 234 | f.hidden = true 235 | return f 236 | } 237 | 238 | // Required makes the flag required. You can not provide a Default() value to a Required() flag. 239 | func (f *FlagClause) Required() *FlagClause { 240 | f.required = true 241 | return f 242 | } 243 | 244 | // Short sets the short flag name. 245 | func (f *FlagClause) Short(name byte) *FlagClause { 246 | f.shorthand = name 247 | return f 248 | } 249 | 250 | // Bool makes this flag a boolean flag. 251 | func (f *FlagClause) Bool() (target *bool) { 252 | target = new(bool) 253 | f.SetValue(newBoolValue(target)) 254 | return 255 | } 256 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | // Default usage template. 4 | var DefaultUsageTemplate = `{{define "FormatCommand"}}\ 5 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 6 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 7 | {{end}}\ 8 | 9 | {{define "FormatCommands"}}\ 10 | {{range .FlattenedCommands}}\ 11 | {{if not .Hidden}}\ 12 | {{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}} 13 | {{.Help|Wrap 4}} 14 | {{end}}\ 15 | {{end}}\ 16 | {{end}}\ 17 | 18 | {{define "FormatUsage"}}\ 19 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 20 | {{if .Help}} 21 | {{.Help|Wrap 0}}\ 22 | {{end}}\ 23 | 24 | {{end}}\ 25 | 26 | {{if .Context.SelectedCommand}}\ 27 | usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}} 28 | {{else}}\ 29 | usage: {{.App.Name}}{{template "FormatUsage" .App}} 30 | {{end}}\ 31 | {{if .Context.Flags}}\ 32 | Flags: 33 | {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}} 34 | {{end}}\ 35 | {{if .Context.Args}}\ 36 | Args: 37 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 38 | {{end}}\ 39 | {{if .Context.SelectedCommand}}\ 40 | Subcommands: 41 | {{if .Context.SelectedCommand.Commands}}\ 42 | {{template "FormatCommands" .Context.SelectedCommand}} 43 | {{end}}\ 44 | {{else if .App.Commands}}\ 45 | Commands: 46 | {{template "FormatCommands" .App}} 47 | {{end}}\ 48 | ` 49 | 50 | // Usage template where command's optional flags are listed separately 51 | var SeparateOptionalFlagsUsageTemplate = `{{define "FormatCommand"}}\ 52 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 53 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 54 | {{end}}\ 55 | 56 | {{define "FormatCommands"}}\ 57 | {{range .FlattenedCommands}}\ 58 | {{if not .Hidden}}\ 59 | {{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}} 60 | {{.Help|Wrap 4}} 61 | {{end}}\ 62 | {{end}}\ 63 | {{end}}\ 64 | 65 | {{define "FormatUsage"}}\ 66 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 67 | {{if .Help}} 68 | {{.Help|Wrap 0}}\ 69 | {{end}}\ 70 | 71 | {{end}}\ 72 | {{if .Context.SelectedCommand}}\ 73 | usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}} 74 | {{else}}\ 75 | usage: {{.App.Name}}{{template "FormatUsage" .App}} 76 | {{end}}\ 77 | 78 | {{if .Context.Flags|RequiredFlags}}\ 79 | Required flags: 80 | {{.Context.Flags|RequiredFlags|FlagsToTwoColumns|FormatTwoColumns}} 81 | {{end}}\ 82 | {{if .Context.Flags|OptionalFlags}}\ 83 | Optional flags: 84 | {{.Context.Flags|OptionalFlags|FlagsToTwoColumns|FormatTwoColumns}} 85 | {{end}}\ 86 | {{if .Context.Args}}\ 87 | Args: 88 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 89 | {{end}}\ 90 | {{if .Context.SelectedCommand}}\ 91 | Subcommands: 92 | {{if .Context.SelectedCommand.Commands}}\ 93 | {{template "FormatCommands" .Context.SelectedCommand}} 94 | {{end}}\ 95 | {{else if .App.Commands}}\ 96 | Commands: 97 | {{template "FormatCommands" .App}} 98 | {{end}}\ 99 | ` 100 | 101 | // Usage template with compactly formatted commands. 102 | var CompactUsageTemplate = `{{define "FormatCommand"}}\ 103 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 104 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 105 | {{end}}\ 106 | 107 | {{define "FormatCommandList"}}\ 108 | {{range .}}\ 109 | {{if not .Hidden}}\ 110 | {{.Depth|Indent}}{{.Name}}{{if .Default}}*{{end}}{{template "FormatCommand" .}} 111 | {{end}}\ 112 | {{template "FormatCommandList" .Commands}}\ 113 | {{end}}\ 114 | {{end}}\ 115 | 116 | {{define "FormatUsage"}}\ 117 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 118 | {{if .Help}} 119 | {{.Help|Wrap 0}}\ 120 | {{end}}\ 121 | 122 | {{end}}\ 123 | 124 | {{if .Context.SelectedCommand}}\ 125 | usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}} 126 | {{else}}\ 127 | usage: {{.App.Name}}{{template "FormatUsage" .App}} 128 | {{end}}\ 129 | {{if .Context.Flags}}\ 130 | Flags: 131 | {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}} 132 | {{end}}\ 133 | {{if .Context.Args}}\ 134 | Args: 135 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 136 | {{end}}\ 137 | {{if .Context.SelectedCommand}}\ 138 | {{if .Context.SelectedCommand.Commands}}\ 139 | Commands: 140 | {{.Context.SelectedCommand}} 141 | {{template "FormatCommandList" .Context.SelectedCommand.Commands}} 142 | {{end}}\ 143 | {{else if .App.Commands}}\ 144 | Commands: 145 | {{template "FormatCommandList" .App.Commands}} 146 | {{end}}\ 147 | ` 148 | 149 | var ManPageTemplate = `{{define "FormatFlags"}}\ 150 | {{range .Flags}}\ 151 | {{if not .Hidden}}\ 152 | .TP 153 | \fB{{if .Short}}-{{.Short|Char}}, {{end}}--{{.Name}}{{if not .IsBoolFlag}}={{.FormatPlaceHolder}}{{end}}\\fR 154 | {{.Help}} 155 | {{end}}\ 156 | {{end}}\ 157 | {{end}}\ 158 | 159 | {{define "FormatCommand"}}\ 160 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 161 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}{{if .Default}}*{{end}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 162 | {{end}}\ 163 | 164 | {{define "FormatCommands"}}\ 165 | {{range .FlattenedCommands}}\ 166 | {{if not .Hidden}}\ 167 | .SS 168 | \fB{{.FullCommand}}{{template "FormatCommand" .}}\\fR 169 | .PP 170 | {{.Help}} 171 | {{template "FormatFlags" .}}\ 172 | {{end}}\ 173 | {{end}}\ 174 | {{end}}\ 175 | 176 | {{define "FormatUsage"}}\ 177 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}}\\fR 178 | {{end}}\ 179 | 180 | .TH {{.App.Name}} 1 {{.App.Version}} "{{.App.Author}}" 181 | .SH "NAME" 182 | {{.App.Name}} 183 | .SH "SYNOPSIS" 184 | .TP 185 | \fB{{.App.Name}}{{template "FormatUsage" .App}} 186 | .SH "DESCRIPTION" 187 | {{.App.Help}} 188 | .SH "OPTIONS" 189 | {{template "FormatFlags" .App}}\ 190 | {{if .App.Commands}}\ 191 | .SH "COMMANDS" 192 | {{template "FormatCommands" .App}}\ 193 | {{end}}\ 194 | ` 195 | 196 | // Default usage template. 197 | var LongHelpTemplate = `{{define "FormatCommand"}}\ 198 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 199 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 200 | {{end}}\ 201 | 202 | {{define "FormatCommands"}}\ 203 | {{range .FlattenedCommands}}\ 204 | {{if not .Hidden}}\ 205 | {{.FullCommand}}{{template "FormatCommand" .}} 206 | {{.Help|Wrap 4}} 207 | {{with .Flags|FlagsToTwoColumns}}{{FormatTwoColumnsWithIndent . 4 2}}{{end}} 208 | {{end}}\ 209 | {{end}}\ 210 | {{end}}\ 211 | 212 | {{define "FormatUsage"}}\ 213 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 214 | {{if .Help}} 215 | {{.Help|Wrap 0}}\ 216 | {{end}}\ 217 | 218 | {{end}}\ 219 | 220 | usage: {{.App.Name}}{{template "FormatUsage" .App}} 221 | {{if .Context.Flags}}\ 222 | Flags: 223 | {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}} 224 | {{end}}\ 225 | {{if .Context.Args}}\ 226 | Args: 227 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 228 | {{end}}\ 229 | {{if .App.Commands}}\ 230 | Commands: 231 | {{template "FormatCommands" .App}} 232 | {{end}}\ 233 | ` 234 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type TokenType int 11 | 12 | // Token types. 13 | const ( 14 | TokenShort TokenType = iota 15 | TokenLong 16 | TokenArg 17 | TokenError 18 | TokenEOL 19 | ) 20 | 21 | func (t TokenType) String() string { 22 | switch t { 23 | case TokenShort: 24 | return "short flag" 25 | case TokenLong: 26 | return "long flag" 27 | case TokenArg: 28 | return "argument" 29 | case TokenError: 30 | return "error" 31 | case TokenEOL: 32 | return "" 33 | } 34 | return "?" 35 | } 36 | 37 | var ( 38 | TokenEOLMarker = Token{-1, TokenEOL, ""} 39 | ) 40 | 41 | type Token struct { 42 | Index int 43 | Type TokenType 44 | Value string 45 | } 46 | 47 | func (t *Token) Equal(o *Token) bool { 48 | return t.Index == o.Index 49 | } 50 | 51 | func (t *Token) IsFlag() bool { 52 | return t.Type == TokenShort || t.Type == TokenLong 53 | } 54 | 55 | func (t *Token) IsEOF() bool { 56 | return t.Type == TokenEOL 57 | } 58 | 59 | func (t *Token) String() string { 60 | switch t.Type { 61 | case TokenShort: 62 | return "-" + t.Value 63 | case TokenLong: 64 | return "--" + t.Value 65 | case TokenArg: 66 | return t.Value 67 | case TokenError: 68 | return "error: " + t.Value 69 | case TokenEOL: 70 | return "" 71 | default: 72 | panic("unhandled type") 73 | } 74 | } 75 | 76 | // A union of possible elements in a parse stack. 77 | type ParseElement struct { 78 | // Clause is either *CmdClause, *ArgClause or *FlagClause. 79 | Clause interface{} 80 | // Value is corresponding value for an ArgClause or FlagClause (if any). 81 | Value *string 82 | } 83 | 84 | // ParseContext holds the current context of the parser. When passed to 85 | // Action() callbacks Elements will be fully populated with *FlagClause, 86 | // *ArgClause and *CmdClause values and their corresponding arguments (if 87 | // any). 88 | type ParseContext struct { 89 | SelectedCommand *CmdClause 90 | ignoreDefault bool 91 | argsOnly bool 92 | peek []*Token 93 | argi int // Index of current command-line arg we're processing. 94 | args []string 95 | flags *flagGroup 96 | arguments *argGroup 97 | argumenti int // Cursor into arguments 98 | // Flags, arguments and commands encountered and collected during parse. 99 | Elements []*ParseElement 100 | } 101 | 102 | func (p *ParseContext) nextArg() *ArgClause { 103 | if p.argumenti >= len(p.arguments.args) { 104 | return nil 105 | } 106 | arg := p.arguments.args[p.argumenti] 107 | if !arg.consumesRemainder() { 108 | p.argumenti++ 109 | } 110 | return arg 111 | } 112 | 113 | func (p *ParseContext) next() { 114 | p.argi++ 115 | p.args = p.args[1:] 116 | } 117 | 118 | // HasTrailingArgs returns true if there are unparsed command-line arguments. 119 | // This can occur if the parser can not match remaining arguments. 120 | func (p *ParseContext) HasTrailingArgs() bool { 121 | return len(p.args) > 0 122 | } 123 | 124 | func tokenize(args []string, ignoreDefault bool) *ParseContext { 125 | return &ParseContext{ 126 | ignoreDefault: ignoreDefault, 127 | args: args, 128 | flags: newFlagGroup(), 129 | arguments: newArgGroup(), 130 | } 131 | } 132 | 133 | func (p *ParseContext) mergeFlags(flags *flagGroup) { 134 | for _, flag := range flags.flagOrder { 135 | if flag.shorthand != 0 { 136 | p.flags.short[string(flag.shorthand)] = flag 137 | } 138 | p.flags.long[flag.name] = flag 139 | p.flags.flagOrder = append(p.flags.flagOrder, flag) 140 | } 141 | } 142 | 143 | func (p *ParseContext) mergeArgs(args *argGroup) { 144 | for _, arg := range args.args { 145 | p.arguments.args = append(p.arguments.args, arg) 146 | } 147 | } 148 | 149 | func (p *ParseContext) EOL() bool { 150 | return p.Peek().Type == TokenEOL 151 | } 152 | 153 | // Next token in the parse context. 154 | func (p *ParseContext) Next() *Token { 155 | if len(p.peek) > 0 { 156 | return p.pop() 157 | } 158 | 159 | // End of tokens. 160 | if len(p.args) == 0 { 161 | return &Token{Index: p.argi, Type: TokenEOL} 162 | } 163 | 164 | arg := p.args[0] 165 | p.next() 166 | 167 | if p.argsOnly { 168 | return &Token{p.argi, TokenArg, arg} 169 | } 170 | 171 | // All remaining args are passed directly. 172 | if arg == "--" { 173 | p.argsOnly = true 174 | return p.Next() 175 | } 176 | 177 | if strings.HasPrefix(arg, "--") { 178 | parts := strings.SplitN(arg[2:], "=", 2) 179 | token := &Token{p.argi, TokenLong, parts[0]} 180 | if len(parts) == 2 { 181 | p.Push(&Token{p.argi, TokenArg, parts[1]}) 182 | } 183 | return token 184 | } 185 | 186 | if strings.HasPrefix(arg, "-") { 187 | if len(arg) == 1 { 188 | return &Token{Index: p.argi, Type: TokenShort} 189 | } 190 | short := arg[1:2] 191 | flag, ok := p.flags.short[short] 192 | // Not a known short flag, we'll just return it anyway. 193 | if !ok { 194 | } else if fb, ok := flag.value.(boolFlag); ok && fb.IsBoolFlag() { 195 | // Bool short flag. 196 | } else { 197 | // Short flag with combined argument: -fARG 198 | token := &Token{p.argi, TokenShort, short} 199 | if len(arg) > 2 { 200 | p.Push(&Token{p.argi, TokenArg, arg[2:]}) 201 | } 202 | return token 203 | } 204 | 205 | if len(arg) > 2 { 206 | p.args = append([]string{"-" + arg[2:]}, p.args...) 207 | } 208 | return &Token{p.argi, TokenShort, short} 209 | } else if strings.HasPrefix(arg, "@") { 210 | expanded, err := ExpandArgsFromFile(arg[1:]) 211 | if err != nil { 212 | return &Token{p.argi, TokenError, err.Error()} 213 | } 214 | if p.argi >= len(p.args) { 215 | p.args = append(p.args[:p.argi-1], expanded...) 216 | } else { 217 | p.args = append(p.args[:p.argi-1], append(expanded, p.args[p.argi+1:]...)...) 218 | } 219 | return p.Next() 220 | } 221 | 222 | return &Token{p.argi, TokenArg, arg} 223 | } 224 | 225 | func (p *ParseContext) Peek() *Token { 226 | if len(p.peek) == 0 { 227 | return p.Push(p.Next()) 228 | } 229 | return p.peek[len(p.peek)-1] 230 | } 231 | 232 | func (p *ParseContext) Push(token *Token) *Token { 233 | p.peek = append(p.peek, token) 234 | return token 235 | } 236 | 237 | func (p *ParseContext) pop() *Token { 238 | end := len(p.peek) - 1 239 | token := p.peek[end] 240 | p.peek = p.peek[0:end] 241 | return token 242 | } 243 | 244 | func (p *ParseContext) String() string { 245 | return p.SelectedCommand.FullCommand() 246 | } 247 | 248 | func (p *ParseContext) matchedFlag(flag *FlagClause, value string) { 249 | p.Elements = append(p.Elements, &ParseElement{Clause: flag, Value: &value}) 250 | } 251 | 252 | func (p *ParseContext) matchedArg(arg *ArgClause, value string) { 253 | p.Elements = append(p.Elements, &ParseElement{Clause: arg, Value: &value}) 254 | } 255 | 256 | func (p *ParseContext) matchedCmd(cmd *CmdClause) { 257 | p.Elements = append(p.Elements, &ParseElement{Clause: cmd}) 258 | p.mergeFlags(cmd.flagGroup) 259 | p.mergeArgs(cmd.argGroup) 260 | p.SelectedCommand = cmd 261 | } 262 | 263 | // Expand arguments from a file. Lines starting with # will be treated as comments. 264 | func ExpandArgsFromFile(filename string) (out []string, err error) { 265 | r, err := os.Open(filename) 266 | if err != nil { 267 | return nil, err 268 | } 269 | defer r.Close() 270 | scanner := bufio.NewScanner(r) 271 | for scanner.Scan() { 272 | line := scanner.Text() 273 | if strings.HasPrefix(line, "#") { 274 | continue 275 | } 276 | out = append(out, line) 277 | } 278 | err = scanner.Err() 279 | return 280 | } 281 | 282 | func parse(context *ParseContext, app *Application) (err error) { 283 | context.mergeFlags(app.flagGroup) 284 | context.mergeArgs(app.argGroup) 285 | 286 | cmds := app.cmdGroup 287 | ignoreDefault := context.ignoreDefault 288 | 289 | loop: 290 | for !context.EOL() { 291 | token := context.Peek() 292 | 293 | switch token.Type { 294 | case TokenLong, TokenShort: 295 | if flag, err := context.flags.parse(context); err != nil { 296 | if !ignoreDefault { 297 | if cmd := cmds.defaultSubcommand(); cmd != nil { 298 | context.matchedCmd(cmd) 299 | cmds = cmd.cmdGroup 300 | break 301 | } 302 | } 303 | return err 304 | } else if flag == HelpFlag { 305 | ignoreDefault = true 306 | } 307 | 308 | case TokenArg: 309 | if cmds.have() { 310 | selectedDefault := false 311 | cmd, ok := cmds.commands[token.String()] 312 | if !ok { 313 | if !ignoreDefault { 314 | if cmd = cmds.defaultSubcommand(); cmd != nil { 315 | selectedDefault = true 316 | } 317 | } 318 | if cmd == nil { 319 | return fmt.Errorf("expected command but got %q", token) 320 | } 321 | } 322 | if cmd == HelpCommand { 323 | ignoreDefault = true 324 | } 325 | context.matchedCmd(cmd) 326 | cmds = cmd.cmdGroup 327 | if !selectedDefault { 328 | context.Next() 329 | } 330 | } else if context.arguments.have() { 331 | if app.noInterspersed { 332 | // no more flags 333 | context.argsOnly = true 334 | } 335 | arg := context.nextArg() 336 | if arg == nil { 337 | break loop 338 | } 339 | context.matchedArg(arg, token.String()) 340 | context.Next() 341 | } else { 342 | break loop 343 | } 344 | 345 | case TokenEOL: 346 | break loop 347 | } 348 | } 349 | 350 | // Move to innermost default command. 351 | for !ignoreDefault { 352 | if cmd := cmds.defaultSubcommand(); cmd != nil { 353 | context.matchedCmd(cmd) 354 | cmds = cmd.cmdGroup 355 | } else { 356 | break 357 | } 358 | } 359 | 360 | if !context.EOL() { 361 | return fmt.Errorf("unexpected %s", context.Peek()) 362 | } 363 | 364 | // Set defaults for all remaining args. 365 | for arg := context.nextArg(); arg != nil && !arg.consumesRemainder(); arg = context.nextArg() { 366 | if arg.defaultValue != "" { 367 | if err := arg.value.Set(arg.defaultValue); err != nil { 368 | return fmt.Errorf("invalid default value '%s' for argument '%s'", arg.defaultValue, arg.name) 369 | } 370 | } 371 | } 372 | 373 | return 374 | } 375 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | //go:generate go run ./cmd/genvalues/main.go 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "net/url" 9 | "os" 10 | "reflect" 11 | "regexp" 12 | "strings" 13 | "time" 14 | 15 | "github.com/alecthomas/units" 16 | ) 17 | 18 | // NOTE: Most of the base type values were lifted from: 19 | // http://golang.org/src/pkg/flag/flag.go?s=20146:20222 20 | 21 | // Value is the interface to the dynamic value stored in a flag. 22 | // (The default value is represented as a string.) 23 | // 24 | // If a Value has an IsBoolFlag() bool method returning true, the command-line 25 | // parser makes --name equivalent to -name=true rather than using the next 26 | // command-line argument, and adds a --no-name counterpart for negating the 27 | // flag. 28 | type Value interface { 29 | String() string 30 | Set(string) error 31 | } 32 | 33 | // Getter is an interface that allows the contents of a Value to be retrieved. 34 | // It wraps the Value interface, rather than being part of it, because it 35 | // appeared after Go 1 and its compatibility rules. All Value types provided 36 | // by this package satisfy the Getter interface. 37 | type Getter interface { 38 | Value 39 | Get() interface{} 40 | } 41 | 42 | // Optional interface to indicate boolean flags that don't accept a value, and 43 | // implicitly have a --no- negation counterpart. 44 | type boolFlag interface { 45 | Value 46 | IsBoolFlag() bool 47 | } 48 | 49 | // Optional interface for arguments that cumulatively consume all remaining 50 | // input. 51 | type remainderArg interface { 52 | Value 53 | IsCumulative() bool 54 | } 55 | 56 | type accumulator struct { 57 | element func(value interface{}) Value 58 | typ reflect.Type 59 | slice reflect.Value 60 | } 61 | 62 | // Use reflection to accumulate values into a slice. 63 | // 64 | // target := []string{} 65 | // newAccumulator(&target, func (value interface{}) Value { 66 | // return newStringValue(value.(*string)) 67 | // }) 68 | func newAccumulator(slice interface{}, element func(value interface{}) Value) *accumulator { 69 | typ := reflect.TypeOf(slice) 70 | if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Slice { 71 | panic("expected a pointer to a slice") 72 | } 73 | return &accumulator{ 74 | element: element, 75 | typ: typ.Elem().Elem(), 76 | slice: reflect.ValueOf(slice), 77 | } 78 | } 79 | 80 | func (a *accumulator) String() string { 81 | out := []string{} 82 | s := a.slice.Elem() 83 | for i := 0; i < s.Len(); i++ { 84 | out = append(out, a.element(s.Index(i).Addr().Interface()).String()) 85 | } 86 | return strings.Join(out, ",") 87 | } 88 | 89 | func (a *accumulator) Set(value string) error { 90 | e := reflect.New(a.typ) 91 | if err := a.element(e.Interface()).Set(value); err != nil { 92 | return err 93 | } 94 | slice := reflect.Append(a.slice.Elem(), e.Elem()) 95 | a.slice.Elem().Set(slice) 96 | return nil 97 | } 98 | 99 | func (a *accumulator) Get() interface{} { 100 | return a.slice.Interface() 101 | } 102 | 103 | func (a *accumulator) IsCumulative() bool { 104 | return true 105 | } 106 | 107 | func (b *boolValue) IsBoolFlag() bool { return true } 108 | 109 | // -- time.Duration Value 110 | type durationValue time.Duration 111 | 112 | func newDurationValue(p *time.Duration) *durationValue { 113 | return (*durationValue)(p) 114 | } 115 | 116 | func (d *durationValue) Set(s string) error { 117 | v, err := time.ParseDuration(s) 118 | *d = durationValue(v) 119 | return err 120 | } 121 | 122 | func (d *durationValue) Get() interface{} { return time.Duration(*d) } 123 | 124 | func (d *durationValue) String() string { return (*time.Duration)(d).String() } 125 | 126 | // -- map[string]string Value 127 | type stringMapValue map[string]string 128 | 129 | func newStringMapValue(p *map[string]string) *stringMapValue { 130 | return (*stringMapValue)(p) 131 | } 132 | 133 | var stringMapRegex = regexp.MustCompile("[:=]") 134 | 135 | func (s *stringMapValue) Set(value string) error { 136 | parts := stringMapRegex.Split(value, 2) 137 | if len(parts) != 2 { 138 | return fmt.Errorf("expected KEY=VALUE got '%s'", value) 139 | } 140 | (*s)[parts[0]] = parts[1] 141 | return nil 142 | } 143 | 144 | func (s *stringMapValue) Get() interface{} { 145 | return (map[string]string)(*s) 146 | } 147 | 148 | func (s *stringMapValue) String() string { 149 | return fmt.Sprintf("%s", map[string]string(*s)) 150 | } 151 | 152 | func (s *stringMapValue) IsCumulative() bool { 153 | return true 154 | } 155 | 156 | // -- net.IP Value 157 | type ipValue net.IP 158 | 159 | func newIPValue(p *net.IP) *ipValue { 160 | return (*ipValue)(p) 161 | } 162 | 163 | func (i *ipValue) Set(value string) error { 164 | if ip := net.ParseIP(value); ip == nil { 165 | return fmt.Errorf("'%s' is not an IP address", value) 166 | } else { 167 | *i = *(*ipValue)(&ip) 168 | return nil 169 | } 170 | } 171 | 172 | func (i *ipValue) Get() interface{} { 173 | return (net.IP)(*i) 174 | } 175 | 176 | func (i *ipValue) String() string { 177 | return (*net.IP)(i).String() 178 | } 179 | 180 | // -- *net.TCPAddr Value 181 | type tcpAddrValue struct { 182 | addr **net.TCPAddr 183 | } 184 | 185 | func newTCPAddrValue(p **net.TCPAddr) *tcpAddrValue { 186 | return &tcpAddrValue{p} 187 | } 188 | 189 | func (i *tcpAddrValue) Set(value string) error { 190 | if addr, err := net.ResolveTCPAddr("tcp", value); err != nil { 191 | return fmt.Errorf("'%s' is not a valid TCP address: %s", value, err) 192 | } else { 193 | *i.addr = addr 194 | return nil 195 | } 196 | } 197 | 198 | func (t *tcpAddrValue) Get() interface{} { 199 | return (*net.TCPAddr)(*t.addr) 200 | } 201 | 202 | func (i *tcpAddrValue) String() string { 203 | return (*i.addr).String() 204 | } 205 | 206 | // -- existingFile Value 207 | 208 | type fileStatValue struct { 209 | path *string 210 | predicate func(os.FileInfo) error 211 | } 212 | 213 | func newFileStatValue(p *string, predicate func(os.FileInfo) error) *fileStatValue { 214 | return &fileStatValue{ 215 | path: p, 216 | predicate: predicate, 217 | } 218 | } 219 | 220 | func (e *fileStatValue) Set(value string) error { 221 | if s, err := os.Stat(value); os.IsNotExist(err) { 222 | return fmt.Errorf("path '%s' does not exist", value) 223 | } else if err != nil { 224 | return err 225 | } else if err := e.predicate(s); err != nil { 226 | return err 227 | } 228 | *e.path = value 229 | return nil 230 | } 231 | 232 | func (f *fileStatValue) Get() interface{} { 233 | return (string)(*f.path) 234 | } 235 | 236 | func (e *fileStatValue) String() string { 237 | return *e.path 238 | } 239 | 240 | // -- os.File value 241 | 242 | type fileValue struct { 243 | f **os.File 244 | flag int 245 | perm os.FileMode 246 | } 247 | 248 | func newFileValue(p **os.File, flag int, perm os.FileMode) *fileValue { 249 | return &fileValue{p, flag, perm} 250 | } 251 | 252 | func (f *fileValue) Set(value string) error { 253 | if fd, err := os.OpenFile(value, f.flag, f.perm); err != nil { 254 | return err 255 | } else { 256 | *f.f = fd 257 | return nil 258 | } 259 | } 260 | 261 | func (f *fileValue) Get() interface{} { 262 | return (*os.File)(*f.f) 263 | } 264 | 265 | func (f *fileValue) String() string { 266 | if *f.f == nil { 267 | return "" 268 | } 269 | return (*f.f).Name() 270 | } 271 | 272 | // -- url.URL Value 273 | type urlValue struct { 274 | u **url.URL 275 | } 276 | 277 | func newURLValue(p **url.URL) *urlValue { 278 | return &urlValue{p} 279 | } 280 | 281 | func (u *urlValue) Set(value string) error { 282 | if url, err := url.Parse(value); err != nil { 283 | return fmt.Errorf("invalid URL: %s", err) 284 | } else { 285 | *u.u = url 286 | return nil 287 | } 288 | } 289 | 290 | func (u *urlValue) Get() interface{} { 291 | return (*url.URL)(*u.u) 292 | } 293 | 294 | func (u *urlValue) String() string { 295 | if *u.u == nil { 296 | return "" 297 | } 298 | return (*u.u).String() 299 | } 300 | 301 | // -- []*url.URL Value 302 | type urlListValue []*url.URL 303 | 304 | func newURLListValue(p *[]*url.URL) *urlListValue { 305 | return (*urlListValue)(p) 306 | } 307 | 308 | func (u *urlListValue) Set(value string) error { 309 | if url, err := url.Parse(value); err != nil { 310 | return fmt.Errorf("invalid URL: %s", err) 311 | } else { 312 | *u = append(*u, url) 313 | return nil 314 | } 315 | } 316 | 317 | func (u *urlListValue) Get() interface{} { 318 | return ([]*url.URL)(*u) 319 | } 320 | 321 | func (u *urlListValue) String() string { 322 | out := []string{} 323 | for _, url := range *u { 324 | out = append(out, url.String()) 325 | } 326 | return strings.Join(out, ",") 327 | } 328 | 329 | // A flag whose value must be in a set of options. 330 | type enumValue struct { 331 | value *string 332 | options []string 333 | } 334 | 335 | func newEnumFlag(target *string, options ...string) *enumValue { 336 | return &enumValue{ 337 | value: target, 338 | options: options, 339 | } 340 | } 341 | 342 | func (a *enumValue) String() string { 343 | return *a.value 344 | } 345 | 346 | func (a *enumValue) Set(value string) error { 347 | for _, v := range a.options { 348 | if v == value { 349 | *a.value = value 350 | return nil 351 | } 352 | } 353 | return fmt.Errorf("enum value must be one of %s, got '%s'", strings.Join(a.options, ","), value) 354 | } 355 | 356 | func (e *enumValue) Get() interface{} { 357 | return (string)(*e.value) 358 | } 359 | 360 | // -- []string Enum Value 361 | type enumsValue struct { 362 | value *[]string 363 | options []string 364 | } 365 | 366 | func newEnumsFlag(target *[]string, options ...string) *enumsValue { 367 | return &enumsValue{ 368 | value: target, 369 | options: options, 370 | } 371 | } 372 | 373 | func (s *enumsValue) Set(value string) error { 374 | for _, v := range s.options { 375 | if v == value { 376 | *s.value = append(*s.value, value) 377 | return nil 378 | } 379 | } 380 | return fmt.Errorf("enum value must be one of %s, got '%s'", strings.Join(s.options, ","), value) 381 | } 382 | 383 | func (e *enumsValue) Get() interface{} { 384 | return ([]string)(*e.value) 385 | } 386 | 387 | func (s *enumsValue) String() string { 388 | return strings.Join(*s.value, ",") 389 | } 390 | 391 | func (s *enumsValue) IsCumulative() bool { 392 | return true 393 | } 394 | 395 | // -- units.Base2Bytes Value 396 | type bytesValue units.Base2Bytes 397 | 398 | func newBytesValue(p *units.Base2Bytes) *bytesValue { 399 | return (*bytesValue)(p) 400 | } 401 | 402 | func (d *bytesValue) Set(s string) error { 403 | v, err := units.ParseBase2Bytes(s) 404 | *d = bytesValue(v) 405 | return err 406 | } 407 | 408 | func (d *bytesValue) Get() interface{} { return units.Base2Bytes(*d) } 409 | 410 | func (d *bytesValue) String() string { return (*units.Base2Bytes)(d).String() } 411 | 412 | func newExistingFileValue(target *string) *fileStatValue { 413 | return newFileStatValue(target, func(s os.FileInfo) error { 414 | if s.IsDir() { 415 | return fmt.Errorf("'%s' is a directory", s.Name()) 416 | } 417 | return nil 418 | }) 419 | } 420 | 421 | func newExistingDirValue(target *string) *fileStatValue { 422 | return newFileStatValue(target, func(s os.FileInfo) error { 423 | if !s.IsDir() { 424 | return fmt.Errorf("'%s' is a file", s.Name()) 425 | } 426 | return nil 427 | }) 428 | } 429 | 430 | func newExistingFileOrDirValue(target *string) *fileStatValue { 431 | return newFileStatValue(target, func(s os.FileInfo) error { return nil }) 432 | } 433 | 434 | type counterValue int 435 | 436 | func newCounterValue(n *int) *counterValue { 437 | return (*counterValue)(n) 438 | } 439 | 440 | func (c *counterValue) Set(s string) error { 441 | *c++ 442 | return nil 443 | } 444 | 445 | func (c *counterValue) Get() interface{} { return (int)(*c) } 446 | func (c *counterValue) IsBoolFlag() bool { return true } 447 | func (c *counterValue) String() string { return fmt.Sprintf("%d", *c) } 448 | 449 | // -- regexp.Regexp value 450 | type regexpValue struct { 451 | r **regexp.Regexp 452 | } 453 | 454 | func newRegexpValue(r **regexp.Regexp) *regexpValue { 455 | return ®expValue{r} 456 | } 457 | 458 | func (r *regexpValue) Set(value string) error { 459 | if re, err := regexp.Compile(value); err != nil { 460 | return err 461 | } else { 462 | *r.r = re 463 | return nil 464 | } 465 | } 466 | 467 | func (r *regexpValue) Get() interface{} { 468 | return (*regexp.Regexp)(*r.r) 469 | } 470 | 471 | func (r *regexpValue) String() string { 472 | if *r.r == nil { 473 | return "" 474 | } 475 | return (*r.r).String() 476 | } 477 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | ErrCommandNotSpecified = fmt.Errorf("command not specified") 13 | ) 14 | 15 | var ( 16 | envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z_]+`) 17 | ) 18 | 19 | type ApplicationValidator func(*Application) error 20 | 21 | // An Application contains the definitions of flags, arguments and commands 22 | // for an application. 23 | type Application struct { 24 | *flagGroup 25 | *argGroup 26 | *cmdGroup 27 | actionMixin 28 | initialized bool 29 | 30 | Name string 31 | Help string 32 | 33 | author string 34 | version string 35 | writer io.Writer // Destination for usage and errors. 36 | usageTemplate string 37 | validator ApplicationValidator 38 | terminate func(status int) // See Terminate() 39 | noInterspersed bool // can flags be interspersed with args (or must they come first) 40 | defaultEnvars bool 41 | 42 | // Help flag. Exposed for user customisation. 43 | HelpFlag *FlagClause 44 | // Help command. Exposed for user customisation. May be nil. 45 | HelpCommand *CmdClause 46 | // Version flag. Exposed for user customisation. May be nil. 47 | VersionFlag *FlagClause 48 | } 49 | 50 | // New creates a new Kingpin application instance. 51 | func New(name, help string) *Application { 52 | a := &Application{ 53 | flagGroup: newFlagGroup(), 54 | argGroup: newArgGroup(), 55 | Name: name, 56 | Help: help, 57 | writer: os.Stderr, 58 | usageTemplate: DefaultUsageTemplate, 59 | terminate: os.Exit, 60 | } 61 | a.cmdGroup = newCmdGroup(a) 62 | a.HelpFlag = a.Flag("help", "Show context-sensitive help (also try --help-long and --help-man).") 63 | a.HelpFlag.Bool() 64 | a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool() 65 | a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool() 66 | return a 67 | } 68 | 69 | func (a *Application) generateLongHelp(c *ParseContext) error { 70 | a.Writer(os.Stdout) 71 | if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil { 72 | return err 73 | } 74 | a.terminate(0) 75 | return nil 76 | } 77 | 78 | func (a *Application) generateManPage(c *ParseContext) error { 79 | a.Writer(os.Stdout) 80 | if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil { 81 | return err 82 | } 83 | a.terminate(0) 84 | return nil 85 | } 86 | 87 | // DefaultEnvars configures all flags (that do not already have an associated 88 | // envar) to use a default environment variable in the form "_". 89 | // 90 | // For example, if the application is named "foo" and a flag is named "bar- 91 | // waz" the environment variable: "FOO_BAR_WAZ". 92 | func (a *Application) DefaultEnvars() *Application { 93 | a.defaultEnvars = true 94 | return a 95 | } 96 | 97 | // Terminate specifies the termination handler. Defaults to os.Exit(status). 98 | // If nil is passed, a no-op function will be used. 99 | func (a *Application) Terminate(terminate func(int)) *Application { 100 | if terminate == nil { 101 | terminate = func(int) {} 102 | } 103 | a.terminate = terminate 104 | return a 105 | } 106 | 107 | // Specify the writer to use for usage and errors. Defaults to os.Stderr. 108 | func (a *Application) Writer(w io.Writer) *Application { 109 | a.writer = w 110 | return a 111 | } 112 | 113 | // UsageTemplate specifies the text template to use when displaying usage 114 | // information. The default is UsageTemplate. 115 | func (a *Application) UsageTemplate(template string) *Application { 116 | a.usageTemplate = template 117 | return a 118 | } 119 | 120 | // Validate sets a validation function to run when parsing. 121 | func (a *Application) Validate(validator ApplicationValidator) *Application { 122 | a.validator = validator 123 | return a 124 | } 125 | 126 | // ParseContext parses the given command line and returns the fully populated 127 | // ParseContext. 128 | func (a *Application) ParseContext(args []string) (*ParseContext, error) { 129 | return a.parseContext(false, args) 130 | } 131 | 132 | func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseContext, error) { 133 | if err := a.init(); err != nil { 134 | return nil, err 135 | } 136 | context := tokenize(args, ignoreDefault) 137 | err := parse(context, a) 138 | return context, err 139 | } 140 | 141 | // Parse parses command-line arguments. It returns the selected command and an 142 | // error. The selected command will be a space separated subcommand, if 143 | // subcommands have been configured. 144 | // 145 | // This will populate all flag and argument values, call all callbacks, and so 146 | // on. 147 | func (a *Application) Parse(args []string) (command string, err error) { 148 | context, err := a.ParseContext(args) 149 | if err != nil { 150 | return "", err 151 | } 152 | a.maybeHelp(context) 153 | if !context.EOL() { 154 | return "", fmt.Errorf("unexpected argument '%s'", context.Peek()) 155 | } 156 | command, err = a.execute(context) 157 | if err == ErrCommandNotSpecified { 158 | a.writeUsage(context, nil) 159 | } 160 | return command, err 161 | } 162 | 163 | func (a *Application) writeUsage(context *ParseContext, err error) { 164 | if err != nil { 165 | a.Errorf("%s", err) 166 | } 167 | if err := a.UsageForContext(context); err != nil { 168 | panic(err) 169 | } 170 | a.terminate(1) 171 | } 172 | 173 | func (a *Application) maybeHelp(context *ParseContext) { 174 | for _, element := range context.Elements { 175 | if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag { 176 | a.writeUsage(context, nil) 177 | } 178 | } 179 | } 180 | 181 | // findCommandFromArgs finds a command (if any) from the given command line arguments. 182 | func (a *Application) findCommandFromArgs(args []string) (command string, err error) { 183 | if err := a.init(); err != nil { 184 | return "", err 185 | } 186 | context := tokenize(args, false) 187 | if _, err := a.parse(context); err != nil { 188 | return "", err 189 | } 190 | return a.findCommandFromContext(context), nil 191 | } 192 | 193 | // findCommandFromContext finds a command (if any) from a parsed context. 194 | func (a *Application) findCommandFromContext(context *ParseContext) string { 195 | commands := []string{} 196 | for _, element := range context.Elements { 197 | if c, ok := element.Clause.(*CmdClause); ok { 198 | commands = append(commands, c.name) 199 | } 200 | } 201 | return strings.Join(commands, " ") 202 | } 203 | 204 | // Version adds a --version flag for displaying the application version. 205 | func (a *Application) Version(version string) *Application { 206 | a.version = version 207 | a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error { 208 | fmt.Fprintln(a.writer, version) 209 | a.terminate(0) 210 | return nil 211 | }) 212 | a.VersionFlag.Bool() 213 | return a 214 | } 215 | 216 | func (a *Application) Author(author string) *Application { 217 | a.author = author 218 | return a 219 | } 220 | 221 | // Action callback to call when all values are populated and parsing is 222 | // complete, but before any command, flag or argument actions. 223 | // 224 | // All Action() callbacks are called in the order they are encountered on the 225 | // command line. 226 | func (a *Application) Action(action Action) *Application { 227 | a.addAction(action) 228 | return a 229 | } 230 | 231 | // Action called after parsing completes but before validation and execution. 232 | func (a *Application) PreAction(action Action) *Application { 233 | a.addPreAction(action) 234 | return a 235 | } 236 | 237 | // Command adds a new top-level command. 238 | func (a *Application) Command(name, help string) *CmdClause { 239 | return a.addCommand(name, help) 240 | } 241 | 242 | // Interspersed control if flags can be interspersed with positional arguments 243 | // 244 | // true (the default) means that they can, false means that all the flags must appear before the first positional arguments. 245 | func (a *Application) Interspersed(interspersed bool) *Application { 246 | a.noInterspersed = !interspersed 247 | return a 248 | } 249 | 250 | func (a *Application) defaultEnvarPrefix() string { 251 | if a.defaultEnvars { 252 | return a.Name 253 | } 254 | return "" 255 | } 256 | 257 | func (a *Application) init() error { 258 | if a.initialized { 259 | return nil 260 | } 261 | if a.cmdGroup.have() && a.argGroup.have() { 262 | return fmt.Errorf("can't mix top-level Arg()s with Command()s") 263 | } 264 | 265 | // If we have subcommands, add a help command at the top-level. 266 | if a.cmdGroup.have() { 267 | var command []string 268 | a.HelpCommand = a.Command("help", "Show help.").PreAction(func(context *ParseContext) error { 269 | a.Usage(command) 270 | a.terminate(0) 271 | return nil 272 | }) 273 | a.HelpCommand.Arg("command", "Show help on command.").StringsVar(&command) 274 | // Make help first command. 275 | l := len(a.commandOrder) 276 | a.commandOrder = append(a.commandOrder[l-1:l], a.commandOrder[:l-1]...) 277 | } 278 | 279 | if err := a.flagGroup.init(a.defaultEnvarPrefix()); err != nil { 280 | return err 281 | } 282 | if err := a.cmdGroup.init(); err != nil { 283 | return err 284 | } 285 | if err := a.argGroup.init(); err != nil { 286 | return err 287 | } 288 | for _, cmd := range a.commands { 289 | if err := cmd.init(); err != nil { 290 | return err 291 | } 292 | } 293 | flagGroups := []*flagGroup{a.flagGroup} 294 | for _, cmd := range a.commandOrder { 295 | if err := checkDuplicateFlags(cmd, flagGroups); err != nil { 296 | return err 297 | } 298 | } 299 | a.initialized = true 300 | return nil 301 | } 302 | 303 | // Recursively check commands for duplicate flags. 304 | func checkDuplicateFlags(current *CmdClause, flagGroups []*flagGroup) error { 305 | // Check for duplicates. 306 | for _, flags := range flagGroups { 307 | for _, flag := range current.flagOrder { 308 | if flag.shorthand != 0 { 309 | if _, ok := flags.short[string(flag.shorthand)]; ok { 310 | return fmt.Errorf("duplicate short flag -%c", flag.shorthand) 311 | } 312 | } 313 | if _, ok := flags.long[flag.name]; ok { 314 | return fmt.Errorf("duplicate long flag --%s", flag.name) 315 | } 316 | } 317 | } 318 | flagGroups = append(flagGroups, current.flagGroup) 319 | // Check subcommands. 320 | for _, subcmd := range current.commandOrder { 321 | if err := checkDuplicateFlags(subcmd, flagGroups); err != nil { 322 | return err 323 | } 324 | } 325 | return nil 326 | } 327 | 328 | func (a *Application) execute(context *ParseContext) (string, error) { 329 | var err error 330 | selected := []string{} 331 | 332 | if err = a.setDefaults(context); err != nil { 333 | return "", err 334 | } 335 | 336 | selected, err = a.setValues(context) 337 | if err != nil { 338 | return "", err 339 | } 340 | 341 | if err = a.applyPreActions(context); err != nil { 342 | return "", err 343 | } 344 | 345 | if err = a.validateRequired(context); err != nil { 346 | return "", err 347 | } 348 | 349 | if err = a.applyValidators(context); err != nil { 350 | return "", err 351 | } 352 | 353 | if err = a.applyActions(context); err != nil { 354 | return "", err 355 | } 356 | 357 | command := strings.Join(selected, " ") 358 | if command == "" && a.cmdGroup.have() { 359 | return "", ErrCommandNotSpecified 360 | } 361 | return command, err 362 | } 363 | 364 | func (a *Application) setDefaults(context *ParseContext) error { 365 | flagElements := map[string]*ParseElement{} 366 | for _, element := range context.Elements { 367 | if flag, ok := element.Clause.(*FlagClause); ok { 368 | flagElements[flag.name] = element 369 | } 370 | } 371 | 372 | argElements := map[string]*ParseElement{} 373 | for _, element := range context.Elements { 374 | if arg, ok := element.Clause.(*ArgClause); ok { 375 | argElements[arg.name] = element 376 | } 377 | } 378 | 379 | // Check required flags and set defaults. 380 | for _, flag := range context.flags.long { 381 | if flagElements[flag.name] == nil { 382 | // Set defaults, if any. 383 | if flag.defaultValue != "" { 384 | if err := flag.value.Set(flag.defaultValue); err != nil { 385 | return err 386 | } 387 | } 388 | } 389 | } 390 | 391 | for _, arg := range context.arguments.args { 392 | if argElements[arg.name] == nil { 393 | // Set defaults, if any. 394 | if arg.defaultValue != "" { 395 | if err := arg.value.Set(arg.defaultValue); err != nil { 396 | return err 397 | } 398 | } 399 | } 400 | } 401 | 402 | return nil 403 | } 404 | 405 | func (a *Application) validateRequired(context *ParseContext) error { 406 | flagElements := map[string]*ParseElement{} 407 | for _, element := range context.Elements { 408 | if flag, ok := element.Clause.(*FlagClause); ok { 409 | flagElements[flag.name] = element 410 | } 411 | } 412 | 413 | argElements := map[string]*ParseElement{} 414 | for _, element := range context.Elements { 415 | if arg, ok := element.Clause.(*ArgClause); ok { 416 | argElements[arg.name] = element 417 | } 418 | } 419 | 420 | // Check required flags and set defaults. 421 | for _, flag := range context.flags.long { 422 | if flagElements[flag.name] == nil { 423 | // Check required flags were provided. 424 | if flag.needsValue() { 425 | return fmt.Errorf("required flag --%s not provided", flag.name) 426 | } 427 | } 428 | } 429 | 430 | for _, arg := range context.arguments.args { 431 | if argElements[arg.name] == nil { 432 | if arg.required { 433 | return fmt.Errorf("required argument '%s' not provided", arg.name) 434 | } 435 | } 436 | } 437 | return nil 438 | } 439 | 440 | func (a *Application) setValues(context *ParseContext) (selected []string, err error) { 441 | // Set all arg and flag values. 442 | var lastCmd *CmdClause 443 | for _, element := range context.Elements { 444 | switch clause := element.Clause.(type) { 445 | case *FlagClause: 446 | if err = clause.value.Set(*element.Value); err != nil { 447 | return 448 | } 449 | 450 | case *ArgClause: 451 | if err = clause.value.Set(*element.Value); err != nil { 452 | return 453 | } 454 | 455 | case *CmdClause: 456 | if clause.validator != nil { 457 | if err = clause.validator(clause); err != nil { 458 | return 459 | } 460 | } 461 | selected = append(selected, clause.name) 462 | lastCmd = clause 463 | } 464 | } 465 | 466 | if lastCmd != nil && len(lastCmd.commands) > 0 { 467 | return nil, fmt.Errorf("must select a subcommand of '%s'", lastCmd.FullCommand()) 468 | } 469 | 470 | return 471 | } 472 | 473 | func (a *Application) applyValidators(context *ParseContext) (err error) { 474 | // Call command validation functions. 475 | for _, element := range context.Elements { 476 | if cmd, ok := element.Clause.(*CmdClause); ok && cmd.validator != nil { 477 | if err = cmd.validator(cmd); err != nil { 478 | return err 479 | } 480 | } 481 | } 482 | 483 | if a.validator != nil { 484 | err = a.validator(a) 485 | } 486 | return err 487 | } 488 | 489 | func (a *Application) applyPreActions(context *ParseContext) error { 490 | if err := a.actionMixin.applyPreActions(context); err != nil { 491 | return err 492 | } 493 | // Dispatch to actions. 494 | for _, element := range context.Elements { 495 | if applier, ok := element.Clause.(actionApplier); ok { 496 | if err := applier.applyPreActions(context); err != nil { 497 | return err 498 | } 499 | } 500 | } 501 | return nil 502 | } 503 | 504 | func (a *Application) applyActions(context *ParseContext) error { 505 | if err := a.actionMixin.applyActions(context); err != nil { 506 | return err 507 | } 508 | // Dispatch to actions. 509 | for _, element := range context.Elements { 510 | if applier, ok := element.Clause.(actionApplier); ok { 511 | if err := applier.applyActions(context); err != nil { 512 | return err 513 | } 514 | } 515 | } 516 | return nil 517 | } 518 | 519 | // Errorf prints an error message to w in the format ": error: ". 520 | func (a *Application) Errorf(format string, args ...interface{}) { 521 | fmt.Fprintf(a.writer, a.Name+": error: "+format+"\n", args...) 522 | } 523 | 524 | // Fatalf writes a formatted error to w then terminates with exit status 1. 525 | func (a *Application) Fatalf(format string, args ...interface{}) { 526 | a.Errorf(format, args...) 527 | a.terminate(1) 528 | } 529 | 530 | // FatalUsage prints an error message followed by usage information, then 531 | // exits with a non-zero status. 532 | func (a *Application) FatalUsage(format string, args ...interface{}) { 533 | a.Errorf(format, args...) 534 | a.Usage([]string{}) 535 | a.terminate(1) 536 | } 537 | 538 | // FatalUsageContext writes a printf formatted error message to w, then usage 539 | // information for the given ParseContext, before exiting. 540 | func (a *Application) FatalUsageContext(context *ParseContext, format string, args ...interface{}) { 541 | a.Errorf(format, args...) 542 | if err := a.UsageForContext(context); err != nil { 543 | panic(err) 544 | } 545 | a.terminate(1) 546 | } 547 | 548 | // FatalIfError prints an error and exits if err is not nil. The error is printed 549 | // with the given formatted string, if any. 550 | func (a *Application) FatalIfError(err error, format string, args ...interface{}) { 551 | if err != nil { 552 | prefix := "" 553 | if format != "" { 554 | prefix = fmt.Sprintf(format, args...) + ": " 555 | } 556 | a.Errorf(prefix+"%s", err) 557 | a.terminate(1) 558 | } 559 | } 560 | 561 | func envarTransform(name string) string { 562 | return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_")) 563 | } 564 | -------------------------------------------------------------------------------- /values_generated.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "regexp" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // This file is autogenerated by "go generate .". Do not modify. 12 | 13 | // -- bool Value 14 | type boolValue bool 15 | 16 | func newBoolValue(p *bool) *boolValue { 17 | return (*boolValue)(p) 18 | } 19 | 20 | func (f *boolValue) Set(s string) error { 21 | v, err := strconv.ParseBool(s) 22 | *f = boolValue(v) 23 | return err 24 | } 25 | 26 | func (f *boolValue) Get() interface{} { return bool(*f) } 27 | 28 | func (f *boolValue) String() string { return fmt.Sprintf("%v", *f) } 29 | 30 | // Bool parses the next command-line value as bool. 31 | func (p *parserMixin) Bool() (target *bool) { 32 | target = new(bool) 33 | p.BoolVar(target) 34 | return 35 | } 36 | 37 | func (p *parserMixin) BoolVar(target *bool) { 38 | p.SetValue(newBoolValue(target)) 39 | } 40 | 41 | // BoolList accumulates bool values into a slice. 42 | func (p *parserMixin) BoolList() (target *[]bool) { 43 | target = new([]bool) 44 | p.BoolListVar(target) 45 | return 46 | } 47 | 48 | func (p *parserMixin) BoolListVar(target *[]bool) { 49 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newBoolValue(v.(*bool)) })) 50 | } 51 | 52 | // -- string Value 53 | type stringValue string 54 | 55 | func newStringValue(p *string) *stringValue { 56 | return (*stringValue)(p) 57 | } 58 | 59 | func (f *stringValue) Set(s string) error { 60 | v, err := s, error(nil) 61 | *f = stringValue(v) 62 | return err 63 | } 64 | 65 | func (f *stringValue) Get() interface{} { return string(*f) } 66 | 67 | func (f *stringValue) String() string { return string(*f) } 68 | 69 | // String parses the next command-line value as string. 70 | func (p *parserMixin) String() (target *string) { 71 | target = new(string) 72 | p.StringVar(target) 73 | return 74 | } 75 | 76 | func (p *parserMixin) StringVar(target *string) { 77 | p.SetValue(newStringValue(target)) 78 | } 79 | 80 | // Strings accumulates string values into a slice. 81 | func (p *parserMixin) Strings() (target *[]string) { 82 | target = new([]string) 83 | p.StringsVar(target) 84 | return 85 | } 86 | 87 | func (p *parserMixin) StringsVar(target *[]string) { 88 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newStringValue(v.(*string)) })) 89 | } 90 | 91 | // -- uint Value 92 | type uintValue uint 93 | 94 | func newUintValue(p *uint) *uintValue { 95 | return (*uintValue)(p) 96 | } 97 | 98 | func (f *uintValue) Set(s string) error { 99 | v, err := strconv.ParseUint(s, 0, 64) 100 | *f = uintValue(v) 101 | return err 102 | } 103 | 104 | func (f *uintValue) Get() interface{} { return uint(*f) } 105 | 106 | func (f *uintValue) String() string { return fmt.Sprintf("%v", *f) } 107 | 108 | // Uint parses the next command-line value as uint. 109 | func (p *parserMixin) Uint() (target *uint) { 110 | target = new(uint) 111 | p.UintVar(target) 112 | return 113 | } 114 | 115 | func (p *parserMixin) UintVar(target *uint) { 116 | p.SetValue(newUintValue(target)) 117 | } 118 | 119 | // Uints accumulates uint values into a slice. 120 | func (p *parserMixin) Uints() (target *[]uint) { 121 | target = new([]uint) 122 | p.UintsVar(target) 123 | return 124 | } 125 | 126 | func (p *parserMixin) UintsVar(target *[]uint) { 127 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newUintValue(v.(*uint)) })) 128 | } 129 | 130 | // -- uint8 Value 131 | type uint8Value uint8 132 | 133 | func newUint8Value(p *uint8) *uint8Value { 134 | return (*uint8Value)(p) 135 | } 136 | 137 | func (f *uint8Value) Set(s string) error { 138 | v, err := strconv.ParseUint(s, 0, 8) 139 | *f = uint8Value(v) 140 | return err 141 | } 142 | 143 | func (f *uint8Value) Get() interface{} { return uint8(*f) } 144 | 145 | func (f *uint8Value) String() string { return fmt.Sprintf("%v", *f) } 146 | 147 | // Uint8 parses the next command-line value as uint8. 148 | func (p *parserMixin) Uint8() (target *uint8) { 149 | target = new(uint8) 150 | p.Uint8Var(target) 151 | return 152 | } 153 | 154 | func (p *parserMixin) Uint8Var(target *uint8) { 155 | p.SetValue(newUint8Value(target)) 156 | } 157 | 158 | // Uint8List accumulates uint8 values into a slice. 159 | func (p *parserMixin) Uint8List() (target *[]uint8) { 160 | target = new([]uint8) 161 | p.Uint8ListVar(target) 162 | return 163 | } 164 | 165 | func (p *parserMixin) Uint8ListVar(target *[]uint8) { 166 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newUint8Value(v.(*uint8)) })) 167 | } 168 | 169 | // -- uint16 Value 170 | type uint16Value uint16 171 | 172 | func newUint16Value(p *uint16) *uint16Value { 173 | return (*uint16Value)(p) 174 | } 175 | 176 | func (f *uint16Value) Set(s string) error { 177 | v, err := strconv.ParseUint(s, 0, 16) 178 | *f = uint16Value(v) 179 | return err 180 | } 181 | 182 | func (f *uint16Value) Get() interface{} { return uint16(*f) } 183 | 184 | func (f *uint16Value) String() string { return fmt.Sprintf("%v", *f) } 185 | 186 | // Uint16 parses the next command-line value as uint16. 187 | func (p *parserMixin) Uint16() (target *uint16) { 188 | target = new(uint16) 189 | p.Uint16Var(target) 190 | return 191 | } 192 | 193 | func (p *parserMixin) Uint16Var(target *uint16) { 194 | p.SetValue(newUint16Value(target)) 195 | } 196 | 197 | // Uint16List accumulates uint16 values into a slice. 198 | func (p *parserMixin) Uint16List() (target *[]uint16) { 199 | target = new([]uint16) 200 | p.Uint16ListVar(target) 201 | return 202 | } 203 | 204 | func (p *parserMixin) Uint16ListVar(target *[]uint16) { 205 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newUint16Value(v.(*uint16)) })) 206 | } 207 | 208 | // -- uint32 Value 209 | type uint32Value uint32 210 | 211 | func newUint32Value(p *uint32) *uint32Value { 212 | return (*uint32Value)(p) 213 | } 214 | 215 | func (f *uint32Value) Set(s string) error { 216 | v, err := strconv.ParseUint(s, 0, 32) 217 | *f = uint32Value(v) 218 | return err 219 | } 220 | 221 | func (f *uint32Value) Get() interface{} { return uint32(*f) } 222 | 223 | func (f *uint32Value) String() string { return fmt.Sprintf("%v", *f) } 224 | 225 | // Uint32 parses the next command-line value as uint32. 226 | func (p *parserMixin) Uint32() (target *uint32) { 227 | target = new(uint32) 228 | p.Uint32Var(target) 229 | return 230 | } 231 | 232 | func (p *parserMixin) Uint32Var(target *uint32) { 233 | p.SetValue(newUint32Value(target)) 234 | } 235 | 236 | // Uint32List accumulates uint32 values into a slice. 237 | func (p *parserMixin) Uint32List() (target *[]uint32) { 238 | target = new([]uint32) 239 | p.Uint32ListVar(target) 240 | return 241 | } 242 | 243 | func (p *parserMixin) Uint32ListVar(target *[]uint32) { 244 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newUint32Value(v.(*uint32)) })) 245 | } 246 | 247 | // -- uint64 Value 248 | type uint64Value uint64 249 | 250 | func newUint64Value(p *uint64) *uint64Value { 251 | return (*uint64Value)(p) 252 | } 253 | 254 | func (f *uint64Value) Set(s string) error { 255 | v, err := strconv.ParseUint(s, 0, 64) 256 | *f = uint64Value(v) 257 | return err 258 | } 259 | 260 | func (f *uint64Value) Get() interface{} { return uint64(*f) } 261 | 262 | func (f *uint64Value) String() string { return fmt.Sprintf("%v", *f) } 263 | 264 | // Uint64 parses the next command-line value as uint64. 265 | func (p *parserMixin) Uint64() (target *uint64) { 266 | target = new(uint64) 267 | p.Uint64Var(target) 268 | return 269 | } 270 | 271 | func (p *parserMixin) Uint64Var(target *uint64) { 272 | p.SetValue(newUint64Value(target)) 273 | } 274 | 275 | // Uint64List accumulates uint64 values into a slice. 276 | func (p *parserMixin) Uint64List() (target *[]uint64) { 277 | target = new([]uint64) 278 | p.Uint64ListVar(target) 279 | return 280 | } 281 | 282 | func (p *parserMixin) Uint64ListVar(target *[]uint64) { 283 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newUint64Value(v.(*uint64)) })) 284 | } 285 | 286 | // -- int Value 287 | type intValue int 288 | 289 | func newIntValue(p *int) *intValue { 290 | return (*intValue)(p) 291 | } 292 | 293 | func (f *intValue) Set(s string) error { 294 | v, err := strconv.ParseFloat(s, 64) 295 | *f = intValue(v) 296 | return err 297 | } 298 | 299 | func (f *intValue) Get() interface{} { return int(*f) } 300 | 301 | func (f *intValue) String() string { return fmt.Sprintf("%v", *f) } 302 | 303 | // Int parses the next command-line value as int. 304 | func (p *parserMixin) Int() (target *int) { 305 | target = new(int) 306 | p.IntVar(target) 307 | return 308 | } 309 | 310 | func (p *parserMixin) IntVar(target *int) { 311 | p.SetValue(newIntValue(target)) 312 | } 313 | 314 | // Ints accumulates int values into a slice. 315 | func (p *parserMixin) Ints() (target *[]int) { 316 | target = new([]int) 317 | p.IntsVar(target) 318 | return 319 | } 320 | 321 | func (p *parserMixin) IntsVar(target *[]int) { 322 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newIntValue(v.(*int)) })) 323 | } 324 | 325 | // -- int8 Value 326 | type int8Value int8 327 | 328 | func newInt8Value(p *int8) *int8Value { 329 | return (*int8Value)(p) 330 | } 331 | 332 | func (f *int8Value) Set(s string) error { 333 | v, err := strconv.ParseInt(s, 0, 8) 334 | *f = int8Value(v) 335 | return err 336 | } 337 | 338 | func (f *int8Value) Get() interface{} { return int8(*f) } 339 | 340 | func (f *int8Value) String() string { return fmt.Sprintf("%v", *f) } 341 | 342 | // Int8 parses the next command-line value as int8. 343 | func (p *parserMixin) Int8() (target *int8) { 344 | target = new(int8) 345 | p.Int8Var(target) 346 | return 347 | } 348 | 349 | func (p *parserMixin) Int8Var(target *int8) { 350 | p.SetValue(newInt8Value(target)) 351 | } 352 | 353 | // Int8List accumulates int8 values into a slice. 354 | func (p *parserMixin) Int8List() (target *[]int8) { 355 | target = new([]int8) 356 | p.Int8ListVar(target) 357 | return 358 | } 359 | 360 | func (p *parserMixin) Int8ListVar(target *[]int8) { 361 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newInt8Value(v.(*int8)) })) 362 | } 363 | 364 | // -- int16 Value 365 | type int16Value int16 366 | 367 | func newInt16Value(p *int16) *int16Value { 368 | return (*int16Value)(p) 369 | } 370 | 371 | func (f *int16Value) Set(s string) error { 372 | v, err := strconv.ParseInt(s, 0, 16) 373 | *f = int16Value(v) 374 | return err 375 | } 376 | 377 | func (f *int16Value) Get() interface{} { return int16(*f) } 378 | 379 | func (f *int16Value) String() string { return fmt.Sprintf("%v", *f) } 380 | 381 | // Int16 parses the next command-line value as int16. 382 | func (p *parserMixin) Int16() (target *int16) { 383 | target = new(int16) 384 | p.Int16Var(target) 385 | return 386 | } 387 | 388 | func (p *parserMixin) Int16Var(target *int16) { 389 | p.SetValue(newInt16Value(target)) 390 | } 391 | 392 | // Int16List accumulates int16 values into a slice. 393 | func (p *parserMixin) Int16List() (target *[]int16) { 394 | target = new([]int16) 395 | p.Int16ListVar(target) 396 | return 397 | } 398 | 399 | func (p *parserMixin) Int16ListVar(target *[]int16) { 400 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newInt16Value(v.(*int16)) })) 401 | } 402 | 403 | // -- int32 Value 404 | type int32Value int32 405 | 406 | func newInt32Value(p *int32) *int32Value { 407 | return (*int32Value)(p) 408 | } 409 | 410 | func (f *int32Value) Set(s string) error { 411 | v, err := strconv.ParseInt(s, 0, 32) 412 | *f = int32Value(v) 413 | return err 414 | } 415 | 416 | func (f *int32Value) Get() interface{} { return int32(*f) } 417 | 418 | func (f *int32Value) String() string { return fmt.Sprintf("%v", *f) } 419 | 420 | // Int32 parses the next command-line value as int32. 421 | func (p *parserMixin) Int32() (target *int32) { 422 | target = new(int32) 423 | p.Int32Var(target) 424 | return 425 | } 426 | 427 | func (p *parserMixin) Int32Var(target *int32) { 428 | p.SetValue(newInt32Value(target)) 429 | } 430 | 431 | // Int32List accumulates int32 values into a slice. 432 | func (p *parserMixin) Int32List() (target *[]int32) { 433 | target = new([]int32) 434 | p.Int32ListVar(target) 435 | return 436 | } 437 | 438 | func (p *parserMixin) Int32ListVar(target *[]int32) { 439 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newInt32Value(v.(*int32)) })) 440 | } 441 | 442 | // -- int64 Value 443 | type int64Value int64 444 | 445 | func newInt64Value(p *int64) *int64Value { 446 | return (*int64Value)(p) 447 | } 448 | 449 | func (f *int64Value) Set(s string) error { 450 | v, err := strconv.ParseInt(s, 0, 64) 451 | *f = int64Value(v) 452 | return err 453 | } 454 | 455 | func (f *int64Value) Get() interface{} { return int64(*f) } 456 | 457 | func (f *int64Value) String() string { return fmt.Sprintf("%v", *f) } 458 | 459 | // Int64 parses the next command-line value as int64. 460 | func (p *parserMixin) Int64() (target *int64) { 461 | target = new(int64) 462 | p.Int64Var(target) 463 | return 464 | } 465 | 466 | func (p *parserMixin) Int64Var(target *int64) { 467 | p.SetValue(newInt64Value(target)) 468 | } 469 | 470 | // Int64List accumulates int64 values into a slice. 471 | func (p *parserMixin) Int64List() (target *[]int64) { 472 | target = new([]int64) 473 | p.Int64ListVar(target) 474 | return 475 | } 476 | 477 | func (p *parserMixin) Int64ListVar(target *[]int64) { 478 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newInt64Value(v.(*int64)) })) 479 | } 480 | 481 | // -- float64 Value 482 | type float64Value float64 483 | 484 | func newFloat64Value(p *float64) *float64Value { 485 | return (*float64Value)(p) 486 | } 487 | 488 | func (f *float64Value) Set(s string) error { 489 | v, err := strconv.ParseFloat(s, 64) 490 | *f = float64Value(v) 491 | return err 492 | } 493 | 494 | func (f *float64Value) Get() interface{} { return float64(*f) } 495 | 496 | func (f *float64Value) String() string { return fmt.Sprintf("%v", *f) } 497 | 498 | // Float64 parses the next command-line value as float64. 499 | func (p *parserMixin) Float64() (target *float64) { 500 | target = new(float64) 501 | p.Float64Var(target) 502 | return 503 | } 504 | 505 | func (p *parserMixin) Float64Var(target *float64) { 506 | p.SetValue(newFloat64Value(target)) 507 | } 508 | 509 | // Float64List accumulates float64 values into a slice. 510 | func (p *parserMixin) Float64List() (target *[]float64) { 511 | target = new([]float64) 512 | p.Float64ListVar(target) 513 | return 514 | } 515 | 516 | func (p *parserMixin) Float64ListVar(target *[]float64) { 517 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newFloat64Value(v.(*float64)) })) 518 | } 519 | 520 | // -- float32 Value 521 | type float32Value float32 522 | 523 | func newFloat32Value(p *float32) *float32Value { 524 | return (*float32Value)(p) 525 | } 526 | 527 | func (f *float32Value) Set(s string) error { 528 | v, err := strconv.ParseFloat(s, 32) 529 | *f = float32Value(v) 530 | return err 531 | } 532 | 533 | func (f *float32Value) Get() interface{} { return float32(*f) } 534 | 535 | func (f *float32Value) String() string { return fmt.Sprintf("%v", *f) } 536 | 537 | // Float32 parses the next command-line value as float32. 538 | func (p *parserMixin) Float32() (target *float32) { 539 | target = new(float32) 540 | p.Float32Var(target) 541 | return 542 | } 543 | 544 | func (p *parserMixin) Float32Var(target *float32) { 545 | p.SetValue(newFloat32Value(target)) 546 | } 547 | 548 | // Float32List accumulates float32 values into a slice. 549 | func (p *parserMixin) Float32List() (target *[]float32) { 550 | target = new([]float32) 551 | p.Float32ListVar(target) 552 | return 553 | } 554 | 555 | func (p *parserMixin) Float32ListVar(target *[]float32) { 556 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newFloat32Value(v.(*float32)) })) 557 | } 558 | 559 | // DurationList accumulates time.Duration values into a slice. 560 | func (p *parserMixin) DurationList() (target *[]time.Duration) { 561 | target = new([]time.Duration) 562 | p.DurationListVar(target) 563 | return 564 | } 565 | 566 | func (p *parserMixin) DurationListVar(target *[]time.Duration) { 567 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newDurationValue(v.(*time.Duration)) })) 568 | } 569 | 570 | // IPList accumulates net.IP values into a slice. 571 | func (p *parserMixin) IPList() (target *[]net.IP) { 572 | target = new([]net.IP) 573 | p.IPListVar(target) 574 | return 575 | } 576 | 577 | func (p *parserMixin) IPListVar(target *[]net.IP) { 578 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newIPValue(v.(*net.IP)) })) 579 | } 580 | 581 | // TCPList accumulates *net.TCPAddr values into a slice. 582 | func (p *parserMixin) TCPList() (target *[]*net.TCPAddr) { 583 | target = new([]*net.TCPAddr) 584 | p.TCPListVar(target) 585 | return 586 | } 587 | 588 | func (p *parserMixin) TCPListVar(target *[]*net.TCPAddr) { 589 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newTCPAddrValue(v.(**net.TCPAddr)) })) 590 | } 591 | 592 | // ExistingFiles accumulates string values into a slice. 593 | func (p *parserMixin) ExistingFiles() (target *[]string) { 594 | target = new([]string) 595 | p.ExistingFilesVar(target) 596 | return 597 | } 598 | 599 | func (p *parserMixin) ExistingFilesVar(target *[]string) { 600 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newExistingFileValue(v.(*string)) })) 601 | } 602 | 603 | // ExistingDirs accumulates string values into a slice. 604 | func (p *parserMixin) ExistingDirs() (target *[]string) { 605 | target = new([]string) 606 | p.ExistingDirsVar(target) 607 | return 608 | } 609 | 610 | func (p *parserMixin) ExistingDirsVar(target *[]string) { 611 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newExistingDirValue(v.(*string)) })) 612 | } 613 | 614 | // ExistingFilesOrDirs accumulates string values into a slice. 615 | func (p *parserMixin) ExistingFilesOrDirs() (target *[]string) { 616 | target = new([]string) 617 | p.ExistingFilesOrDirsVar(target) 618 | return 619 | } 620 | 621 | func (p *parserMixin) ExistingFilesOrDirsVar(target *[]string) { 622 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newExistingFileOrDirValue(v.(*string)) })) 623 | } 624 | 625 | // RegexpList accumulates *regexp.Regexp values into a slice. 626 | func (p *parserMixin) RegexpList() (target *[]*regexp.Regexp) { 627 | target = new([]*regexp.Regexp) 628 | p.RegexpListVar(target) 629 | return 630 | } 631 | 632 | func (p *parserMixin) RegexpListVar(target *[]*regexp.Regexp) { 633 | p.SetValue(newAccumulator(target, func(v interface{}) Value { return newRegexpValue(v.(**regexp.Regexp)) })) 634 | } 635 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kingpin - A Go (golang) command line and flag parser [![Build Status](https://travis-ci.org/alecthomas/kingpin.png)](https://travis-ci.org/alecthomas/kingpin) 2 | 3 | 4 | 5 | - [Overview](#overview) 6 | - [Features](#features) 7 | - [User-visible changes between v1 and v2](#user-visible-changes-between-v1-and-v2) 8 | - [Flags can be used at any point after their definition.](#flags-can-be-used-at-any-point-after-their-definition) 9 | - [Short flags can be combined with their parameters](#short-flags-can-be-combined-with-their-parameters) 10 | - [API changes between v1 and v2](#api-changes-between-v1-and-v2) 11 | - [Versions](#versions) 12 | - [V2 is the current stable version](#v2-is-the-current-stable-version) 13 | - [V1 is the OLD stable version](#v1-is-the-old-stable-version) 14 | - [Change History](#change-history) 15 | - [Examples](#examples) 16 | - [Simple Example](#simple-example) 17 | - [Complex Example](#complex-example) 18 | - [Reference Documentation](#reference-documentation) 19 | - [Displaying errors and usage information](#displaying-errors-and-usage-information) 20 | - [Sub-commands](#sub-commands) 21 | - [Custom Parsers](#custom-parsers) 22 | - [Default Values](#default-values) 23 | - [Place-holders in Help](#place-holders-in-help) 24 | - [Consuming all remaining arguments](#consuming-all-remaining-arguments) 25 | - [Supporting -h for help](#supporting--h-for-help) 26 | - [Custom help](#custom-help) 27 | 28 | 29 | 30 | ## Overview 31 | 32 | Kingpin is a [fluent-style](http://en.wikipedia.org/wiki/Fluent_interface), 33 | type-safe command-line parser. It supports flags, nested commands, and 34 | positional arguments. 35 | 36 | Install it with: 37 | 38 | $ go get gopkg.in/alecthomas/kingpin.v2 39 | 40 | It looks like this: 41 | 42 | ```go 43 | var ( 44 | verbose = kingpin.Flag("verbose", "Verbose mode.").Short('v').Bool() 45 | name = kingpin.Arg("name", "Name of user.").Required().String() 46 | ) 47 | 48 | func main() { 49 | kingpin.Parse() 50 | fmt.Printf("%v, %s\n", *verbose, *name) 51 | } 52 | ``` 53 | 54 | More [examples](https://github.com/alecthomas/kingpin/tree/master/examples) are available. 55 | 56 | Second to parsing, providing the user with useful help is probably the most 57 | important thing a command-line parser does. Kingpin tries to provide detailed 58 | contextual help if `--help` is encountered at any point in the command line 59 | (excluding after `--`). 60 | 61 | ## Features 62 | 63 | - Help output that isn't as ugly as sin. 64 | - Fully [customisable help](#custom-help), via Go templates. 65 | - Parsed, type-safe flags (`kingpin.Flag("f", "help").Int()`) 66 | - Parsed, type-safe positional arguments (`kingpin.Arg("a", "help").Int()`). 67 | - Parsed, type-safe, arbitrarily deep commands (`kingpin.Command("c", "help")`). 68 | - Support for required flags and required positional arguments (`kingpin.Flag("f", "").Required().Int()`). 69 | - Support for arbitrarily nested default commands (`command.Default()`). 70 | - Callbacks per command, flag and argument (`kingpin.Command("c", "").Action(myAction)`). 71 | - POSIX-style short flag combining (`-a -b` -> `-ab`). 72 | - Short-flag+parameter combining (`-a parm` -> `-aparm`). 73 | - Read command-line from files (`@`). 74 | - Automatically generate man pages (`--man-page`). 75 | 76 | ## User-visible changes between v1 and v2 77 | 78 | ### Flags can be used at any point after their definition. 79 | 80 | Flags can be specified at any point after their definition, not just 81 | *immediately after their associated command*. From the chat example below, the 82 | following used to be required: 83 | 84 | ``` 85 | $ chat --server=chat.server.com:8080 post --image=~/Downloads/owls.jpg pics 86 | ``` 87 | 88 | But the following will now work: 89 | 90 | ``` 91 | $ chat post --server=chat.server.com:8080 --image=~/Downloads/owls.jpg pics 92 | ``` 93 | 94 | ### Short flags can be combined with their parameters 95 | 96 | Previously, if a short flag was used, any argument to that flag would have to 97 | be separated by a space. That is no longer the case. 98 | 99 | ## API changes between v1 and v2 100 | 101 | - `ParseWithFileExpansion()` is gone. The new parser directly supports expanding `@`. 102 | - Added `FatalUsage()` and `FatalUsageContext()` for displaying an error + usage and terminating. 103 | - `Dispatch()` renamed to `Action()`. 104 | - Added `ParseContext()` for parsing a command line into its intermediate context form without executing. 105 | - Added `Terminate()` function to override the termination function. 106 | - Added `UsageForContextWithTemplate()` for printing usage via a custom template. 107 | - Added `UsageTemplate()` for overriding the default template to use. Two templates are included: 108 | 1. `DefaultUsageTemplate` - default template. 109 | 2. `CompactUsageTemplate` - compact command template for larger applications. 110 | 111 | ## Versions 112 | 113 | Kingpin uses [gopkg.in](https://gopkg.in/alecthomas/kingpin) for versioning. 114 | 115 | The current stable version is [gopkg.in/alecthomas/kingpin.v2](https://gopkg.in/alecthomas/kingpin.v2). The previous version, [gopkg.in/alecthomas/kingpin.v1](https://gopkg.in/alecthomas/kingpin.v1), is deprecated and in maintenance mode. 116 | 117 | ### [V2](https://gopkg.in/alecthomas/kingpin.v2) is the current stable version 118 | 119 | Installation: 120 | 121 | ```sh 122 | $ go get gopkg.in/alecthomas/kingpin.v2 123 | ``` 124 | 125 | ### [V1](https://gopkg.in/alecthomas/kingpin.v1) is the OLD stable version 126 | 127 | Installation: 128 | 129 | ```sh 130 | $ go get gopkg.in/alecthomas/kingpin.v1 131 | ``` 132 | 133 | ## Change History 134 | 135 | - *2015-09-19* -- Stable v2.1.0 release. 136 | - Added `command.Default()` to specify a default command to use if no other 137 | command matches. This allows for convenient user shortcuts. 138 | - Exposed `HelpFlag` and `VersionFlag` for further cusomisation. 139 | - `Action()` and `PreAction()` added and both now support an arbitrary 140 | number of callbacks. 141 | - `kingpin.SeparateOptionalFlagsUsageTemplate`. 142 | - `--help-long` and `--help-man` (hidden by default) flags. 143 | - Flags are "interspersed" by default, but can be disabled with `app.Interspersed(false)`. 144 | - Added flags for all simple builtin types (int8, uint16, etc.) and slice variants. 145 | - Use `app.Writer(os.Writer)` to specify the default writer for all output functions. 146 | - Dropped `os.Writer` prefix from all printf-like functions. 147 | 148 | - *2015-05-22* -- Stable v2.0.0 release. 149 | - Initial stable release of v2.0.0. 150 | - Fully supports interspersed flags, commands and arguments. 151 | - Flags can be present at any point after their logical definition. 152 | - Application.Parse() terminates if commands are present and a command is not parsed. 153 | - Dispatch() -> Action(). 154 | - Actions are dispatched after all values are populated. 155 | - Override termination function (defaults to os.Exit). 156 | - Override output stream (defaults to os.Stderr). 157 | - Templatised usage help, with default and compact templates. 158 | - Make error/usage functions more consistent. 159 | - Support argument expansion from files by default (with @). 160 | - Fully public data model is available via .Model(). 161 | - Parser has been completely refactored. 162 | - Parsing and execution has been split into distinct stages. 163 | - Use `go generate` to generate repeated flags. 164 | - Support combined short-flag+argument: -fARG. 165 | 166 | - *2015-01-23* -- Stable v1.3.4 release. 167 | - Support "--" for separating flags from positional arguments. 168 | - Support loading flags from files (ParseWithFileExpansion()). Use @FILE as an argument. 169 | - Add post-app and post-cmd validation hooks. This allows arbitrary validation to be added. 170 | - A bunch of improvements to help usage and formatting. 171 | - Support arbitrarily nested sub-commands. 172 | 173 | - *2014-07-08* -- Stable v1.2.0 release. 174 | - Pass any value through to `Strings()` when final argument. 175 | Allows for values that look like flags to be processed. 176 | - Allow `--help` to be used with commands. 177 | - Support `Hidden()` flags. 178 | - Parser for [units.Base2Bytes](https://github.com/alecthomas/units) 179 | type. Allows for flags like `--ram=512MB` or `--ram=1GB`. 180 | - Add an `Enum()` value, allowing only one of a set of values 181 | to be selected. eg. `Flag(...).Enum("debug", "info", "warning")`. 182 | 183 | - *2014-06-27* -- Stable v1.1.0 release. 184 | - Bug fixes. 185 | - Always return an error (rather than panicing) when misconfigured. 186 | - `OpenFile(flag, perm)` value type added, for finer control over opening files. 187 | - Significantly improved usage formatting. 188 | 189 | - *2014-06-19* -- Stable v1.0.0 release. 190 | - Support [cumulative positional](#consuming-all-remaining-arguments) arguments. 191 | - Return error rather than panic when there are fatal errors not caught by 192 | the type system. eg. when a default value is invalid. 193 | - Use gokpg.in. 194 | 195 | - *2014-06-10* -- Place-holder streamlining. 196 | - Renamed `MetaVar` to `PlaceHolder`. 197 | - Removed `MetaVarFromDefault`. Kingpin now uses [heuristics](#place-holders-in-help) 198 | to determine what to display. 199 | 200 | ## Examples 201 | 202 | ### Simple Example 203 | 204 | Kingpin can be used for simple flag+arg applications like so: 205 | 206 | ``` 207 | $ ping --help 208 | usage: ping [] [] 209 | 210 | Flags: 211 | --debug Enable debug mode. 212 | --help Show help. 213 | -t, --timeout=5s Timeout waiting for ping. 214 | 215 | Args: 216 | IP address to ping. 217 | [] Number of packets to send 218 | $ ping 1.2.3.4 5 219 | Would ping: 1.2.3.4 with timeout 5s and count 0 220 | ``` 221 | 222 | From the following source: 223 | 224 | ```go 225 | package main 226 | 227 | import ( 228 | "fmt" 229 | 230 | "gopkg.in/alecthomas/kingpin.v2" 231 | ) 232 | 233 | var ( 234 | debug = kingpin.Flag("debug", "Enable debug mode.").Bool() 235 | timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration() 236 | ip = kingpin.Arg("ip", "IP address to ping.").Required().IP() 237 | count = kingpin.Arg("count", "Number of packets to send").Int() 238 | ) 239 | 240 | func main() { 241 | kingpin.Version("0.0.1") 242 | kingpin.Parse() 243 | fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count) 244 | } 245 | ``` 246 | 247 | ### Complex Example 248 | 249 | Kingpin can also produce complex command-line applications with global flags, 250 | subcommands, and per-subcommand flags, like this: 251 | 252 | ``` 253 | $ chat --help 254 | usage: chat [] [] [ ...] 255 | 256 | A command-line chat application. 257 | 258 | Flags: 259 | --help Show help. 260 | --debug Enable debug mode. 261 | --server=127.0.0.1 Server address. 262 | 263 | Commands: 264 | help [] 265 | Show help for a command. 266 | 267 | register 268 | Register a new user. 269 | 270 | post [] [] 271 | Post a message to a channel. 272 | 273 | $ chat help post 274 | usage: chat [] post [] [] 275 | 276 | Post a message to a channel. 277 | 278 | Flags: 279 | --image=IMAGE Image to post. 280 | 281 | Args: 282 | Channel to post to. 283 | [] Text to post. 284 | 285 | $ chat post --image=~/Downloads/owls.jpg pics 286 | ... 287 | ``` 288 | 289 | From this code: 290 | 291 | ```go 292 | package main 293 | 294 | import ( 295 | "os" 296 | "strings" 297 | "gopkg.in/alecthomas/kingpin.v2" 298 | ) 299 | 300 | var ( 301 | app = kingpin.New("chat", "A command-line chat application.") 302 | debug = app.Flag("debug", "Enable debug mode.").Bool() 303 | serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP() 304 | 305 | register = app.Command("register", "Register a new user.") 306 | registerNick = register.Arg("nick", "Nickname for user.").Required().String() 307 | registerName = register.Arg("name", "Name of user.").Required().String() 308 | 309 | post = app.Command("post", "Post a message to a channel.") 310 | postImage = post.Flag("image", "Image to post.").File() 311 | postChannel = post.Arg("channel", "Channel to post to.").Required().String() 312 | postText = post.Arg("text", "Text to post.").Strings() 313 | ) 314 | 315 | func main() { 316 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 317 | // Register user 318 | case register.FullCommand(): 319 | println(*registerNick) 320 | 321 | // Post message 322 | case post.FullCommand(): 323 | if *postImage != nil { 324 | } 325 | text := strings.Join(*postText, " ") 326 | println("Post:", text) 327 | } 328 | } 329 | ``` 330 | 331 | ## Reference Documentation 332 | 333 | ### Displaying errors and usage information 334 | 335 | Kingpin exports a set of functions to provide consistent errors and usage 336 | information to the user. 337 | 338 | Error messages look something like this: 339 | 340 | : error: 341 | 342 | The functions on `Application` are: 343 | 344 | Function | Purpose 345 | ---------|-------------- 346 | `Errorf(format, args)` | Display a printf formatted error to the user. 347 | `Fatalf(format, args)` | As with Errorf, but also call the termination handler. 348 | `FatalUsage(format, args)` | As with Fatalf, but also print contextual usage information. 349 | `FatalUsageContext(context, format, args)` | As with Fatalf, but also print contextual usage information from a `ParseContext`. 350 | `FatalIfError(err, format, args)` | Conditionally print an error prefixed with format+args, then call the termination handler 351 | 352 | There are equivalent global functions in the kingpin namespace for the default 353 | `kingpin.CommandLine` instance. 354 | 355 | ### Sub-commands 356 | 357 | Kingpin supports nested sub-commands, with separate flag and positional 358 | arguments per sub-command. Note that positional arguments may only occur after 359 | sub-commands. 360 | 361 | For example: 362 | 363 | ```go 364 | var ( 365 | deleteCommand = kingpin.Command("delete", "Delete an object.") 366 | deleteUserCommand = deleteCommand.Command("user", "Delete a user.") 367 | deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.") 368 | deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.") 369 | deletePostCommand = deleteCommand.Command("post", "Delete a post.") 370 | ) 371 | 372 | func main() { 373 | switch kingpin.Parse() { 374 | case "delete user": 375 | case "delete post": 376 | } 377 | } 378 | ``` 379 | 380 | ### Custom Parsers 381 | 382 | Kingpin supports both flag and positional argument parsers for converting to 383 | Go types. For example, some included parsers are `Int()`, `Float()`, 384 | `Duration()` and `ExistingFile()`. 385 | 386 | Parsers conform to Go's [`flag.Value`](http://godoc.org/flag#Value) 387 | interface, so any existing implementations will work. 388 | 389 | For example, a parser for accumulating HTTP header values might look like this: 390 | 391 | ```go 392 | type HTTPHeaderValue http.Header 393 | 394 | func (h *HTTPHeaderValue) Set(value string) error { 395 | parts := strings.SplitN(value, ":", 2) 396 | if len(parts) != 2 { 397 | return fmt.Errorf("expected HEADER:VALUE got '%s'", value) 398 | } 399 | (*http.Header)(h).Add(parts[0], parts[1]) 400 | return nil 401 | } 402 | 403 | func (h *HTTPHeaderValue) String() string { 404 | return "" 405 | } 406 | ``` 407 | 408 | As a convenience, I would recommend something like this: 409 | 410 | ```go 411 | func HTTPHeader(s Settings) (target *http.Header) { 412 | target = new(http.Header) 413 | s.SetValue((*HTTPHeaderValue)(target)) 414 | return 415 | } 416 | ``` 417 | 418 | You would use it like so: 419 | 420 | ```go 421 | headers = HTTPHeader(kingpin.Flag("header", "Add a HTTP header to the request.").Short('H')) 422 | ``` 423 | 424 | ### Default Values 425 | 426 | The default value is the zero value for a type. This can be overridden with 427 | the `Default(value)` function on flags and arguments. This function accepts a 428 | string, which is parsed by the value itself, so it *must* be compliant with 429 | the format expected. 430 | 431 | ### Place-holders in Help 432 | 433 | The place-holder value for a flag is the value used in the help to describe 434 | the value of a non-boolean flag. 435 | 436 | The value provided to PlaceHolder() is used if provided, then the value 437 | provided by Default() if provided, then finally the capitalised flag name is 438 | used. 439 | 440 | Here are some examples of flags with various permutations: 441 | 442 | --name=NAME // Flag(...).String() 443 | --name="Harry" // Flag(...).Default("Harry").String() 444 | --name=FULL-NAME // flag(...).PlaceHolder("FULL-NAME").Default("Harry").String() 445 | 446 | ### Consuming all remaining arguments 447 | 448 | A common command-line idiom is to use all remaining arguments for some 449 | purpose. eg. The following command accepts an arbitrary number of 450 | IP addresses as positional arguments: 451 | 452 | ./cmd ping 10.1.1.1 192.168.1.1 453 | 454 | Kingpin supports this by having `Value` provide a `IsCumulative() bool` 455 | function. If this function exists and returns true, the value parser will be 456 | called repeatedly for every remaining argument. 457 | 458 | Examples of this are the `Strings()` and `StringMap()` values. 459 | 460 | To implement the above example we might do something like this: 461 | 462 | ```go 463 | type ipList []net.IP 464 | 465 | func (i *ipList) Set(value string) error { 466 | if ip := net.ParseIP(value); ip == nil { 467 | return fmt.Errorf("'%s' is not an IP address", value) 468 | } else { 469 | *i = append(*i, ip) 470 | return nil 471 | } 472 | } 473 | 474 | func (i *ipList) String() string { 475 | return "" 476 | } 477 | 478 | func (i *ipList) IsCumulative() bool { 479 | return true 480 | } 481 | 482 | func IPList(s Settings) (target *[]net.IP) { 483 | target = new([]net.IP) 484 | s.SetValue((*ipList)(target)) 485 | return 486 | } 487 | ``` 488 | 489 | And use it like so: 490 | 491 | ```go 492 | ips := IPList(kingpin.Arg("ips", "IP addresses to ping.")) 493 | ``` 494 | 495 | ### Supporting -h for help 496 | 497 | `kingpin.CommandLine.HelpFlag.Short('h')` 498 | 499 | ### Custom help 500 | 501 | Kingpin v2 supports templatised help using the text/template library (actually, [a fork](https://github.com/alecthomas/template)). 502 | 503 | You can specify the template to use with the [Application.UsageTemplate()](http://godoc.org/gopkg.in/alecthomas/kingpin.v2#Application.UsageTemplate) function. 504 | 505 | There are four included templates: `kingpin.DefaultUsageTemplate` is the default, 506 | `kingpin.CompactUsageTemplate` provides a more compact representation for more complex command-line structures, 507 | `kingpin.SeparateOptionalFlagsUsageTemplate` looks like the default template, but splits required 508 | and optional command flags into separate lists, and `kingpin.ManPageTemplate` is used to generate man pages. 509 | 510 | See the above templates for examples of usage, and the the function [UsageForContextWithTemplate()](https://github.com/alecthomas/kingpin/blob/master/usage.go#L198) method for details on the context. 511 | 512 | #### Default help template 513 | 514 | ``` 515 | $ go run ./examples/curl/curl.go --help 516 | usage: curl [] [ ...] 517 | 518 | An example implementation of curl. 519 | 520 | Flags: 521 | --help Show help. 522 | -t, --timeout=5s Set connection timeout. 523 | -H, --headers=HEADER=VALUE 524 | Add HTTP headers to the request. 525 | 526 | Commands: 527 | help [...] 528 | Show help. 529 | 530 | get url 531 | Retrieve a URL. 532 | 533 | get file 534 | Retrieve a file. 535 | 536 | post [] 537 | POST a resource. 538 | ``` 539 | 540 | #### Compact help template 541 | 542 | ``` 543 | $ go run ./examples/curl/curl.go --help 544 | usage: curl [] [ ...] 545 | 546 | An example implementation of curl. 547 | 548 | Flags: 549 | --help Show help. 550 | -t, --timeout=5s Set connection timeout. 551 | -H, --headers=HEADER=VALUE 552 | Add HTTP headers to the request. 553 | 554 | Commands: 555 | help [...] 556 | get [] 557 | url 558 | file 559 | post [] 560 | ``` 561 | --------------------------------------------------------------------------------