diff --git a/README.md b/README.md index 5b7b3fb..18b5411 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,108 @@ go get github.com/broothie/cli@latest ## Documentation -https://pkg.go.dev/github.com/broothie/cli +Detailed documentation can be found at [pkg.go.dev](https://pkg.go.dev/github.com/broothie/cli). -## To Do +## Usage + +Using `cli` is as simple as: + +```go +// Create and run a command called "fileserver". +// `Run` automatically passes down a `context.Background()` and parses `os.Args[1:]`. +// If an error is returned, and it is either a `cli.ExitError` or an `*exec.ExitError`, the error's exit code will be used. +// For any other errors returned, it exits with code 1. +cli.Run("fileserver", "An HTTP server.", + + // Add an optional positional argument called "root" which will default to ".". + cli.AddArg("root", "Directory to serve from", cli.SetArgDefault(".")), + + // Add an optional flag called "port" which will default to 3000. + cli.AddFlag("port", "Port to run server.", cli.SetFlagDefault(3000)), + + // Register a handler for this command. + // If no handler is registered, it will simply print help and exit. + cli.SetHandler(func(ctx context.Context) error { + // Extract the value of the "root" argument. + root, _ := cli.ArgValue[string](ctx, "root") + + // Extract the value of the "port" flag. + port, _ := cli.FlagValue[int](ctx, "port") + + addr := fmt.Sprintf(":%d", port) + return http.ListenAndServe(addr, http.FileServer(http.Dir(root))) + }), +) + +``` + +Here's an example using the complete set of options: + +```go +cmd, err := cli.NewCommand("git", "Modern version control.", + // Set command version + cli.SetVersion("2.37.0"), + + // Add a "--version" flag with a short flag "-V" for printing the command version + cli.AddVersionFlag(cli.AddFlagShort('V')), + + // Add a "--help" flag + cli.AddHelpFlag( + + // Add a short flag "-h" to help + cli.AddFlagShort('h'), + + // Make this flag inherited by sub-commands + cli.SetFlagIsInherited(true), + ), + + // Add a hidden "--debug" flag + cli.AddFlag("debug", "Enable debugging", + cli.SetFlagDefault(false), // Default parser for flags is cli.StringParser + + // Make it hidden + cli.SetFlagIsHidden(true), + ), + + // Add a sub-command "clone" + cli.AddSubCmd("clone", "Clone a repository.", + + // Add a required argument "" + cli.AddArg("url", "Repository to clone.", + + // Parse it into a *url.URL + cli.SetArgParser(cli.URLParser), + ), + + // Add optional argument "" + cli.AddArg("dir", "Directory to clone repo into.", + + // Set its default value to "." + cli.SetArgDefault("."), + ), + + // Add a flag "--verbose" + cli.AddFlag("verbose", "Be more verbose.", + + // Add a short "-v" + cli.AddFlagShort('v'), + + // Make it a boolean that defaults to false + cli.SetFlagDefault(false), + ), + ), +) +if err != nil { + cli.ExitWithError(err) +} + +// Pass in your `context.Context` and args +if err := cmd.Run(context.TODO(), os.Args[1:]); err != nil { + cli.ExitWithError(err) +} +``` + +## Roadmap - [ ] Audit bare `err` returns - [ ] Two types of errors: config and parse diff --git a/command.go b/command.go index 7f24f14..6069032 100644 --- a/command.go +++ b/command.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "os/exec" "strings" "github.com/bobg/errors" @@ -53,13 +52,7 @@ func Run(name, description string, options ...option.Option[*Command]) { } if err := command.Run(context.Background(), os.Args[1:]); err != nil { - fmt.Println(err) - - if exitErr := new(exec.ExitError); errors.As(err, &exitErr) { - os.Exit(exitErr.ExitCode()) - } else { - os.Exit(1) - } + ExitWithError(err) } } diff --git a/command_test.go b/command_test.go index a737d60..2167be9 100644 --- a/command_test.go +++ b/command_test.go @@ -1,14 +1,12 @@ package cli -import ( - "os" -) +import "os" func ExampleNewCommand() { command, _ := NewCommand("server", "An http server.", - AddHelpFlag(AddFlagShort('h')), SetVersion("v0.1.0"), AddVersionFlag(AddFlagShort('V')), + AddHelpFlag(AddFlagShort('h')), AddFlag("port", "Port to run server on.", SetFlagDefault(3000), AddFlagShort('p'), @@ -34,8 +32,8 @@ func ExampleNewCommand() { // proxy: Proxy requests to another server. // // Flags: - // --help -h Print help. (type: bool, default: "false") // --version -V Print version. (type: bool, default: "false") + // --help -h Print help. (type: bool, default: "false") // --port -p Port to run server on. (type: int, default: "3000") // --auth-required Whether to require authentication. (type: bool, default: "true") } diff --git a/error.go b/error.go new file mode 100644 index 0000000..bf062ac --- /dev/null +++ b/error.go @@ -0,0 +1,33 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + + "github.com/bobg/errors" +) + +type ExitError struct { + Code int +} + +func (e ExitError) Error() string { + return fmt.Sprintf("exit status %d", e.Code) +} + +func ExitCode(code int) ExitError { + return ExitError{Code: code} +} + +func ExitWithError(err error) { + fmt.Println(err) + + if exitErr := new(ExitError); errors.As(err, &exitErr) { + os.Exit(exitErr.Code) + } else if exitErr := new(exec.ExitError); errors.As(err, &exitErr) { + os.Exit(exitErr.ExitCode()) + } else { + os.Exit(1) + } +} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..3cbcb47 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,103 @@ +package cli_test + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/broothie/cli" +) + +func Example_basic_usage() { + // Create and run a command called "fileserver". + // `Run` automatically passes down a `context.Background()` and parses `os.Args[1:]`. + // If an error is returned, and it is either a `cli.ExitError` or an `*exec.ExitError`, the error's exit code will be used. + // For any other errors returned, it exits with code 1. + cli.Run("fileserver", "An HTTP server.", + + // Add an optional positional argument called "root" which will default to ".". + cli.AddArg("root", "Directory to serve from", cli.SetArgDefault(".")), + + // Add an optional flag called "port" (usage: --port) which will default to 3000. + cli.AddFlag("port", "Port to run server.", cli.SetFlagDefault(3000)), + + // Register a handler for this command. + // If no handler is registered, it will simply print help and exit. + cli.SetHandler(func(ctx context.Context) error { + // Extract the value of the "root" argument. + root, _ := cli.ArgValue[string](ctx, "root") + + // Extract the value of the "port" flag. + port, _ := cli.FlagValue[int](ctx, "port") + + addr := fmt.Sprintf(":%d", port) + return http.ListenAndServe(addr, http.FileServer(http.Dir(root))) + }), + ) +} + +func Example_kitchen_sink() { + // Create a new command + cmd, err := cli.NewCommand("git", "Modern version control.", + // Set command version + cli.SetVersion("2.37.0"), + + // Add a "--version" flag with a short flag "-V" for printing the command version + cli.AddVersionFlag(cli.AddFlagShort('V')), + + // Add a "--help" flag + cli.AddHelpFlag( + + // Add a short flag "-h" to help + cli.AddFlagShort('h'), + + // Make this flag inherited by sub-commands + cli.SetFlagIsInherited(true), + ), + + // Add a hidden "--debug" flag + cli.AddFlag("debug", "Enable debugging", + cli.SetFlagDefault(false), // Default parser for flags is cli.StringParser + + // Make it hidden + cli.SetFlagIsHidden(true), + ), + + // Add a sub-command "clone" + cli.AddSubCmd("clone", "Clone a repository.", + + // Add a required argument "" + cli.AddArg("url", "Repository to clone.", + + // Parse it into a *url.URL + cli.SetArgParser(cli.URLParser), + ), + + // Add optional argument "" + cli.AddArg("dir", "Directory to clone repo into.", + + // Set its default value to "." + cli.SetArgDefault("."), + ), + + // Add a flag "--verbose" + cli.AddFlag("verbose", "Be more verbose.", + + // Add a short "-v" + cli.AddFlagShort('v'), + + // Make it a boolean that defaults to false + cli.SetFlagDefault(false), + ), + ), + ) + if err != nil { + cli.ExitWithError(err) + } + + // Pass in your `context.Context` and args + if err := cmd.Run(context.TODO(), os.Args[1:]); err != nil { + cli.ExitWithError(err) + } +} diff --git a/help.go b/help.go index 9b38aa5..ec4d214 100644 --- a/help.go +++ b/help.go @@ -77,11 +77,16 @@ func (h helpContext) ArgumentList() string { func (h helpContext) ArgumentTable() (string, error) { return tableToString(lo.Map(h.Arguments(), func(argument *Argument, _ int) []string { + valueInfo := fmt.Sprintf("(type: %T)", argument.parser.Type()) + if argument.isOptional() { + valueInfo = fmt.Sprintf("(type: %T, default: %q)", argument.parser.Type(), fmt.Sprint(argument.defaultValue)) + } + return []string{ "", argument.inBrackets(), argument.description, - fmt.Sprintf("(type: %T)", argument.parser.Type()), + valueInfo, } })) } diff --git a/help_test.go b/help_test.go index 0ba2eba..7b1b3db 100644 --- a/help_test.go +++ b/help_test.go @@ -29,6 +29,9 @@ func TestCommand_renderHelp(t *testing.T) { SetFlagDefaultAndParser(CustomType{Field: "field default"}, func(s string) (CustomType, error) { return CustomType{Field: s}, nil }), ), AddFlag("hidden-flag", "some hidden flag", SetFlagIsHidden(true)), + AddArg("another-arg", "another arg", + SetArgDefault(123), + ), AddArg("some-arg", "some arg", SetArgParser(TimeLayoutParser(time.RubyDate)), ), @@ -44,10 +47,11 @@ func TestCommand_renderHelp(t *testing.T) { test v1.2.3-rc10: test command Usage: - test [flags] + test [flags] Arguments: - some arg (type: time.Time) + some arg (type: time.Time) + another arg (type: int, default: "123") Flags: --help Print help. (type: bool, default: "false")