Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 101 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<url>"
cli.AddArg("url", "Repository to clone.",

// Parse it into a *url.URL
cli.SetArgParser(cli.URLParser),
),

// Add optional argument "<dir?>"
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
Expand Down
9 changes: 1 addition & 8 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"context"
"fmt"
"os"
"os/exec"
"strings"

"github.com/bobg/errors"
Expand Down Expand Up @@ -53,13 +52,7 @@
}

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)

Check warning on line 55 in command.go

View check run for this annotation

Codecov / codecov/patch

command.go#L55

Added line #L55 was not covered by tests
}
}

Expand Down
8 changes: 3 additions & 5 deletions command_test.go
Original file line number Diff line number Diff line change
@@ -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'),
Expand All @@ -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")
}
33 changes: 33 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -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)

Check warning on line 16 in error.go

View check run for this annotation

Codecov / codecov/patch

error.go#L15-L16

Added lines #L15 - L16 were not covered by tests
}

func ExitCode(code int) ExitError {
return ExitError{Code: code}

Check warning on line 20 in error.go

View check run for this annotation

Codecov / codecov/patch

error.go#L19-L20

Added lines #L19 - L20 were not covered by tests
}

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)
}

Check warning on line 32 in error.go

View check run for this annotation

Codecov / codecov/patch

error.go#L23-L32

Added lines #L23 - L32 were not covered by tests
}
103 changes: 103 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
@@ -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 "<url>"
cli.AddArg("url", "Repository to clone.",

// Parse it into a *url.URL
cli.SetArgParser(cli.URLParser),
),

// Add optional argument "<dir?>"
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)
}
}
7 changes: 6 additions & 1 deletion help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}))
}
Expand Down
8 changes: 6 additions & 2 deletions help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
),
Expand All @@ -44,10 +47,11 @@ func TestCommand_renderHelp(t *testing.T) {
test v1.2.3-rc10: test command

Usage:
test [flags] <some-arg>
test [flags] <some-arg> <another-arg?>

Arguments:
<some-arg> some arg (type: time.Time)
<some-arg> some arg (type: time.Time)
<another-arg?> another arg (type: int, default: "123")

Flags:
--help Print help. (type: bool, default: "false")
Expand Down