Skip to content
Open
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
82 changes: 61 additions & 21 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Cli represents the structure of a CLI app. It should be constructed using the Ap
type Cli struct {
*Cmd
version *cliVersion
exiter func(code int) // REVIEW: might be desirable to have other options than just callback; the most common use I can imagine, other than `os.Exit`, is to simply capture the value again, for which a callback is both overkill and high friction.
stdOut io.Writer // REVIEW: I brought this along for the ride, because it was there at package scope before, but... it's never used, afaict?
stdErr io.Writer
}

type cliVersion struct {
Expand All @@ -34,15 +37,20 @@ name and description will be used to construct the help message for the app:

*/
func App(name, desc string) *Cli {
return &Cli{
Cmd: &Cmd{
name: name,
desc: desc,
optionsIdx: map[string]*container.Container{},
argsIdx: map[string]*container.Container{},
ErrorHandling: flag.ExitOnError,
},
cli := &Cli{
exiter: func(code int) { os.Exit(code) },
stdOut: os.Stdout,
stdErr: os.Stderr,
}
cli.Cmd = &Cmd{
cli: cli,
name: name,
desc: desc,
optionsIdx: map[string]*container.Container{},
argsIdx: map[string]*container.Container{},
ErrorHandling: flag.ExitOnError,
}
return cli
}

/*
Expand All @@ -65,6 +73,44 @@ func (cli *Cli) Version(name, version string) {
cli.version = &cliVersion{version, option}
}

/*
SetStdout sets the CLI's concept of what is "standard out".

If SetStdout is not called, the default behavior is to use os.Stdout.

This is currently unused.
*/
func (cli *Cli) SetStdout(wr io.Writer) {
cli.stdOut = wr
}

/*
SetStderr sets the CLI's concept of what is "standard error".

If SetStderr is not called, the default behavior is to use os.Stderr.

Information about parse errors is written to this stream,
as well as usage info or version info, if those are requested.
*/
func (cli *Cli) SetStderr(wr io.Writer) {
cli.stdErr = wr
}

/*
SetExiter sets a callback to define what happens when an exit should happen with an exit code.

If SetExiter is not called, the default behavior is to call os.Exit
(which immediately halts the program).

Common uses of setting a custom exit function include gathering the code instead of halting the program
(which is often useful for writing tests of CLI behavior, for example).

SetExiter should not be used for cleanup hooks; use a Cmd.After callback for that.
*/
func (cli *Cli) SetExiter(exiter func(code int)) {
cli.exiter = exiter
}

func (cli *Cli) parse(args []string, entry, inFlow, outFlow *flow.Step) error {
// We overload Cmd.parse() and handle cases that only apply to the CLI command, like versioning
// After that, we just call Cmd.parse() for the default behavior
Expand All @@ -86,7 +132,7 @@ In most cases the library users won't need to call this method, unless
a more complex validation is needed.
*/
func (cli *Cli) PrintVersion() {
fmt.Fprintln(stdErr, cli.version.version)
fmt.Fprintln(cli.stdErr, cli.version.version)
}

/*
Expand All @@ -96,12 +142,12 @@ and to execute the matching command.
In case of an incorrect usage, and depending on the configured ErrorHandling policy,
it may return an error, panic or exit
*/
func (cli *Cli) Run(args []string) error {
func (cli *Cli) Run(args []string) error { // REVIEW: I would actually prefer this returned `(error, int)`... but, that's a breaking change. We could also: use special error types for code; or, introduce a new function; or, do nothing, and require users to write a capturing thunk for an `exiter`.
if err := cli.doInit(); err != nil {
panic(err)
}
inFlow := &flow.Step{Desc: "RootIn", Exiter: exiter}
outFlow := &flow.Step{Desc: "RootOut", Exiter: exiter}
inFlow := &flow.Step{Desc: "RootIn", Exiter: cli.exiter}
outFlow := &flow.Step{Desc: "RootOut", Exiter: cli.exiter}
return cli.parse(args[1:], inFlow, inFlow, outFlow)
}

Expand All @@ -119,16 +165,10 @@ func ActionCommand(action func()) CmdInitializer {
/*
Exit causes the app the exit with the specified exit code while giving the After interceptors a chance to run.
This should be used instead of os.Exit.

This function is implemented using a panic; nothing will occur after it is called
(except other deferred functions, and the After intercepters).
*/
func Exit(code int) {
panic(flow.ExitCode(code))
}

var exiter = func(code int) {
os.Exit(code)
}

var (
stdOut io.Writer = os.Stdout
stdErr io.Writer = os.Stderr
)
Loading