From 20ff302210c5606c9800df63b4fbe8e767eb85d9 Mon Sep 17 00:00:00 2001 From: Tom Fleet Date: Tue, 9 Jul 2024 17:53:52 +0100 Subject: [PATCH] Implement a very basic Command struct (#1) --- Taskfile.yml | 30 ++++++++++++++++------ cli.go | 7 ----- cli_test.go | 16 ------------ command.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ command_test.go | 33 ++++++++++++++++++++++++ go.mod | 9 ++++++- go.sum | 6 +++++ 7 files changed, 137 insertions(+), 32 deletions(-) delete mode 100644 cli.go delete mode 100644 cli_test.go create mode 100644 command.go create mode 100644 command_test.go create mode 100644 go.sum diff --git a/Taskfile.yml b/Taskfile.yml index e6f0fce..ae068a2 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -9,34 +9,47 @@ tasks: default: desc: List all available tasks silent: true - cmd: task --list + cmds: + - task --list tidy: desc: Tidy dependencies in go.mod and go.sum - cmd: go mod tidy + cmds: + - go mod tidy fmt: desc: Run go fmt on all source files - cmd: go fmt ./... + preconditions: + - sh: command -v golines + msg: golines not installed, see https://github.com/segmentio/golines + cmds: + - go fmt ./... + - golines . --chain-split-dots --ignore-generated --write-output test: desc: Run the test suite - cmd: go test -race ./... {{ .CLI_ARGS }} + cmds: + - go test -race ./... {{ .CLI_ARGS }} bench: desc: Run all project benchmarks - cmd: go test ./... -run None -benchmem -bench . {{ .CLI_ARGS }} + cmds: + - go test ./... -run None -benchmem -bench . {{ .CLI_ARGS }} lint: desc: Run the linters and auto-fix if possible - cmd: golangci-lint run --fix + deps: + - fmt + cmds: + - golangci-lint run --fix preconditions: - sh: command -v golangci-lint msg: golangci-lint not installed, see https://golangci-lint.run/usage/install/#local-installation doc: desc: Render the pkg docs locally - cmd: pkgsite -open + cmds: + - pkgsite -open preconditions: - sh: command -v pkgsite msg: pkgsite not installed, run go install golang.org/x/pkgsite/cmd/pkgsite@latest @@ -57,7 +70,8 @@ tasks: sloc: desc: Print lines of code - cmd: fd . -e go | xargs wc -l | sort -nr | head + cmds: + - fd . -e go | xargs wc -l | sort -nr | head clean: desc: Remove build artifacts and other clutter diff --git a/cli.go b/cli.go deleted file mode 100644 index 361dcd0..0000000 --- a/cli.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package cli is a placeholder for something cool. -package cli - -// Hello returns a welcome message for the project. -func Hello() string { - return "Hello cli" -} diff --git a/cli_test.go b/cli_test.go deleted file mode 100644 index aba115d..0000000 --- a/cli_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package cli_test - -import ( - "testing" - - "github.com/FollowTheProcess/cli" -) - -func TestHello(t *testing.T) { - got := cli.Hello() - want := "Hello cli" - - if got != want { - t.Errorf("got %s, wanted %s", got, want) - } -} diff --git a/command.go b/command.go new file mode 100644 index 0000000..ac9a70c --- /dev/null +++ b/command.go @@ -0,0 +1,68 @@ +// Package cli provides a tiny, simple and minimalistic CLI framework for building Go CLI tools. +package cli + +import ( + "fmt" + "io" + + "github.com/spf13/pflag" +) + +// Command represents a CLI command. +type Command struct { + // Run is the function actually implementing the command, the command and arguments to it, are passed into the function, flags + // are parsed out before the arguments are passed to Run, so `args` here are the command line arguments minus flags. + Run func(cmd *Command, args []string) error + + // flags is the set of flags for this command. + flags *pflag.FlagSet + + // Stdin is an [io.Reader] from which command input is read. + // + // It defaults to [os.Stdin] but can be overridden as desired e.g. for testing. + Stdin io.Reader + + // Stdout is an [io.Writer] to which normal command output is written. + // + // It defaults to [os.Stdout] but can be overridden as desired e.g. for testing. + Stdout io.Writer + + // Stderr is an [io.Writer] to which error command output is written. + // + // It defaults to [os.Stderr] but can be overridden as desired e.g. for testing. + Stderr io.Writer + + // Name is the name of the command. + Name string + + // Short is the one line summary for the command, shown inline in the -h/--help output. + Short string + + // Long is the long form description for the command, shown when -h/--help is called on the command itself. + Long string + + // Example is examples of how to use the command, free form text. + Example string +} + +// Execute parses the flags and arguments, and invokes the Command's Run +// function, returning any error. +// +// The arguments should not include the command name. +// +// If the flags fail to parse, an error will be returned and the Run function +// will not be called. +// +// err := cmd.Execute(os.Args[1:]) +func (c *Command) Execute(args []string) error { + if c.flags == nil { + c.flags = pflag.NewFlagSet(c.Name, pflag.ExitOnError) + } + if err := c.flags.Parse(args); err != nil { + return fmt.Errorf("failed to parse command flags: %w", err) + } + + argsWithoutFlags := c.flags.Args() + + return c.Run(c, argsWithoutFlags) +} diff --git a/command_test.go b/command_test.go new file mode 100644 index 0000000..9262443 --- /dev/null +++ b/command_test.go @@ -0,0 +1,33 @@ +package cli_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/FollowTheProcess/cli" + "github.com/FollowTheProcess/test" +) + +func TestExecute(t *testing.T) { + stderr := &bytes.Buffer{} + stdout := &bytes.Buffer{} + + testCmd := &cli.Command{ + Run: func(cmd *cli.Command, args []string) error { + fmt.Fprintf(cmd.Stdout, "Oooh look, it ran, here are some args: %v\n", args) + return nil + }, + Stdout: stdout, + Stderr: stderr, + Name: "test", + Short: "A simple test command", + Long: "Much longer description blah blah blah", + } + + err := testCmd.Execute([]string{"arg1", "arg2", "arg3"}) + test.Ok(t, err) + + want := "Oooh look, it ran, here are some args: [arg1 arg2 arg3]\n" + test.Equal(t, stdout.String(), want) +} diff --git a/go.mod b/go.mod index 0620da9..52e9982 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/FollowTheProcess/cli -go 1.22 \ No newline at end of file +go 1.22 + +require ( + github.com/FollowTheProcess/test v0.9.0 + github.com/spf13/pflag v1.0.5 +) + +require github.com/google/go-cmp v0.6.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e2a11ec --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/FollowTheProcess/test v0.9.0 h1:X7/cqUi6qwbB8Z8PGHI5Hk4RJJF/wRZ+3Mko3QlL/VU= +github.com/FollowTheProcess/test v0.9.0/go.mod h1:9oxZcKkTAgz3bZMiHPtYCytdPcvICS+AAp5mzZzB2oA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=