diff --git a/.rumdl.toml b/.rumdl.toml deleted file mode 100644 index 509c326..0000000 --- a/.rumdl.toml +++ /dev/null @@ -1,34 +0,0 @@ -# rumdl configuration file - -# Global configuration options -[global] -# List of rules to disable (uncomment and modify as needed) -disable = [ - "MD033", # No inline html - "MD013", # Line length -] - -# List of rules to enable exclusively (if provided, only these rules will run) -# enable = ["MD001", "MD003", "MD004"] - -# List of file/directory patterns to include for linting (if provided, only these will be linted) -# include = [ -# "docs/*.md", -# "src/**/*.md", -# "README.md" -# ] - -# List of file/directory patterns to exclude from linting -exclude = [ - # Common directories to exclude - ".git", - ".github", - "node_modules", - "vendor", - "dist", - "build", - - # Specific files or patterns - "CHANGELOG.md", - "LICENSE.md", -] diff --git a/README.md b/README.md index 6a445d4..7a322f6 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ go get go.followtheprocess.codes/cli@latest package main import ( + "context" "fmt" "os" @@ -77,7 +78,7 @@ func run() error { cli.Example("Do a thing", "quickstart something"), cli.Example("Count the things", "quickstart something --count 3"), cli.Flag(&count, "count", 'c', 0, "Count the things"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Stdout(), "Hello from quickstart!, my args were: %v, count was %d\n", cmd.Args(), count) return nil }), @@ -86,7 +87,7 @@ func run() error { return err } - return cmd.Execute() + return cmd.Execute(context.Background()) } ``` @@ -107,9 +108,9 @@ To create CLI commands, you simply call `cli.New`: cmd, err := cli.New( "name", // The name of your command cli.Short("A new command") // Shown in the help - cli.Run(func(cmd *cli.Command, args []string) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { // This function is what your command does - fmt.Printf("name called with args: %v\n", args) + fmt.Printf("name called with args: %v\n", cmd.Args()) return nil }) ) @@ -124,7 +125,7 @@ To add a subcommand underneath the command you've just created, it's again `cli. ```go // Best to abstract it into a function -func buildSubcommand() (*cli.Command, error) { +func buildSubcommand(ctx context.Context) (*cli.Command, error) { return cli.New( "sub", // Name of the sub command e.g. 'clone' for 'git clone' cli.Short("A sub command"), @@ -136,11 +137,13 @@ func buildSubcommand() (*cli.Command, error) { And add it to your parent command: ```go +ctx := context.Background() + // From the example above cmd, err := cli.New( "name", // The name of your command // ... - cli.SubCommands(buildSubcommand), + cli.SubCommands(buildSubcommand(ctx)), ) ``` @@ -151,6 +154,8 @@ This pattern can be repeated recursively to create complex command structures. Flags in `cli` are generic, that is, there is *one* way to add a flag to your command, and that's with the `cli.Flag` option to `cli.New` ```go +// These will get set at command line parse time +// based on your flags type options struct { name string force bool @@ -167,7 +172,7 @@ func buildCmd() (*cli.Command, error) { cli.Flag(&options.force, "force", cli.NoShortHand, false, "Force delete without confirmation"), cli.Flag(&options.size, "size", 's', 0, "Size of something"), cli.Flag(&options.items, "items", 'i', nil, "Items to include"), - cli.Run(runCmd(&options)), // Pass the parsed flag values to your command run function + // ... ) } ``` @@ -220,7 +225,7 @@ There are two approaches to positional arguments in `cli`, you can either just g cli.New( "my-command", // ... - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Stdout(), "Hello! My arguments were: %v\n", cmd.Args()) return nil }) diff --git a/Taskfile.yml b/Taskfile.yml index 348bd36..967d4a9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -26,16 +26,12 @@ tasks: preconditions: - sh: command -v golangci-lint msg: golangci-lint not installed, see https://golangci-lint.run/usage/install/#local-installation - - - sh: command -v rumdl - msg: rumdl not installed, see https://github.com/rvben/rumdl#installation sources: - "**/*.go" - .golangci.yml - "**/*.md" cmds: - golangci-lint fmt ./... - - rumdl fmt --fix . test: desc: Run the test suite @@ -73,12 +69,8 @@ tasks: - sh: command -v typos msg: requires typos-cli, run `brew install typos-cli` - - - sh: command -v rumdl - msg: rumdl not installed, see https://github.com/rvben/rumdl#installation cmds: - golangci-lint run --fix - - rumdl check --fix . - typos - nilaway ./... diff --git a/command.go b/command.go index 7d1ceb3..d7673e4 100644 --- a/command.go +++ b/command.go @@ -2,6 +2,7 @@ package cli // import "go.followtheprocess.codes/cli" import ( + "context" "errors" "fmt" "io" @@ -116,7 +117,7 @@ type Command struct { // run is the function actually implementing the command, the command is passed into the function for access // to things like cmd.Stdout(). - run func(cmd *Command) error + run func(ctx context.Context, cmd *Command) error // flags is the set of flags for this command. flags *flag.Set @@ -184,7 +185,7 @@ type example struct { // // If the flags fail to parse, an error will be returned and the Run function // will not be called. -func (cmd *Command) Execute() error { +func (cmd *Command) Execute(ctx context.Context) error { if cmd == nil { return errors.New("Execute called on a nil Command") } @@ -265,7 +266,7 @@ func (cmd *Command) Execute() error { // If the command is runnable, go and execute its run function if cmd.run != nil { - return cmd.run(cmd) + return cmd.run(ctx, cmd) } // The only way we get here is if the command has subcommands defined but got no arguments given to it diff --git a/command_test.go b/command_test.go index 6f1af28..c8bcbaf 100644 --- a/command_test.go +++ b/command_test.go @@ -2,6 +2,7 @@ package cli_test import ( "bytes" + "context" goflag "flag" "fmt" "io" @@ -99,7 +100,7 @@ func TestExecute(t *testing.T) { options := []cli.Option{ cli.Stdout(stdout), cli.Stderr(stderr), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Stdout(), "My arguments were: %v\nForce was: %v\n", cmd.Args(), force) return nil @@ -110,7 +111,7 @@ func TestExecute(t *testing.T) { cmd, err := cli.New("test", slices.Concat(options, tt.options)...) test.Ok(t, err) - err = cmd.Execute() + err = cmd.Execute(t.Context()) test.WantErr(t, err, tt.wantErr) test.Equal(t, stdout.String(), tt.stdout) @@ -271,7 +272,7 @@ func TestSubCommandExecute(t *testing.T) { defaultOpts := []cli.Option{ cli.Flag(&force, "force", 'f', false, "Force for sub1"), cli.Flag(&something, "something", 's', "", "Something for sub1"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { if something == "" { something = "" } @@ -303,7 +304,7 @@ func TestSubCommandExecute(t *testing.T) { sub2 := func() (*cli.Command, error) { defaultOpts := []cli.Option{ - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf( cmd.Stdout(), "Hello from sub2, my args were: %v, delete was %v, number was %d", @@ -337,7 +338,7 @@ func TestSubCommandExecute(t *testing.T) { test.Ok(t, err) // Execute the command, we should see the sub commands get executed based on what args we provide - err = root.Execute() + err = root.Execute(t.Context()) test.WantErr(t, err, tt.wantErr) if !tt.wantErr { @@ -353,7 +354,7 @@ func TestHelp(t *testing.T) { return cli.New( "sub1", cli.Short("Do one thing"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub1") return nil @@ -364,7 +365,7 @@ func TestHelp(t *testing.T) { return cli.New( "sub2", cli.Short("Do another thing"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub2") return nil @@ -376,7 +377,7 @@ func TestHelp(t *testing.T) { return cli.New( "very-long-subcommand", cli.Short("Wow so long"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub3") return nil @@ -393,7 +394,7 @@ func TestHelp(t *testing.T) { name: "default long", options: []cli.Option{ cli.OverrideArgs([]string{"--help"}), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -401,7 +402,7 @@ func TestHelp(t *testing.T) { name: "default short", options: []cli.Option{ cli.OverrideArgs([]string{"-h"}), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -411,7 +412,7 @@ func TestHelp(t *testing.T) { cli.OverrideArgs([]string{"--help"}), cli.Example("Do a thing", "test do thing --now"), cli.Example("Do a different thing", "test do thing --different"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -422,7 +423,7 @@ func TestHelp(t *testing.T) { cli.Arg(new(string), "src", "The file to copy"), // This one is required cli.Arg(new(string), "dest", "Destination to copy to", cli.ArgDefault("default.txt")), // This one is optional cli.Arg(new(int), "other", "Something else", cli.ArgDefault(0)), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -433,7 +434,7 @@ func TestHelp(t *testing.T) { cli.Arg(new(string), "src", "The file to copy"), // This one is required cli.Arg(new(string), "dest", "Destination to copy to", cli.ArgDefault("destination.txt")), // This one is optional cli.Flag(new(flag.Count), "verbosity", 'v', 0, "Increase the verbosity level"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -443,7 +444,7 @@ func TestHelp(t *testing.T) { cli.OverrideArgs([]string{"--help"}), cli.Short("A cool CLI to do things"), cli.Long("A longer, probably multiline description"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -453,7 +454,7 @@ func TestHelp(t *testing.T) { cli.OverrideArgs([]string{"--help"}), cli.Short(" \t\n A cool CLI to do things \n "), cli.Long(" \t\n\n A longer, probably multiline description \t\n\n "), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -461,7 +462,7 @@ func TestHelp(t *testing.T) { name: "with no description", options: []cli.Option{ cli.OverrideArgs([]string{"--help"}), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, wantErr: false, }, @@ -519,7 +520,7 @@ func TestHelp(t *testing.T) { test.Ok(t, err) - err = cmd.Execute() + err = cmd.Execute(t.Context()) test.WantErr(t, err, tt.wantErr) if *debug { @@ -541,7 +542,7 @@ func TestVersion(t *testing.T) { "sub1", cli.Short("Do one thing"), // No version set on sub1 - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub1") return nil @@ -553,7 +554,7 @@ func TestVersion(t *testing.T) { "sub2", cli.Short("Do another thing"), cli.Version("sub2 version text"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub2") return nil @@ -571,7 +572,7 @@ func TestVersion(t *testing.T) { name: "default long", options: []cli.Option{ cli.OverrideArgs([]string{"--version"}), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, stderr: "version-test\n\nVersion: dev\n", wantErr: false, @@ -580,7 +581,7 @@ func TestVersion(t *testing.T) { name: "default short", options: []cli.Option{ cli.OverrideArgs([]string{"-V"}), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, stderr: "version-test\n\nVersion: dev\n", wantErr: false, @@ -590,7 +591,7 @@ func TestVersion(t *testing.T) { options: []cli.Option{ cli.OverrideArgs([]string{"--version"}), cli.Version("v3.1.7"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, stderr: "version-test\n\nVersion: v3.1.7\n", wantErr: false, @@ -600,7 +601,7 @@ func TestVersion(t *testing.T) { options: []cli.Option{ cli.OverrideArgs([]string{"--version"}), cli.Commit("eedb45b"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, stderr: "version-test\n\nVersion: dev\nCommit: eedb45b\n", wantErr: false, @@ -610,7 +611,7 @@ func TestVersion(t *testing.T) { options: []cli.Option{ cli.OverrideArgs([]string{"--version"}), cli.BuildDate("2024-04-11T02:23:42Z"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, stderr: "version-test\n\nVersion: dev\nBuildDate: 2024-04-11T02:23:42Z\n", wantErr: false, @@ -621,7 +622,7 @@ func TestVersion(t *testing.T) { cli.OverrideArgs([]string{"--version"}), cli.Version("v8.17.6"), cli.Commit("b9aaafd"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, stderr: "version-test\n\nVersion: v8.17.6\nCommit: b9aaafd\n", wantErr: false, @@ -633,7 +634,7 @@ func TestVersion(t *testing.T) { cli.Version("v8.17.6"), cli.Commit("b9aaafd"), cli.BuildDate("2024-08-17T10:37:30Z"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, stderr: "version-test\n\nVersion: v8.17.6\nCommit: b9aaafd\nBuildDate: 2024-08-17T10:37:30Z\n", wantErr: false, @@ -646,7 +647,7 @@ func TestVersion(t *testing.T) { cli.Commit("b9aaafd"), cli.BuildDate("2024-08-17T10:37:30Z"), cli.SubCommands(sub1, sub2), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, // Should show the root commands version info stderr: "sub1\n\nVersion: v8.17.6\nCommit: b9aaafd\nBuildDate: 2024-08-17T10:37:30Z\n", @@ -660,7 +661,7 @@ func TestVersion(t *testing.T) { cli.Commit("b9aaafd"), cli.BuildDate("2024-08-17T10:37:30Z"), cli.SubCommands(sub1, sub2), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), }, // Should show sub2's version text stderr: "sub2\n\nVersion: sub2 version text\n", @@ -683,7 +684,7 @@ func TestVersion(t *testing.T) { cmd, err := cli.New("version-test", slices.Concat(options, tt.options)...) test.Ok(t, err) - err = cmd.Execute() + err = cmd.Execute(t.Context()) test.WantErr(t, err, tt.wantErr) // Should have no output to stdout @@ -792,21 +793,21 @@ func TestDuplicateSubCommands(t *testing.T) { sub1 := func() (*cli.Command, error) { return cli.New( "sub1", - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), ) } sub2 := func() (*cli.Command, error) { return cli.New( "sub2", - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), ) } sub1Again := func() (*cli.Command, error) { return cli.New( "sub1", - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), ) } @@ -834,7 +835,7 @@ func TestCommandNoRunNoSub(t *testing.T) { func TestExecuteNilCommand(t *testing.T) { var cmd *cli.Command - err := cmd.Execute() + err := cmd.Execute(t.Context()) test.Err(t, err) if err != nil { @@ -860,7 +861,7 @@ func TestCommandOptionOrder(t *testing.T) { sub := func() (*cli.Command, error) { return cli.New( "sub", - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub") return nil @@ -873,7 +874,7 @@ func TestCommandOptionOrder(t *testing.T) { cli.Short("Short description"), cli.Long("Long description"), cli.Example("Do a thing", "demo run something --flag"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Stdout(), "args: %v, flag: %v, count: %v\n", cmd.Args(), f, count) return nil @@ -897,7 +898,7 @@ func TestCommandOptionOrder(t *testing.T) { baseline, err := cli.New("baseline", baseLineOptions...) test.Ok(t, err) - err = baseline.Execute() + err = baseline.Execute(t.Context()) test.Ok(t, err) // Make sure the baseline is behaving as expected @@ -922,7 +923,7 @@ func TestCommandOptionOrder(t *testing.T) { test.Ok(t, err) // The two commands should behave equivalently - err = shuffle.Execute() + err = shuffle.Execute(t.Context()) test.Ok(t, err) test.Equal(t, shuffleStdout.String(), baseLineStdout.String()) @@ -939,7 +940,7 @@ func BenchmarkExecuteHelp(b *testing.B) { return cli.New( "sub1", cli.Short("Do one thing"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub1") return nil @@ -951,7 +952,7 @@ func BenchmarkExecuteHelp(b *testing.B) { return cli.New( "sub2", cli.Short("Do another thing"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub2") return nil @@ -963,7 +964,7 @@ func BenchmarkExecuteHelp(b *testing.B) { return cli.New( "very-long-subcommand", cli.Short("Wow so long"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from sub3") return nil @@ -984,7 +985,7 @@ func BenchmarkExecuteHelp(b *testing.B) { test.Ok(b, err) for b.Loop() { - err := cmd.Execute() + err := cmd.Execute(b.Context()) if err != nil { b.Fatalf("Execute returned an error: %v", err) } @@ -1003,7 +1004,7 @@ func BenchmarkNew(b *testing.B) { cli.Flag(new(bool), "force", 'f', false, "Force something"), cli.Flag(new(string), "name", 'n', "", "The name of something"), cli.Flag(new(int), "count", 'c', 1, "Count something"), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), ) if err != nil { b.Fatal(err) @@ -1021,12 +1022,12 @@ func BenchmarkVersion(b *testing.B) { cli.OverrideArgs([]string{"--version"}), cli.Stderr(io.Discard), cli.Stdout(io.Discard), - cli.Run(func(cmd *cli.Command) error { return nil }), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { return nil }), ) test.Ok(b, err) for b.Loop() { - err := cmd.Execute() + err := cmd.Execute(b.Context()) if err != nil { b.Fatalf("Execute returned an error: %v", err) } diff --git a/docs/img/cancel.gif b/docs/img/cancel.gif new file mode 100644 index 0000000..1dcd367 Binary files /dev/null and b/docs/img/cancel.gif differ diff --git a/docs/img/demo.png b/docs/img/demo.png index 0f99c2a..70e8fb9 100644 Binary files a/docs/img/demo.png and b/docs/img/demo.png differ diff --git a/docs/img/namedargs.gif b/docs/img/namedargs.gif index a5d5ae8..2cdc07a 100644 Binary files a/docs/img/namedargs.gif and b/docs/img/namedargs.gif differ diff --git a/docs/img/quickstart.gif b/docs/img/quickstart.gif index 98b9732..cc4176c 100644 Binary files a/docs/img/quickstart.gif and b/docs/img/quickstart.gif differ diff --git a/docs/img/subcommands.gif b/docs/img/subcommands.gif index e70d455..53b3d81 100644 Binary files a/docs/img/subcommands.gif and b/docs/img/subcommands.gif differ diff --git a/docs/src/cancel.tape b/docs/src/cancel.tape new file mode 100644 index 0000000..4b749c9 --- /dev/null +++ b/docs/src/cancel.tape @@ -0,0 +1,42 @@ +Output ./docs/img/cancel.gif + +Set FontSize 18 +Set FontFamily "Geist Mono" +Set Theme "Catppuccin Macchiato" +Set WindowBar Colorful +Set Padding 5 +Set Height 800 +Set Width 1200 +Set Margin 40 +Set MarginFill "#7983FF" +Set BorderRadius 10 +Set TypingSpeed 100ms + +# Setup +Hide +Type "go build -o ./cancel ./examples/cancel && clear" Sleep 2s +Enter +Sleep 2s +Show +Sleep 1s + +Type "./cancel --help" Sleep 500ms Enter + +Sleep 6s + +Type "clear" Sleep 500ms Enter + +Sleep 1s + +Type "./cancel" Sleep 500ms Enter + +Sleep 8s + +Ctrl+c + +Sleep 1s + +# Cleanup +Hide +Type "rm -f ./cancel" +Enter diff --git a/examples/README.md b/examples/README.md index 5fa53c4..2c854f2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,6 +7,7 @@ This directory contains a bunch of example programs built with `cli` to show you - [`./quickstart`](#quickstart) - [`./subcommands`](#subcommands) - [`./namedargs`](#namedargs) + - [`./cancel`](#cancel) - [TODO](#todo) ## `./cover` @@ -31,6 +32,12 @@ A CLI with named positional arguments that may or may not have default values. S ![namedargs](../docs/img/namedargs.gif) +## `./cancel` + +This examples shows how `cli` requiring you to pass a `context.Context` to your run functions leads to elegant and resilient cancellation and `CTRL+C` handling. + +![cancel](../docs/img/cancel.gif) + ### TODO - Replicate one or two well known CLI tools as an example diff --git a/examples/cancel/cli.go b/examples/cancel/cli.go new file mode 100644 index 0000000..73c1d35 --- /dev/null +++ b/examples/cancel/cli.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "fmt" + "time" + + "go.followtheprocess.codes/cli" +) + +func BuildCLI() (*cli.Command, error) { + return cli.New( + "cancel", + cli.Short("Cancel me!"), + cli.Run(func(ctx context.Context, cmd *cli.Command) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1 * time.Second): + fmt.Fprintln(cmd.Stdout(), "working...") + } + } + }), + ) +} diff --git a/examples/cancel/main.go b/examples/cancel/main.go new file mode 100644 index 0000000..4ce1f42 --- /dev/null +++ b/examples/cancel/main.go @@ -0,0 +1,31 @@ +// Package cancel demonstrates how to handle CTRL+C and cancellation/timeouts +// easily with cli. +package main + +import ( + "context" + "fmt" + "os" + "os/signal" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + ctx := context.Background() + + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() + + cli, err := BuildCLI() + if err != nil { + return err + } + + return cli.Execute(ctx) +} diff --git a/examples/cover/main.go b/examples/cover/main.go index c5b94a1..fe9eb13 100644 --- a/examples/cover/main.go +++ b/examples/cover/main.go @@ -2,6 +2,7 @@ package main import ( + "context" "fmt" "log" "os" @@ -20,7 +21,7 @@ func main() { cli.Stdout(os.Stdout), cli.Example("Do a thing", "demo thing --count"), cli.Flag(&count, "count", 'c', 0, "Count the thing"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintln(cmd.Stdout(), "Hello from demo, my arguments were: ", cmd.Args()) return nil }), @@ -29,7 +30,10 @@ func main() { log.Fatalln(err) } - if err := cmd.Execute(); err != nil { + // Good command line tools allow for timeouts, cancellations etc. + // so in cli, you pass a context.Context to your root command, and + // it gets passed down to your Run function. + if err := cmd.Execute(context.Background()); err != nil { log.Fatalln(err) } } diff --git a/examples/namedargs/main.go b/examples/namedargs/main.go index 7dd5415..651b4c8 100644 --- a/examples/namedargs/main.go +++ b/examples/namedargs/main.go @@ -2,6 +2,7 @@ package main import ( + "context" "fmt" "os" @@ -31,7 +32,7 @@ func run() error { cli.Arg(&arguments.dest, "dest", "The file to copy to", cli.ArgDefault("dest.txt")), cli.Example("Copy a file to somewhere", "copy src.txt ./some/where/else"), cli.Example("Use the default destination", "copy src.txt"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Stdout(), "Copying from %s to %s\n", arguments.src, arguments.dest) return nil }), @@ -40,5 +41,5 @@ func run() error { return err } - return cmd.Execute() + return cmd.Execute(context.Background()) } diff --git a/examples/quickstart/main.go b/examples/quickstart/main.go index f166f9e..f6041c6 100644 --- a/examples/quickstart/main.go +++ b/examples/quickstart/main.go @@ -2,6 +2,7 @@ package main import ( + "context" "fmt" "os" @@ -29,7 +30,7 @@ func run() error { cli.Example("Do a thing", "quickstart something"), cli.Example("Count the things", "quickstart something --count 3"), cli.Flag(&count, "count", 'c', 0, "Count the things"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(cmd.Stdout(), "Hello from quickstart!, my args were: %v, count was %d\n", cmd.Args(), count) return nil }), @@ -38,5 +39,5 @@ func run() error { return err } - return cmd.Execute() + return cmd.Execute(context.Background()) } diff --git a/examples/subcommands/cli.go b/examples/subcommands/cli.go index a5ad251..b37d2c5 100644 --- a/examples/subcommands/cli.go +++ b/examples/subcommands/cli.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "net/url" "strings" @@ -42,7 +43,7 @@ func buildSayCommand() (*cli.Command, error) { cli.Flag(&options.count, "count", 'c', 0, "Count the things"), cli.Flag(&options.thing, "thing", 't', "", "Name of the thing"), cli.Flag(&options.items, "item", 'i', nil, "Items to add to a list"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { if options.shout { for _, arg := range cmd.Args() { fmt.Fprintln(cmd.Stdout(), strings.ToUpper(arg), " ") @@ -91,7 +92,7 @@ func buildDoCommand() (*cli.Command, error) { cli.Flag(&options.fast, "fast", 'f', false, "Do the thing quickly"), cli.Flag(&options.verbosity, "verbosity", 'v', 0, "Increase the verbosity level"), cli.Flag(&options.duration, "duration", 'd', 1*time.Second, "Do the thing for a specific duration"), - cli.Run(func(cmd *cli.Command) error { + cli.Run(func(ctx context.Context, cmd *cli.Command) error { if options.fast { fmt.Fprintf( cmd.Stdout(), diff --git a/examples/subcommands/main.go b/examples/subcommands/main.go index 683c78c..525d7cb 100644 --- a/examples/subcommands/main.go +++ b/examples/subcommands/main.go @@ -2,6 +2,7 @@ package main import ( + "context" "fmt" "os" ) @@ -19,7 +20,7 @@ func run() error { return fmt.Errorf("could not build root command: %w", err) } - if err := cmd.Execute(); err != nil { + if err := cmd.Execute(context.Background()); err != nil { return fmt.Errorf("could not execute root command: %w", err) } diff --git a/go.mod b/go.mod index a53f431..faea645 100644 --- a/go.mod +++ b/go.mod @@ -15,5 +15,5 @@ require ( require ( golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.36.0 // indirect + golang.org/x/term v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index fae3612..d76fedc 100644 --- a/go.sum +++ b/go.sum @@ -6,7 +6,7 @@ go.followtheprocess.codes/test v1.0.0 h1:5m2MPOQpohDC9pf5hgqpH+4ldJP5g+YFVdoGQY4 go.followtheprocess.codes/test v1.0.0/go.mod h1:e627pR8IhsTV/RfuP/WKYjyL0BmuIbmaw2iKlQBCWrY= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= diff --git a/option.go b/option.go index 4f1940d..b8a3f22 100644 --- a/option.go +++ b/option.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "fmt" "io" @@ -35,7 +36,7 @@ type config struct { stdin io.Reader stdout io.Writer stderr io.Writer - run func(cmd *Command) error + run func(ctx context.Context, cmd *Command) error flags *internalflag.Set parent *Command name string @@ -257,7 +258,7 @@ func Example(comment, command string) Option { // want it to do when invoked. // // Successive calls overwrite previous ones. -func Run(run func(cmd *Command) error) Option { +func Run(run func(ctx context.Context, cmd *Command) error) Option { f := func(cfg *config) error { if run == nil { return errors.New("cannot set Run to nil")