feat(bonfire): add FlagBool and read every bare switch with it (IN-04, quick-261028-rqd)
This commit is contained in:
@@ -25,7 +25,7 @@ Use cases:
|
||||
- Command values with a description, positional arguments (`bonfire.Arg`) and string flags (`bonfire.Flag`), collected from plugins or the tool itself.
|
||||
- Command name validation: plugin commands must use the `namespace:verb` form (for example `blog:import`); `build`, `dev`, `new`, `serve` and `migrate` are the only bare names accepted. Invalid names make `bonfire.NewRoot` fail with `bonfire.ErrCommandName`.
|
||||
- Usage strings and argument-count checks derived from the declared arguments (`<name>` for required, `[name]` for optional).
|
||||
- Scalar flags, bare flags (`bonfire.Flag.Bare`, so `--force` alone stores `true`) and ordered repeatable flags (`bonfire.Flag.Repeatable`, read back through `bonfire.Input.Flags`).
|
||||
- Scalar flags, bare flags (`bonfire.Flag.Bare`, so `--force` alone stores `true`, read as a switch with `bonfire.FlagBool`, which also accepts `--force=1` and `--force=false`) and ordered repeatable flags (`bonfire.Flag.Repeatable`, read back through `bonfire.Input.Flags`).
|
||||
- Styled status lines: `bonfire.Output.Info`, `bonfire.Output.Success`, `bonfire.Output.Warning` and `bonfire.Output.Error` (the last one writes to the error stream).
|
||||
- Widgets on the charm stack, ported from SSU's terminal UI: box-drawn tables (`bonfire.Output.Table`, lipgloss), a spinner around a function (`bonfire.Output.Spinner`), a gradient progress bar (`bonfire.Output.Progress`, whose `bonfire.Progress` also implements `bonfire.ItemProgress`) and a multi-row process view for bulk loops (`bonfire.Process`), each a bubbletea program that keeps stdin and signals to the command: Ctrl+C or SIGTERM during a view restores the cursor and the terminal, then stops the command as before (or reaches its own signal handler). Log lines, and output the command prints through the console that started the view, appear above a running view (`bonfire.PrintAbove`); other consoles (a `bonfire.Call` buffer, a console on a file) keep writing to their own writers. When the context given to `bonfire.Process` is cancelled, the view ends with its last frame and later lines print plainly below it. Table cells, the item names and errors of `bonfire.Process` (in the process view and in its plain lines), the process view's operation label, and failure lines are stripped of control characters (`bonfire.Sanitize`).
|
||||
- Prompts: `bonfire.Output.Ask`, `bonfire.Output.Confirm` and `bonfire.Output.Secret` use huh fields on an interactive terminal, and `bonfire.Output.Choice` an arrow-key selector with a confirmation screen. Off a terminal they read lines from stdin as before (a piped password still works), return their defaults when input ends, and `bonfire.Output.Confirm` returns its default without asking. A cancelled prompt returns `bonfire.ErrAborted`. `bonfire.ConfirmProceed` asks before a destructive step only on an interactive terminal.
|
||||
@@ -74,7 +74,7 @@ func main() {
|
||||
},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
url, _ := in.Argument("url")
|
||||
_, dryRun := in.Flag("dry-run")
|
||||
dryRun := bonfire.FlagBool(in, "dry-run")
|
||||
return out.Spinner("Importing "+url, func() error {
|
||||
out.Table([]string{"Tag"}, [][]string{{"news"}})
|
||||
if dryRun {
|
||||
@@ -117,6 +117,7 @@ if catalog.Has("blog:import") {
|
||||
| `bonfire.Flag` | A string flag; `bonfire.Flag.Bare` allows the flag without a value, `bonfire.Flag.Repeatable` makes it an ordered multi-value flag. |
|
||||
| `bonfire.Arg` | A positional argument with a name, description and required marker. |
|
||||
| `bonfire.Input` | Parsed view handed to `bonfire.Command.Run`: `bonfire.Input.Args`, `bonfire.Input.Argument`, `bonfire.Input.Flag` and `bonfire.Input.Flags`. |
|
||||
| `bonfire.FlagBool` | Reports whether a bare switch is on: set and true under `strconv.ParseBool` (`--x`, `--x=1`, `--x=true`); unset, empty or unparsable (`--x=yes`) values and a nil input are false. |
|
||||
| `bonfire.Output` | Injected console: printing, status lines, tables, spinner, progress bar and prompts. |
|
||||
| `bonfire.Progress` | A progress bar advanced from inside `bonfire.Output.Progress`. |
|
||||
| `bonfire.NewRoot` | Builds the Cobra root command for a binary from a slice of commands, using the process stdin, with the global options, the `list` command and the command list on a bare call; takes `bonfire.RootOption` values. When the output is the process stdout, the error block goes to stderr. |
|
||||
|
||||
@@ -3,6 +3,7 @@ package bonfire
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -134,6 +135,23 @@ func (in cobraInput) Flag(name string) (string, bool) {
|
||||
return val, true
|
||||
}
|
||||
|
||||
// FlagBool reports whether the bare switch name is on: the flag is set and
|
||||
// its value parses with strconv.ParseBool as true (--name, --name=true,
|
||||
// --name=1, --name=t, TRUE, True). Unset, empty and unparsable values
|
||||
// (--name=yes) are false, and a nil Input has no flags. Read every Bare
|
||||
// flag with it, so --json=1 works like --json.
|
||||
func FlagBool(in Input, name string) bool {
|
||||
if in == nil {
|
||||
return false
|
||||
}
|
||||
v, ok := in.Flag(name)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
on, err := strconv.ParseBool(v)
|
||||
return err == nil && on
|
||||
}
|
||||
|
||||
func validCommandName(name string) bool {
|
||||
switch name {
|
||||
case "build", "dev", "new", "serve", "migrate":
|
||||
|
||||
@@ -115,3 +115,40 @@ func TestHelpShowsLongOrDescription(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlagBool (IN-04): FlagBool reads a bare switch with
|
||||
// strconv.ParseBool; unset, empty or unparsable values are false, and a
|
||||
// nil Input has no flags.
|
||||
func TestFlagBool(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []string
|
||||
want bool
|
||||
}{
|
||||
{nil, false},
|
||||
{[]string{"--x"}, true},
|
||||
{[]string{"--x=1"}, true},
|
||||
{[]string{"--x=t"}, true},
|
||||
{[]string{"--x=TRUE"}, true},
|
||||
{[]string{"--x=True"}, true},
|
||||
{[]string{"--x=false"}, false},
|
||||
{[]string{"--x=0"}, false},
|
||||
{[]string{"--x="}, false},
|
||||
{[]string{"--x=yes"}, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
var got bool
|
||||
cmd := Command{Name: "acme:switch", Flags: []Flag{{Name: "x", Bare: true}}, Run: func(_ context.Context, in Input, _ Output) error {
|
||||
got = FlagBool(in, "x")
|
||||
return nil
|
||||
}}
|
||||
if err := Call(context.Background(), []Command{cmd}, "acme:switch", tc.args, nil); err != nil {
|
||||
t.Fatalf("%q: %v", tc.args, err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("FlagBool with %q = %v, want %v", tc.args, got, tc.want)
|
||||
}
|
||||
}
|
||||
if FlagBool(nil, "x") {
|
||||
t.Error("FlagBool(nil) = true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ func ExampleCatalog() {
|
||||
},
|
||||
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
||||
slug, _ := in.Argument("slug")
|
||||
dryRun, _ := in.Flag("dry-run")
|
||||
out.Printf("publish %s, tags %v, dry run %s\n", slug, in.Flags("tag"), dryRun)
|
||||
dryRun := bonfire.FlagBool(in, "dry-run")
|
||||
out.Printf("publish %s, tags %v, dry run %v\n", slug, in.Flags("tag"), dryRun)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@ func usageName(name string) string {
|
||||
func (st *rootState) listCommand() Command {
|
||||
c := builtinCommand("list")
|
||||
c.Run = func(_ context.Context, in Input, _ Output) error {
|
||||
all, _ := in.Flag("all")
|
||||
all := FlagBool(in, "all")
|
||||
format, _ := in.Flag("format")
|
||||
switch format {
|
||||
case "", "txt":
|
||||
return st.renderList(st.globals(), all == "true")
|
||||
return st.renderList(st.globals(), all)
|
||||
case "json":
|
||||
return st.renderJSON()
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user