diff --git a/global.go b/global.go index 180fd7f..9daaac6 100644 --- a/global.go +++ b/global.go @@ -2,8 +2,9 @@ package kong import "os" -func Parse(cli interface{}) { - parser, err := New("", "", cli) +// Parse constructs a new parser and parses the default command-line. +func Parse(cli interface{}, options ...Option) { + parser, err := New(cli, options...) parser.FatalIfErrorf(err) _, err = parser.Parse(os.Args[1:]) parser.FatalIfErrorf(err) diff --git a/help.go b/help.go new file mode 100644 index 0000000..6332bef --- /dev/null +++ b/help.go @@ -0,0 +1,28 @@ +package kong + +import ( + "io" + "text/template" +) + +const defaultHelp = `{{- with .Application -}} +usage: {{.Name}} + +{{- end -}} +` + +var defaultHelpTemplate = template.Must(template.New("help").Parse(defaultHelp)) + +// WriteHelp to w. If w is nil, the default stdout writer will be used. +func (k *Kong) WriteHelp(w io.Writer) error { + if w == nil { + w = k.stdout + } + ctx := map[string]interface{}{ + "Application": k.Model, + } + for k, v := range k.helpContext { + ctx[k] = v + } + return k.help.Execute(w, ctx) +} diff --git a/kong.go b/kong.go index 6fdfa47..7cfe223 100644 --- a/kong.go +++ b/kong.go @@ -2,10 +2,12 @@ package kong import ( "fmt" + "io" "os" "path/filepath" "reflect" "strings" + "text/template" ) type Error struct{ msg string } @@ -16,27 +18,40 @@ func fail(format string, args ...interface{}) { panic(Error{fmt.Sprintf(format, args...)}) } +// Kong is the main parser type. type Kong struct { Model *Application // Termination function (defaults to os.Exit) - Terminate func(int) + terminate func(int) + + stdout io.Writer + stderr io.Writer + + help *template.Template + helpContext map[string]interface{} + helpFuncs template.FuncMap } // New creates a new Kong parser into ast. -func New(name, description string, ast interface{}) (*Kong, error) { - if name == "" { - name = filepath.Base(os.Args[0]) - } +func New(ast interface{}, options ...Option) (*Kong, error) { model, err := build(ast) if err != nil { return nil, err } - model.Name = name - model.Help = description - return &Kong{ - Model: model, - Terminate: os.Exit, - }, nil + model.Name = filepath.Base(os.Args[0]) + k := &Kong{ + Model: model, + terminate: os.Exit, + stdout: os.Stdout, + stderr: os.Stderr, + help: defaultHelpTemplate, + helpContext: map[string]interface{}{}, + helpFuncs: template.FuncMap{}, + } + for _, option := range options { + option(k) + } + return k, nil } // Parse arguments into target. @@ -183,6 +198,24 @@ func (k *Kong) applyNode(scan *Scanner, node *Node) (command []string, err error return nil, fmt.Errorf("unexpected token %s", token) } } + if positional < len(node.Positional) { + missing := []string{} + for ; positional < len(node.Positional); positional++ { + missing = append(missing, "<"+node.Positional[positional].Name+">") + } + return nil, fmt.Errorf("missing positional arguments %s", strings.Join(missing, " ")) + } + if len(node.Children) > 0 { + missing := []string{} + for _, child := range node.Children { + if child.Argument != nil { + missing = append(missing, "<"+child.Argument.Name+">") + } else { + missing = append(missing, child.Command.Name) + } + } + return nil, fmt.Errorf("expected one of %s", strings.Join(missing, ", ")) + } return } @@ -221,5 +254,5 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) { msg = fmt.Sprintf(args[0].(string), args...) + ": " + err.Error() } k.Errorf("%s", msg) - k.Terminate(1) + k.terminate(1) } diff --git a/kong_test.go b/kong_test.go index 007a317..99f1621 100644 --- a/kong_test.go +++ b/kong_test.go @@ -8,12 +8,12 @@ import ( func mustNew(t *testing.T, cli interface{}) *Kong { t.Helper() - parser, err := New("", "", cli) + parser, err := New(cli) require.NoError(t, err) return parser } -func TestArgumentSequence(t *testing.T) { +func TestPositionalArguments(t *testing.T) { var cli struct { User struct { Create struct { @@ -27,6 +27,10 @@ func TestArgumentSequence(t *testing.T) { cmd, err := p.Parse([]string{"user", "create", "10", "Alec", "Thomas"}) require.NoError(t, err) require.Equal(t, "user create ", cmd) + t.Run("Missing", func(t *testing.T) { + _, err := p.Parse([]string{"user", "create", "10"}) + require.Error(t, err) + }) } func TestBranchingArgument(t *testing.T) { @@ -60,6 +64,10 @@ func TestBranchingArgument(t *testing.T) { require.NoError(t, err) require.Equal(t, 10, cli.User.ID.ID) require.Equal(t, "user delete", cmd) + t.Run("Missing", func(t *testing.T) { + _, err = p.Parse([]string{"user"}) + require.Error(t, err) + }) } func TestResetWithDefaults(t *testing.T) { @@ -102,7 +110,7 @@ func TestUnsupportedFieldErrors(t *testing.T) { var cli struct { Keys map[string]string } - _, err := New("", "", &cli) + _, err := New(&cli) require.Error(t, err) } @@ -113,6 +121,6 @@ func TestMatchingArgField(t *testing.T) { } `arg:""` } - _, err := New("", "", &cli) + _, err := New(&cli) require.Error(t, err) } diff --git a/options.go b/options.go new file mode 100644 index 0000000..ecabeaf --- /dev/null +++ b/options.go @@ -0,0 +1,40 @@ +package kong + +import ( + "io" + "text/template" +) + +type Option func(k *Kong) + +// ExitFunction overrides the function used to terminate. This is useful for testing or interactive use. +func ExitFunction(exit func(int)) Option { + return func(k *Kong) { k.terminate = exit } +} + +// Name overrides the application name. +func Name(name string) Option { + return func(k *Kong) { k.Model.Name = name } +} + +// Description sets the application description. +func Description(description string) Option { + return func(k *Kong) { k.Model.Help = description } +} + +// HelpTemplate overrides the default help template. +func HelpTemplate(template *template.Template) Option { + return func(k *Kong) { k.help = template } +} + +// HelpContext sets extra context in the help template. +// +// The key "Application" will always be available and is the root of the application model. +func HelpContext(context map[string]interface{}) Option { + return func(k *Kong) { k.helpContext = context } +} + +// Writers overrides the default writers. Useful for testing or interactive use. +func Writers(stdout, stderr io.Writer) Option { + return func(k *Kong) { k.stdout = stdout } +}