From 02a69fd7d556a8964b2f1a18a2dfd9c6aad50343 Mon Sep 17 00:00:00 2001 From: Code0987 <1825861+Code0987@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:40:39 +0000 Subject: [PATCH] feat: color and unify sc CLI output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TTY color for OK, errors, usage, warnings, help, and the REPL prompt. Mutations print OK …; misses print (nil); bools print true/false. Help is a grouped catalog; per-command usage still comes from no-arg invocations. Color off for NO_COLOR, TERM=dumb, non-TTY, and -json. --- cmd/sc/README.md | 46 +++--- cmd/sc/cmds.go | 348 +++++++++++++++++++++--------------------- cmd/sc/cmds_cms.go | 15 +- cmd/sc/cmds_topk.go | 13 +- cmd/sc/cmds_vecset.go | 39 +++-- cmd/sc/help.go | 86 +++++++++++ cmd/sc/main.go | 115 +------------- cmd/sc/repl.go | 69 +++------ cmd/sc/style.go | 95 ++++++++++++ cmd/sc/style_test.go | 206 +++++++++++++++++++++++++ 10 files changed, 651 insertions(+), 381 deletions(-) create mode 100644 cmd/sc/help.go create mode 100644 cmd/sc/style.go create mode 100644 cmd/sc/style_test.go diff --git a/cmd/sc/README.md b/cmd/sc/README.md index ae68109..e40f964 100644 --- a/cmd/sc/README.md +++ b/cmd/sc/README.md @@ -17,9 +17,9 @@ go run ./cmd/sc peers # Interactive REPL go run ./cmd/sc -# sc demo@:9000> put k v -# sc demo@:9000> get k -# sc demo@:9000> quit +# sc cacheonly@:9000> put k v +# sc cacheonly@:9000> get k +# sc cacheonly@:9000> quit ``` Install: @@ -50,10 +50,10 @@ This is **not** client-side sharding. Any healthy cache node is a valid front do | Command | Port | What it does | |---------|------|----------------| -| `get [key...]` | Cache gRPC | Fetch value(s); exit `1` if any missing | +| `get [key...]` | Cache gRPC | Fetch value(s); miss = `(nil)`; exit `1` if any missing | | `put` / `set` | Cache gRPC | Store a value (string, `-file`, or stdin) — **KV modes only** (`set` is put, not ModeSet) | | `del` / `delete` | Cache gRPC | Cluster invalidate (peer warnings on stderr); also wipes named Bloom/set/zset/geo/list/hash/counter/json/bitmap | -| `bloom add\|test ` | Cache gRPC | `ModeBloom` membership | +| `bloom add\|test ` | Cache gRPC | `ModeBloom` membership (`test` prints `true`/`false`) | | `sadd ` | Cache gRPC | `ModeSet` add | | `srem ` | Cache gRPC | `ModeSet` remove | | `sismember ` | Cache gRPC | `ModeSet` contains (`true`/`false`; exit 1 if false) | @@ -97,13 +97,14 @@ This is **not** client-side sharding. Any healthy cache node is a valid front do | `topklist ` | Cache gRPC | `item count` lines, or `(nil)` + exit 1 if name missing | | `cmsincr [n]` | Cache gRPC | `ModeCMS` increment (`n` optional, default 1; 0 means 1) | | `cmsquery ` | Cache gRPC | decimal estimate, or `(nil)` + exit 1 if name missing | +| `vadd` / `vrem` / `vsim` / `vcard` / `vdim` / `vemb` | Cache gRPC | `ModeVectorSet`; miss = `(nil)` | | `ping` | both | Dial cache seeds + admin `/healthz` | | `peers` / `keyspaces` / `metrics` | Admin HTTP | Diagnostics | | `health` / `ready` | Admin HTTP | Probes | | `repl` (or bare `sc` on a TTY) | — | Interactive shell | | `version` | — | CLI version | -Use `-keyspace` / REPL `keyspace` to select the mode’s keyspace (`demo` KV, `tags` ModeSet, `board` ModeZSet, `profile` ModeHash, `doc` ModeJSON, `flags` ModeBitmap, or your own). +Use `-keyspace` / REPL `keyspace` to select an existing keyspace. Stock demo names are lowercase `Mode.String()`: `cacheonly`, `set`, `zset`, `hash`, `json`, `bitmap`, `vectorset`. ## REPL @@ -112,16 +113,16 @@ sc -addr 127.0.0.1:9000,127.0.0.1:9010 ``` ```text -connected 127.0.0.1:9000 keyspace=demo seeds=2 -sc demo@:9000> put session:1 '{"user":1}' -sc demo@:9000> get session:1 -sc demo@:9000> keyspace board -sc board@:9000> zadd lb 100 alice -sc board@:9000> zrange lb 0 -1 -sc board@:9000> seeds -sc board@:9000> connect :9010 -sc board@:9000> peers -sc board@:9000> quit +connected 127.0.0.1:9000 keyspace=cacheonly seeds=2 +sc cacheonly@:9000> put session:1 '{"user":1}' +sc cacheonly@:9000> get session:1 +sc cacheonly@:9000> keyspace zset +sc zset@:9000> zadd lb 100 alice +sc zset@:9000> zrange lb 0 -1 +sc zset@:9000> seeds +sc zset@:9000> connect :9010 +sc zset@:9000> peers +sc zset@:9000> quit ``` | REPL meta | Meaning | @@ -142,7 +143,7 @@ Quotes work: `put k "hello world"`. Per-line flags: `put k -file ./x.bin`, `get |------|---------|-----| | `-addr` | `127.0.0.1:9000` | `SC_ADDR` (comma-separated OK) | | `-admin` | `127.0.0.1:8080` | `SC_ADMIN` | -| `-keyspace` | `demo` | `SC_KEYSPACE` | +| `-keyspace` | `cacheonly` | `SC_KEYSPACE` | | `-timeout` | `5s` | | | `-ttl` / `-no-expiry` | keyspace default | put only | | `-file` | | put value from path (`-` = stdin) | @@ -151,6 +152,17 @@ Quotes work: `put k "hello world"`. Per-line flags: `put k -file ./x.bin`, `get Flags may appear before or after the subcommand. +## Output + +On a TTY, `sc` colors `OK`, errors, `usage:`, `warning:`, help headings, and the REPL prompt. Color is off when the stream is not a TTY, `NO_COLOR` is set, `TERM=dumb`, or `-json` is on. + +| Kind | Text | +|------|------| +| Mutation | `OK …` (e.g. `OK put greeting (11 bytes)`, `OK del greeting`) | +| Miss | `(nil)` on stdout; exit `1` | +| Bool | `true` / `false` | +| Usage / error | `usage: …` / `: …` on stderr | + ## Notes - Dial the **Cache** port (`-cache` on the node), never the Peer mesh port. diff --git a/cmd/sc/cmds.go b/cmd/sc/cmds.go index f2c918c..bddabfb 100644 --- a/cmd/sc/cmds.go +++ b/cmd/sc/cmds.go @@ -141,14 +141,15 @@ func dispatch(ctx context.Context, sess *session, cmd string, args []string) int case "vemb": return cmdVEmb(ctx, sess, args) default: - fmt.Fprintf(os.Stderr, "unknown command %q\n", cmd) + printUnknown(cmd) + printNote("type help for commands") return 2 } } func cmdGet(ctx context.Context, sess *session, keys []string) int { if len(keys) == 0 { - fmt.Fprintln(os.Stderr, "usage: get [key...]") + printUsageLine("usage: get [key...]") return 2 } cfg := sess.cfg @@ -197,7 +198,7 @@ func cmdGet(ctx context.Context, sess *session, keys []string) int { return nil }) if err != nil { - fmt.Fprintf(os.Stderr, "get: %v\n", err) + printCmdErr("get", err) return 1 } @@ -217,11 +218,15 @@ func cmdGet(ctx context.Context, sess *session, keys []string) int { for _, it := range items { if it.Error != "" { - fmt.Fprintf(os.Stderr, "%s: %s\n", it.Key, it.Error) + printCmdMsg(it.Key, it.Error) continue } if !it.Found { - fmt.Fprintf(os.Stderr, "%s: (not found)\n", it.Key) + if len(keys) == 1 { + printNil() + } else { + fmt.Printf("%s\t%s\n", it.Key, paint(os.Stdout, ansiDim, "(nil)")) + } continue } if len(keys) == 1 { @@ -230,13 +235,13 @@ func cmdGet(ctx context.Context, sess *session, keys []string) int { } else { fmt.Println(it.Value) if it.Base64 && !cfg.base64 { - fmt.Fprintln(os.Stderr, "# note: binary value shown as base64; pass -base64 to make it explicit") + printNote("binary value shown as base64; pass -base64 to make it explicit") } } continue } if it.Base64 { - fmt.Printf("%s\t(base64)\t%s\n", it.Key, it.Value) + fmt.Printf("%s\t%s\t%s\n", it.Key, paint(os.Stdout, ansiDim, "(base64)"), it.Value) } else { fmt.Printf("%s\t%s\n", it.Key, it.Value) } @@ -249,7 +254,7 @@ func cmdGet(ctx context.Context, sess *session, keys []string) int { func cmdPut(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: put | put -file ") + printUsageLine("usage: put | put -file ") return 2 } cfg := sess.cfg @@ -261,7 +266,7 @@ func cmdPut(ctx context.Context, sess *session, args []string) int { case cfg.filePath != "": value, err = readFileOrStdin(cfg.filePath) if err != nil { - fmt.Fprintf(os.Stderr, "read value: %v\n", err) + printCmdErr("put", err) return 1 } case len(args) >= 2: @@ -269,14 +274,14 @@ func cmdPut(ctx context.Context, sess *session, args []string) int { if cfg.base64 { value, err = base64.StdEncoding.DecodeString(raw) if err != nil { - fmt.Fprintf(os.Stderr, "base64 decode: %v\n", err) + printCmdMsg("put", fmt.Sprintf("bad base64: %v", err)) return 1 } } else { value = []byte(raw) } default: - fmt.Fprintln(os.Stderr, "usage: put | put -file ") + printUsageLine("usage: put | put -file ") return 2 } @@ -289,7 +294,7 @@ func cmdPut(ctx context.Context, sess *session, args []string) int { return cli.Put(ctx, cfg.keyspace, key, value, opts...) }) if err != nil { - fmt.Fprintf(os.Stderr, "put: %v\n", err) + printCmdErr("put", err) return 1 } if cfg.jsonOut { @@ -299,14 +304,14 @@ func cmdPut(ctx context.Context, sess *session, args []string) int { "seed": sess.ConnectedAddr(), }) } else if !cfg.quiet { - fmt.Printf("OK %s/%s (%d bytes) via %s\n", cfg.keyspace, key, len(value), sess.ConnectedAddr()) + printOK(fmt.Sprintf("put %s (%d bytes)", key, len(value))) } return 0 } func cmdDel(ctx context.Context, sess *session, keys []string) int { if len(keys) == 0 { - fmt.Fprintln(os.Stderr, "usage: del [key...]") + printUsageLine("usage: del [key...]") return 2 } cfg := sess.cfg @@ -333,25 +338,25 @@ func cmdDel(ctx context.Context, sess *session, keys []string) int { return nil }) if err != nil { - fmt.Fprintf(os.Stderr, "delete: %v\n", err) + printCmdErr("del", err) return 1 } if delErr != nil { var pf client.PeerFailures var ke client.KeyErrors if errors.As(delErr, &pf) || errors.As(delErr, &ke) { - fmt.Fprintf(os.Stderr, "warning: %v\n", delErr) + printWarn(delErr.Error()) if cfg.jsonOut { _ = json.NewEncoder(os.Stdout).Encode(map[string]any{ "ok": true, "partial": true, "keyspace": cfg.keyspace, "keys": keys, "warning": delErr.Error(), "seed": sess.ConnectedAddr(), }) } else if !cfg.quiet { - fmt.Printf("OK %s (with peer warnings) via %s\n", strings.Join(keys, ", "), sess.ConnectedAddr()) + printOK("del " + strings.Join(keys, " ")) } return 0 } - fmt.Fprintf(os.Stderr, "delete: %v\n", delErr) + printCmdErr("del", delErr) return 1 } if cfg.jsonOut { @@ -359,17 +364,21 @@ func cmdDel(ctx context.Context, sess *session, keys []string) int { "ok": true, "keyspace": cfg.keyspace, "keys": keys, "seed": sess.ConnectedAddr(), }) } else if !cfg.quiet { - fmt.Printf("OK deleted %s via %s\n", strings.Join(keys, ", "), sess.ConnectedAddr()) + printOK("del " + strings.Join(keys, " ")) } return 0 } func cmdBloom(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: bloom add|test ") + printUsageLine("usage: bloom add|test ") return 2 } op, name, item := strings.ToLower(args[0]), args[1], args[2] + if op != "add" && op != "test" { + printUsageLine("usage: bloom add|test ") + return 2 + } var ( maybe bool opErr error @@ -379,37 +388,32 @@ func cmdBloom(ctx context.Context, sess *session, args []string) int { case "add": opErr = cli.BloomAdd(ctx, sess.cfg.keyspace, name, []byte(item)) return opErr - case "test": + default: maybe, opErr = cli.BloomTest(ctx, sess.cfg.keyspace, name, []byte(item)) return opErr - default: - opErr = fmt.Errorf("usage: bloom add|test ") - return nil } }) if err != nil { - fmt.Fprintf(os.Stderr, "bloom: %v\n", err) + printCmdErr("bloom", err) return 1 } - if opErr != nil && op != "add" && op != "test" { - fmt.Fprintf(os.Stderr, "%v\n", opErr) - return 2 - } if opErr != nil { - fmt.Fprintf(os.Stderr, "bloom: %v\n", opErr) + printCmdErr("bloom", opErr) return 1 } if op == "test" { - fmt.Printf("maybe=%v\n", maybe) - } else if !sess.cfg.quiet { - fmt.Printf("OK bloom add %s %s\n", name, item) + printBool(maybe) + return 0 + } + if !sess.cfg.quiet { + printOK(fmt.Sprintf("bloom add %s %s", name, item)) } return 0 } func cmdSAdd(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: sadd ") + printUsageLine("usage: sadd ") return 2 } name, item := args[0], args[1] @@ -417,18 +421,18 @@ func cmdSAdd(ctx context.Context, sess *session, args []string) int { return cli.SetAdd(ctx, sess.cfg.keyspace, name, []byte(item)) }) if err != nil { - fmt.Fprintf(os.Stderr, "sadd: %v\n", err) + printCmdErr("sadd", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK sadd %s %s\n", name, item) + printOK(fmt.Sprintf("sadd %s %s", name, item)) } return 0 } func cmdSRem(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: srem ") + printUsageLine("usage: srem ") return 2 } name, item := args[0], args[1] @@ -436,18 +440,18 @@ func cmdSRem(ctx context.Context, sess *session, args []string) int { return cli.SetRemove(ctx, sess.cfg.keyspace, name, []byte(item)) }) if err != nil { - fmt.Fprintf(os.Stderr, "srem: %v\n", err) + printCmdErr("srem", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK srem %s %s\n", name, item) + printOK(fmt.Sprintf("srem %s %s", name, item)) } return 0 } func cmdSIsMember(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: sismember ") + printUsageLine("usage: sismember ") return 2 } name, item := args[0], args[1] @@ -458,10 +462,10 @@ func cmdSIsMember(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "sismember: %v\n", err) + printCmdErr("sismember", err) return 1 } - fmt.Println(present) + printBool(present) if !present { return 1 } @@ -470,7 +474,7 @@ func cmdSIsMember(ctx context.Context, sess *session, args []string) int { func cmdSCard(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: scard ") + printUsageLine("usage: scard ") return 2 } name := args[0] @@ -481,7 +485,7 @@ func cmdSCard(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "scard: %v\n", err) + printCmdErr("scard", err) return 1 } fmt.Println(n) @@ -490,7 +494,7 @@ func cmdSCard(ctx context.Context, sess *session, args []string) int { func cmdSMembers(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: smembers ") + printUsageLine("usage: smembers ") return 2 } name := args[0] @@ -501,7 +505,7 @@ func cmdSMembers(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "smembers: %v\n", err) + printCmdErr("smembers", err) return 1 } for _, m := range mem { @@ -512,31 +516,31 @@ func cmdSMembers(ctx context.Context, sess *session, args []string) int { func cmdZAdd(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: zadd ") + printUsageLine("usage: zadd ") return 2 } name, scoreStr, member := args[0], args[1], args[2] score, err := parseFloat(scoreStr) if err != nil { - fmt.Fprintf(os.Stderr, "zadd: bad score %q: %v\n", scoreStr, err) + printCmdMsg("zadd", fmt.Sprintf("bad score %q: %v", scoreStr, err)) return 2 } err = sess.withClient(func(cli *client.Client, _ string) error { return cli.ZAdd(ctx, sess.cfg.keyspace, name, []byte(member), score) }) if err != nil { - fmt.Fprintf(os.Stderr, "zadd: %v\n", err) + printCmdErr("zadd", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK zadd %s %g %s\n", name, score, member) + printOK(fmt.Sprintf("zadd %s %g %s", name, score, member)) } return 0 } func cmdZRem(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: zrem ") + printUsageLine("usage: zrem ") return 2 } name, member := args[0], args[1] @@ -544,18 +548,18 @@ func cmdZRem(ctx context.Context, sess *session, args []string) int { return cli.ZRem(ctx, sess.cfg.keyspace, name, []byte(member)) }) if err != nil { - fmt.Fprintf(os.Stderr, "zrem: %v\n", err) + printCmdErr("zrem", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK zrem %s %s\n", name, member) + printOK(fmt.Sprintf("zrem %s %s", name, member)) } return 0 } func cmdZScore(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: zscore ") + printUsageLine("usage: zscore ") return 2 } name, member := args[0], args[1] @@ -569,11 +573,11 @@ func cmdZScore(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "zscore: %v\n", err) + printCmdErr("zscore", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(score) @@ -582,7 +586,7 @@ func cmdZScore(ctx context.Context, sess *session, args []string) int { func cmdZCard(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: zcard ") + printUsageLine("usage: zcard ") return 2 } name := args[0] @@ -593,7 +597,7 @@ func cmdZCard(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "zcard: %v\n", err) + printCmdErr("zcard", err) return 1 } fmt.Println(n) @@ -602,18 +606,18 @@ func cmdZCard(ctx context.Context, sess *session, args []string) int { func cmdZRange(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: zrange ") + printUsageLine("usage: zrange ") return 2 } name := args[0] start, err := parseInt(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "zrange: bad start %q: %v\n", args[1], err) + printCmdMsg("zrange", fmt.Sprintf("bad start %q: %v", args[1], err)) return 2 } stop, err := parseInt(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "zrange: bad stop %q: %v\n", args[2], err) + printCmdMsg("zrange", fmt.Sprintf("bad stop %q: %v", args[2], err)) return 2 } var mem []client.ZMember @@ -623,7 +627,7 @@ func cmdZRange(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "zrange: %v\n", err) + printCmdErr("zrange", err) return 1 } printZMembers(mem) @@ -632,18 +636,18 @@ func cmdZRange(ctx context.Context, sess *session, args []string) int { func cmdZRangeByScore(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: zrangebyscore ") + printUsageLine("usage: zrangebyscore ") return 2 } name := args[0] min, err := parseFloat(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "zrangebyscore: bad min %q: %v\n", args[1], err) + printCmdMsg("zrangebyscore", fmt.Sprintf("bad min %q: %v", args[1], err)) return 2 } max, err := parseFloat(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "zrangebyscore: bad max %q: %v\n", args[2], err) + printCmdMsg("zrangebyscore", fmt.Sprintf("bad max %q: %v", args[2], err)) return 2 } var mem []client.ZMember @@ -653,7 +657,7 @@ func cmdZRangeByScore(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "zrangebyscore: %v\n", err) + printCmdErr("zrangebyscore", err) return 1 } printZMembers(mem) @@ -668,36 +672,36 @@ func printZMembers(mem []client.ZMember) { func cmdGeoAdd(ctx context.Context, sess *session, args []string) int { if len(args) < 4 { - fmt.Fprintln(os.Stderr, "usage: geoadd ") + printUsageLine("usage: geoadd ") return 2 } name, member := args[0], args[3] lon, err := parseFloat(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "geoadd: bad lon %q: %v\n", args[1], err) + printCmdMsg("geoadd", fmt.Sprintf("bad lon %q: %v", args[1], err)) return 2 } lat, err := parseFloat(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "geoadd: bad lat %q: %v\n", args[2], err) + printCmdMsg("geoadd", fmt.Sprintf("bad lat %q: %v", args[2], err)) return 2 } err = sess.withClient(func(cli *client.Client, _ string) error { return cli.GeoAdd(ctx, sess.cfg.keyspace, name, []byte(member), lon, lat) }) if err != nil { - fmt.Fprintf(os.Stderr, "geoadd: %v\n", err) + printCmdErr("geoadd", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK geoadd %s %g %g %s\n", name, lon, lat, member) + printOK(fmt.Sprintf("geoadd %s %g %g %s", name, lon, lat, member)) } return 0 } func cmdGeoRem(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: georem ") + printUsageLine("usage: georem ") return 2 } name, member := args[0], args[1] @@ -705,18 +709,18 @@ func cmdGeoRem(ctx context.Context, sess *session, args []string) int { return cli.GeoRem(ctx, sess.cfg.keyspace, name, []byte(member)) }) if err != nil { - fmt.Fprintf(os.Stderr, "georem: %v\n", err) + printCmdErr("georem", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK georem %s %s\n", name, member) + printOK(fmt.Sprintf("georem %s %s", name, member)) } return 0 } func cmdGeoPos(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: geopos ") + printUsageLine("usage: geopos ") return 2 } name, member := args[0], args[1] @@ -728,11 +732,11 @@ func cmdGeoPos(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "geopos: %v\n", err) + printCmdErr("geopos", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Printf("%g %g\n", lon, lat) @@ -741,7 +745,7 @@ func cmdGeoPos(ctx context.Context, sess *session, args []string) int { func cmdGeoCard(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: geocard ") + printUsageLine("usage: geocard ") return 2 } var n int @@ -751,7 +755,7 @@ func cmdGeoCard(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "geocard: %v\n", err) + printCmdErr("geocard", err) return 1 } fmt.Println(n) @@ -760,7 +764,7 @@ func cmdGeoCard(ctx context.Context, sess *session, args []string) int { func cmdGeoDist(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: geodist ") + printUsageLine("usage: geodist ") return 2 } var meters float64 @@ -771,11 +775,11 @@ func cmdGeoDist(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "geodist: %v\n", err) + printCmdErr("geodist", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(meters) @@ -784,29 +788,29 @@ func cmdGeoDist(ctx context.Context, sess *session, args []string) int { func cmdGeoRadius(ctx context.Context, sess *session, args []string) int { if len(args) < 4 { - fmt.Fprintln(os.Stderr, "usage: georadius [limit]") + printUsageLine("usage: georadius [limit]") return 2 } lon, err := parseFloat(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "georadius: bad lon %q: %v\n", args[1], err) + printCmdMsg("georadius", fmt.Sprintf("bad lon %q: %v", args[1], err)) return 2 } lat, err := parseFloat(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "georadius: bad lat %q: %v\n", args[2], err) + printCmdMsg("georadius", fmt.Sprintf("bad lat %q: %v", args[2], err)) return 2 } rad, err := parseFloat(args[3]) if err != nil { - fmt.Fprintf(os.Stderr, "georadius: bad radius %q: %v\n", args[3], err) + printCmdMsg("georadius", fmt.Sprintf("bad radius %q: %v", args[3], err)) return 2 } limit := 0 if len(args) >= 5 { limit, err = parseInt(args[4]) if err != nil { - fmt.Fprintf(os.Stderr, "georadius: bad limit %q: %v\n", args[4], err) + printCmdMsg("georadius", fmt.Sprintf("bad limit %q: %v", args[4], err)) return 2 } } @@ -817,7 +821,7 @@ func cmdGeoRadius(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "georadius: %v\n", err) + printCmdErr("georadius", err) return 1 } for _, m := range mem { @@ -828,43 +832,43 @@ func cmdGeoRadius(ctx context.Context, sess *session, args []string) int { func cmdLPush(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: lpush ") + printUsageLine("usage: lpush ") return 2 } err := sess.withClient(func(cli *client.Client, _ string) error { return cli.LPush(ctx, sess.cfg.keyspace, args[0], []byte(args[1])) }) if err != nil { - fmt.Fprintf(os.Stderr, "lpush: %v\n", err) + printCmdErr("lpush", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK lpush %s %s\n", args[0], args[1]) + printOK(fmt.Sprintf("lpush %s %s", args[0], args[1])) } return 0 } func cmdRPush(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: rpush ") + printUsageLine("usage: rpush ") return 2 } err := sess.withClient(func(cli *client.Client, _ string) error { return cli.RPush(ctx, sess.cfg.keyspace, args[0], []byte(args[1])) }) if err != nil { - fmt.Fprintf(os.Stderr, "rpush: %v\n", err) + printCmdErr("rpush", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK rpush %s %s\n", args[0], args[1]) + printOK(fmt.Sprintf("rpush %s %s", args[0], args[1])) } return 0 } func cmdLPop(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: lpop ") + printUsageLine("usage: lpop ") return 2 } var item []byte @@ -875,11 +879,11 @@ func cmdLPop(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "lpop: %v\n", err) + printCmdErr("lpop", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(string(item)) @@ -888,7 +892,7 @@ func cmdLPop(ctx context.Context, sess *session, args []string) int { func cmdRPop(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: rpop ") + printUsageLine("usage: rpop ") return 2 } var item []byte @@ -899,11 +903,11 @@ func cmdRPop(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "rpop: %v\n", err) + printCmdErr("rpop", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(string(item)) @@ -912,7 +916,7 @@ func cmdRPop(ctx context.Context, sess *session, args []string) int { func cmdLLen(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: llen ") + printUsageLine("usage: llen ") return 2 } var n int @@ -922,7 +926,7 @@ func cmdLLen(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "llen: %v\n", err) + printCmdErr("llen", err) return 1 } fmt.Println(n) @@ -931,12 +935,12 @@ func cmdLLen(ctx context.Context, sess *session, args []string) int { func cmdLIndex(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: lindex ") + printUsageLine("usage: lindex ") return 2 } idx, err := parseInt(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "lindex: bad idx %q: %v\n", args[1], err) + printCmdMsg("lindex", fmt.Sprintf("bad idx %q: %v", args[1], err)) return 2 } var item []byte @@ -947,11 +951,11 @@ func cmdLIndex(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "lindex: %v\n", err) + printCmdErr("lindex", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(string(item)) @@ -960,17 +964,17 @@ func cmdLIndex(ctx context.Context, sess *session, args []string) int { func cmdLRange(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: lrange ") + printUsageLine("usage: lrange ") return 2 } start, err := parseInt(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "lrange: bad start %q: %v\n", args[1], err) + printCmdMsg("lrange", fmt.Sprintf("bad start %q: %v", args[1], err)) return 2 } stop, err := parseInt(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "lrange: bad stop %q: %v\n", args[2], err) + printCmdMsg("lrange", fmt.Sprintf("bad stop %q: %v", args[2], err)) return 2 } var items [][]byte @@ -980,7 +984,7 @@ func cmdLRange(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "lrange: %v\n", err) + printCmdErr("lrange", err) return 1 } for _, it := range items { @@ -991,7 +995,7 @@ func cmdLRange(ctx context.Context, sess *session, args []string) int { func cmdHSet(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: hset ") + printUsageLine("usage: hset ") return 2 } name, field, value := args[0], args[1], strings.Join(args[2:], " ") @@ -999,18 +1003,18 @@ func cmdHSet(ctx context.Context, sess *session, args []string) int { return cli.HSet(ctx, sess.cfg.keyspace, name, []byte(field), []byte(value)) }) if err != nil { - fmt.Fprintf(os.Stderr, "hset: %v\n", err) + printCmdErr("hset", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK hset %s %s\n", name, field) + printOK(fmt.Sprintf("hset %s %s", name, field)) } return 0 } func cmdHGet(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: hget ") + printUsageLine("usage: hget ") return 2 } var v []byte @@ -1021,11 +1025,11 @@ func cmdHGet(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "hget: %v\n", err) + printCmdErr("hget", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(string(v)) @@ -1034,25 +1038,25 @@ func cmdHGet(ctx context.Context, sess *session, args []string) int { func cmdHDel(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: hdel ") + printUsageLine("usage: hdel ") return 2 } err := sess.withClient(func(cli *client.Client, _ string) error { return cli.HDel(ctx, sess.cfg.keyspace, args[0], []byte(args[1])) }) if err != nil { - fmt.Fprintf(os.Stderr, "hdel: %v\n", err) + printCmdErr("hdel", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK hdel %s %s\n", args[0], args[1]) + printOK(fmt.Sprintf("hdel %s %s", args[0], args[1])) } return 0 } func cmdHExists(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: hexists ") + printUsageLine("usage: hexists ") return 2 } var present bool @@ -1062,10 +1066,10 @@ func cmdHExists(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "hexists: %v\n", err) + printCmdErr("hexists", err) return 1 } - fmt.Println(present) + printBool(present) if !present { return 1 } @@ -1074,7 +1078,7 @@ func cmdHExists(ctx context.Context, sess *session, args []string) int { func cmdHLen(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: hlen ") + printUsageLine("usage: hlen ") return 2 } var n int @@ -1084,7 +1088,7 @@ func cmdHLen(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "hlen: %v\n", err) + printCmdErr("hlen", err) return 1 } fmt.Println(n) @@ -1093,7 +1097,7 @@ func cmdHLen(ctx context.Context, sess *session, args []string) int { func cmdHGetAll(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: hgetall ") + printUsageLine("usage: hgetall ") return 2 } var all []client.HashField @@ -1103,7 +1107,7 @@ func cmdHGetAll(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "hgetall: %v\n", err) + printCmdErr("hgetall", err) return 1 } for _, f := range all { @@ -1114,14 +1118,14 @@ func cmdHGetAll(ctx context.Context, sess *session, args []string) int { func cmdIncr(ctx context.Context, sess *session, args []string) int { if len(args) < 1 || len(args) > 2 { - fmt.Fprintln(os.Stderr, "usage: incr [delta]") + printUsageLine("usage: incr [delta]") return 2 } delta := int64(1) if len(args) == 2 { n, err := strconv.ParseInt(args[1], 10, 64) if err != nil { - fmt.Fprintf(os.Stderr, "incr: bad delta %q: %v\n", args[1], err) + printCmdMsg("incr", fmt.Sprintf("bad delta %q: %v", args[1], err)) return 2 } delta = n @@ -1133,7 +1137,7 @@ func cmdIncr(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "incr: %v\n", err) + printCmdErr("incr", err) return 1 } fmt.Println(v) @@ -1142,7 +1146,7 @@ func cmdIncr(ctx context.Context, sess *session, args []string) int { func cmdCGet(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: cget ") + printUsageLine("usage: cget ") return 2 } var v int64 @@ -1153,11 +1157,11 @@ func cmdCGet(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "cget: %v\n", err) + printCmdErr("cget", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(v) @@ -1166,7 +1170,7 @@ func cmdCGet(ctx context.Context, sess *session, args []string) int { func cmdJSONSet(ctx context.Context, sess *session, args []string) int { if len(args) < 3 { - fmt.Fprintln(os.Stderr, "usage: jsonset ") + printUsageLine("usage: jsonset ") return 2 } name, path, value := args[0], args[1], strings.Join(args[2:], " ") @@ -1174,18 +1178,18 @@ func cmdJSONSet(ctx context.Context, sess *session, args []string) int { return cli.JsonSet(ctx, sess.cfg.keyspace, name, path, []byte(value)) }) if err != nil { - fmt.Fprintf(os.Stderr, "jsonset: %v\n", err) + printCmdErr("jsonset", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK jsonset %s %s\n", name, path) + printOK(fmt.Sprintf("jsonset %s %s", name, path)) } return 0 } func cmdJSONGet(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: jsonget [path]") + printUsageLine("usage: jsonget [path]") return 2 } path := "$" @@ -1200,11 +1204,11 @@ func cmdJSONGet(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "jsonget: %v\n", err) + printCmdErr("jsonget", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(string(v)) @@ -1213,7 +1217,7 @@ func cmdJSONGet(ctx context.Context, sess *session, args []string) int { func cmdJSONDel(ctx context.Context, sess *session, args []string) int { if len(args) < 1 { - fmt.Fprintln(os.Stderr, "usage: jsondel [path]") + printUsageLine("usage: jsondel [path]") return 2 } path := "$" @@ -1224,23 +1228,23 @@ func cmdJSONDel(ctx context.Context, sess *session, args []string) int { return cli.JsonDel(ctx, sess.cfg.keyspace, args[0], path) }) if err != nil { - fmt.Fprintf(os.Stderr, "jsondel: %v\n", err) + printCmdErr("jsondel", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK jsondel %s %s\n", args[0], path) + printOK(fmt.Sprintf("jsondel %s %s", args[0], path)) } return 0 } func cmdBitSet(ctx context.Context, sess *session, args []string) int { if len(args) != 3 { - fmt.Fprintln(os.Stderr, "usage: bitset <0|1>") + printUsageLine("usage: bitset <0|1>") return 2 } off, err := strconv.ParseUint(args[1], 10, 64) if err != nil { - fmt.Fprintf(os.Stderr, "bitset: bad offset %q: %v\n", args[1], err) + printCmdMsg("bitset", fmt.Sprintf("bad offset %q: %v", args[1], err)) return 2 } var bit bool @@ -1250,30 +1254,30 @@ func cmdBitSet(ctx context.Context, sess *session, args []string) int { case "1": bit = true default: - fmt.Fprintln(os.Stderr, "usage: bitset <0|1>") + printUsageLine("usage: bitset <0|1>") return 2 } err = sess.withClient(func(cli *client.Client, _ string) error { return cli.BitSet(ctx, sess.cfg.keyspace, args[0], off, bit) }) if err != nil { - fmt.Fprintf(os.Stderr, "bitset: %v\n", err) + printCmdErr("bitset", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK bitset %s %d %s\n", args[0], off, args[2]) + printOK(fmt.Sprintf("bitset %s %d %s", args[0], off, args[2])) } return 0 } func cmdBitGet(ctx context.Context, sess *session, args []string) int { if len(args) != 2 { - fmt.Fprintln(os.Stderr, "usage: bitget ") + printUsageLine("usage: bitget ") return 2 } off, err := strconv.ParseUint(args[1], 10, 64) if err != nil { - fmt.Fprintf(os.Stderr, "bitget: bad offset %q: %v\n", args[1], err) + printCmdMsg("bitget", fmt.Sprintf("bad offset %q: %v", args[1], err)) return 2 } var bit, ok bool @@ -1283,11 +1287,11 @@ func cmdBitGet(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "bitget: %v\n", err) + printCmdErr("bitget", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } if bit { @@ -1300,7 +1304,7 @@ func cmdBitGet(ctx context.Context, sess *session, args []string) int { func cmdBitCount(ctx context.Context, sess *session, args []string) int { if len(args) != 1 && len(args) != 3 { - fmt.Fprintln(os.Stderr, "usage: bitcount [start end]") + printUsageLine("usage: bitcount [start end]") return 2 } start, end := 0, -1 @@ -1308,12 +1312,12 @@ func cmdBitCount(ctx context.Context, sess *session, args []string) int { var err error start, err = parseInt(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "bitcount: bad start %q: %v\n", args[1], err) + printCmdMsg("bitcount", fmt.Sprintf("bad start %q: %v", args[1], err)) return 2 } end, err = parseInt(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "bitcount: bad end %q: %v\n", args[2], err) + printCmdMsg("bitcount", fmt.Sprintf("bad end %q: %v", args[2], err)) return 2 } } @@ -1324,7 +1328,7 @@ func cmdBitCount(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "bitcount: %v\n", err) + printCmdErr("bitcount", err) return 1 } fmt.Println(n) @@ -1333,7 +1337,7 @@ func cmdBitCount(ctx context.Context, sess *session, args []string) int { func cmdBitPos(ctx context.Context, sess *session, args []string) int { if len(args) != 2 && len(args) != 4 { - fmt.Fprintln(os.Stderr, "usage: bitpos <0|1> [start end]") + printUsageLine("usage: bitpos <0|1> [start end]") return 2 } var bit bool @@ -1343,7 +1347,7 @@ func cmdBitPos(ctx context.Context, sess *session, args []string) int { case "1": bit = true default: - fmt.Fprintln(os.Stderr, "usage: bitpos <0|1> [start end]") + printUsageLine("usage: bitpos <0|1> [start end]") return 2 } start, end := 0, -1 @@ -1351,12 +1355,12 @@ func cmdBitPos(ctx context.Context, sess *session, args []string) int { var err error start, err = parseInt(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "bitpos: bad start %q: %v\n", args[2], err) + printCmdMsg("bitpos", fmt.Sprintf("bad start %q: %v", args[2], err)) return 2 } end, err = parseInt(args[3]) if err != nil { - fmt.Fprintf(os.Stderr, "bitpos: bad end %q: %v\n", args[3], err) + printCmdMsg("bitpos", fmt.Sprintf("bad end %q: %v", args[3], err)) return 2 } } @@ -1368,11 +1372,11 @@ func cmdBitPos(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "bitpos: %v\n", err) + printCmdErr("bitpos", err) return 1 } if !found { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(pos) @@ -1381,7 +1385,7 @@ func cmdBitPos(ctx context.Context, sess *session, args []string) int { func cmdHLLAdd(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: hlladd ") + printUsageLine("usage: hlladd ") return 2 } name := args[0] @@ -1390,11 +1394,11 @@ func cmdHLLAdd(ctx context.Context, sess *session, args []string) int { return cli.HLLAdd(ctx, sess.cfg.keyspace, name, []byte(item)) }) if err != nil { - fmt.Fprintf(os.Stderr, "hlladd: %v\n", err) + printCmdErr("hlladd", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK hlladd %s %s\n", name, item) + printOK(fmt.Sprintf("hlladd %s %s", name, item)) } } return 0 @@ -1402,7 +1406,7 @@ func cmdHLLAdd(ctx context.Context, sess *session, args []string) int { func cmdHLLCount(ctx context.Context, sess *session, args []string) int { if len(args) != 1 { - fmt.Fprintln(os.Stderr, "usage: hllcount ") + printUsageLine("usage: hllcount ") return 2 } var n uint64 @@ -1413,11 +1417,11 @@ func cmdHLLCount(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "hllcount: %v\n", err) + printCmdErr("hllcount", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(n) @@ -1439,20 +1443,20 @@ func cmdPing(ctx context.Context, sess *session) int { if err != nil && !errors.Is(err, client.ErrNotFound) { return err } - fmt.Printf("cache %s: ok\n", addr) + fmt.Printf("%s %s: %s\n", paint(os.Stdout, ansiCyan, "cache"), addr, paint(os.Stdout, ansiGreen, "ok")) return nil }) if err != nil { - fmt.Fprintf(os.Stderr, "cache: %v\n", err) + printCmdErr("cache", err) return 1 } code, body, aerr, used := adminGET(ctx, sess.cfg, "/healthz") if aerr != nil { - fmt.Printf("admin: unreachable (%v)\n", aerr) + fmt.Printf("%s: %s (%v)\n", paint(os.Stdout, ansiCyan, "admin"), paint(os.Stdout, ansiYellow, "unreachable"), aerr) return 0 } - fmt.Printf("admin %s: HTTP %d\n", used, code) + fmt.Printf("%s %s: HTTP %d\n", paint(os.Stdout, ansiCyan, "admin"), used, code) if sess.cfg.jsonOut && len(body) > 0 { os.Stdout.Write(body) } @@ -1462,11 +1466,11 @@ func cmdPing(ctx context.Context, sess *session) int { func cmdAdmin(ctx context.Context, cfg *config, path string) int { code, body, err, used := adminGET(ctx, cfg, path) if err != nil { - fmt.Fprintf(os.Stderr, "admin %s: %v\n", path, err) + printCmdMsg("admin", fmt.Sprintf("%s: %v", path, err)) return 1 } if code >= 400 { - fmt.Fprintf(os.Stderr, "admin %s HTTP %d\n", used, code) + printCmdMsg("admin", fmt.Sprintf("%s HTTP %d", used, code)) writeBody(body) return 1 } diff --git a/cmd/sc/cmds_cms.go b/cmd/sc/cmds_cms.go index 339033f..8265c0b 100644 --- a/cmd/sc/cmds_cms.go +++ b/cmd/sc/cmds_cms.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "os" "strconv" "github.com/Code0987/supercache/pkg/client" @@ -12,7 +11,7 @@ import ( // cmdCMSIncr is `cmsincr [n]`. n optional, default 1. 0 means 1. func cmdCMSIncr(ctx context.Context, sess *session, args []string) int { if len(args) < 2 || len(args) > 3 { - fmt.Fprintln(os.Stderr, "usage: cmsincr [n]") + printUsageLine("usage: cmsincr [n]") return 2 } name := args[0] @@ -21,7 +20,7 @@ func cmdCMSIncr(ctx context.Context, sess *session, args []string) int { if len(args) == 3 { parsed, err := strconv.ParseUint(args[2], 10, 64) if err != nil { - fmt.Fprintf(os.Stderr, "cmsincr: bad n %q\n", args[2]) + printCmdMsg("cmsincr", fmt.Sprintf("bad n %q", args[2])) return 2 } n = parsed @@ -30,11 +29,11 @@ func cmdCMSIncr(ctx context.Context, sess *session, args []string) int { return cli.CMSIncr(ctx, sess.cfg.keyspace, name, []byte(item), n) }) if err != nil { - fmt.Fprintf(os.Stderr, "cmsincr: %v\n", err) + printCmdErr("cmsincr", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK cmsincr %s %s %d\n", name, item, n) + printOK(fmt.Sprintf("cmsincr %s %s %d", name, item, n)) } return 0 } @@ -42,7 +41,7 @@ func cmdCMSIncr(ctx context.Context, sess *session, args []string) int { // cmdCMSQuery is `cmsquery `. Miss → "(nil)" + exit 1. func cmdCMSQuery(ctx context.Context, sess *session, args []string) int { if len(args) != 2 { - fmt.Fprintln(os.Stderr, "usage: cmsquery ") + printUsageLine("usage: cmsquery ") return 2 } var n uint64 @@ -53,11 +52,11 @@ func cmdCMSQuery(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "cmsquery: %v\n", err) + printCmdErr("cmsquery", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } fmt.Println(n) diff --git a/cmd/sc/cmds_topk.go b/cmd/sc/cmds_topk.go index 0b40d1e..1114daf 100644 --- a/cmd/sc/cmds_topk.go +++ b/cmd/sc/cmds_topk.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "os" "github.com/Code0987/supercache/pkg/client" ) @@ -11,7 +10,7 @@ import ( // cmdTopKAdd is `topkadd ` — one TopKAdd RPC per item. func cmdTopKAdd(ctx context.Context, sess *session, args []string) int { if len(args) < 2 { - fmt.Fprintln(os.Stderr, "usage: topkadd ") + printUsageLine("usage: topkadd ") return 2 } name := args[0] @@ -20,11 +19,11 @@ func cmdTopKAdd(ctx context.Context, sess *session, args []string) int { return cli.TopKAdd(ctx, sess.cfg.keyspace, name, []byte(item)) }) if err != nil { - fmt.Fprintf(os.Stderr, "topkadd: %v\n", err) + printCmdErr("topkadd", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK topkadd %s %s\n", name, item) + printOK(fmt.Sprintf("topkadd %s %s", name, item)) } } return 0 @@ -33,7 +32,7 @@ func cmdTopKAdd(ctx context.Context, sess *session, args []string) int { // cmdTopKList is `topklist `. Miss → "(nil)" + exit 1. Live empty chart → exit 0, no lines. func cmdTopKList(ctx context.Context, sess *session, args []string) int { if len(args) != 1 { - fmt.Fprintln(os.Stderr, "usage: topklist ") + printUsageLine("usage: topklist ") return 2 } var rows []client.TopKEntry @@ -44,11 +43,11 @@ func cmdTopKList(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "topklist: %v\n", err) + printCmdErr("topklist", err) return 1 } if !ok { - fmt.Println("(nil)") + printNil() return 1 } for _, r := range rows { diff --git a/cmd/sc/cmds_vecset.go b/cmd/sc/cmds_vecset.go index 3aef211..2742904 100644 --- a/cmd/sc/cmds_vecset.go +++ b/cmd/sc/cmds_vecset.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "os" "strconv" "strings" @@ -29,60 +28,60 @@ func parseVec(s string) ([]float32, error) { func cmdVAdd(ctx context.Context, sess *session, args []string) int { if len(args) != 3 { - fmt.Fprintln(os.Stderr, "usage: vadd ") + printUsageLine("usage: vadd ") return 2 } vec, err := parseVec(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "vadd: bad vec %q\n", args[2]) + printCmdMsg("vadd", fmt.Sprintf("bad vec %q", args[2])) return 2 } err = sess.withClient(func(cli *client.Client, _ string) error { return cli.VAdd(ctx, sess.cfg.keyspace, args[0], []byte(args[1]), vec) }) if err != nil { - fmt.Fprintf(os.Stderr, "vadd: %v\n", err) + printCmdErr("vadd", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK vadd %s %s\n", args[0], args[1]) + printOK(fmt.Sprintf("vadd %s %s", args[0], args[1])) } return 0 } func cmdVRem(ctx context.Context, sess *session, args []string) int { if len(args) != 2 { - fmt.Fprintln(os.Stderr, "usage: vrem ") + printUsageLine("usage: vrem ") return 2 } err := sess.withClient(func(cli *client.Client, _ string) error { return cli.VRem(ctx, sess.cfg.keyspace, args[0], []byte(args[1])) }) if err != nil { - fmt.Fprintf(os.Stderr, "vrem: %v\n", err) + printCmdErr("vrem", err) return 1 } if !sess.cfg.quiet { - fmt.Printf("OK vrem %s %s\n", args[0], args[1]) + printOK(fmt.Sprintf("vrem %s %s", args[0], args[1])) } return 0 } func cmdVSim(ctx context.Context, sess *session, args []string) int { if len(args) < 2 || len(args) > 3 { - fmt.Fprintln(os.Stderr, "usage: vsim [k]") + printUsageLine("usage: vsim [k]") return 2 } vec, err := parseVec(args[1]) if err != nil { - fmt.Fprintf(os.Stderr, "vsim: bad vec %q\n", args[1]) + printCmdMsg("vsim", fmt.Sprintf("bad vec %q", args[1])) return 2 } k := 0 if len(args) == 3 { parsed, err := strconv.Atoi(args[2]) if err != nil { - fmt.Fprintf(os.Stderr, "vsim: bad k %q\n", args[2]) + printCmdMsg("vsim", fmt.Sprintf("bad k %q", args[2])) return 2 } k = parsed @@ -94,7 +93,7 @@ func cmdVSim(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "vsim: %v\n", err) + printCmdErr("vsim", err) return 1 } for _, h := range hits { @@ -105,7 +104,7 @@ func cmdVSim(ctx context.Context, sess *session, args []string) int { func cmdVCard(ctx context.Context, sess *session, args []string) int { if len(args) != 1 { - fmt.Fprintln(os.Stderr, "usage: vcard ") + printUsageLine("usage: vcard ") return 2 } var n int @@ -115,7 +114,7 @@ func cmdVCard(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "vcard: %v\n", err) + printCmdErr("vcard", err) return 1 } fmt.Println(n) @@ -124,7 +123,7 @@ func cmdVCard(ctx context.Context, sess *session, args []string) int { func cmdVDim(ctx context.Context, sess *session, args []string) int { if len(args) != 1 { - fmt.Fprintln(os.Stderr, "usage: vdim ") + printUsageLine("usage: vdim ") return 2 } var dim int @@ -135,11 +134,11 @@ func cmdVDim(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "vdim: %v\n", err) + printCmdErr("vdim", err) return 1 } if !ok { - fmt.Println("missing") + printNil() return 1 } fmt.Println(dim) @@ -148,7 +147,7 @@ func cmdVDim(ctx context.Context, sess *session, args []string) int { func cmdVEmb(ctx context.Context, sess *session, args []string) int { if len(args) != 2 { - fmt.Fprintln(os.Stderr, "usage: vemb ") + printUsageLine("usage: vemb ") return 2 } var vec []float32 @@ -159,11 +158,11 @@ func cmdVEmb(ctx context.Context, sess *session, args []string) int { return e }) if err != nil { - fmt.Fprintf(os.Stderr, "vemb: %v\n", err) + printCmdErr("vemb", err) return 1 } if !ok { - fmt.Println("missing") + printNil() return 1 } parts := make([]string, len(vec)) diff --git a/cmd/sc/help.go b/cmd/sc/help.go new file mode 100644 index 0000000..c48e38d --- /dev/null +++ b/cmd/sc/help.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "os" +) + +type helpGroup struct { + label string + cmds string +} + +var cacheHelpGroups = []helpGroup{ + {"kv", "get put del"}, + {"set", "sadd srem sismember scard smembers"}, + {"bloom", "add|test"}, + {"zset", "zadd zrem zscore zcard zrange zrangebyscore"}, + {"geo", "geoadd georem geopos geocard geodist georadius"}, + {"list", "lpush rpush lpop rpop llen lindex lrange"}, + {"hash", "hset hget hdel hexists hlen hgetall"}, + {"counter", "incr cget"}, + {"json", "jsonset jsonget jsondel"}, + {"bitmap", "bitset bitget bitcount bitpos"}, + {"hll", "hlladd hllcount"}, + {"topk", "topkadd topklist"}, + {"cms", "cmsincr cmsquery"}, + {"vector", "vadd vrem vsim vcard vdim vemb"}, + {"admin", "ping peers keyspaces metrics health ready"}, +} + +func printUsage() { + printAppHelp(os.Stderr, false) +} + +func printREPLHelp() { + printAppHelp(os.Stdout, true) +} + +func printAppHelp(w *os.File, repl bool) { + if !repl { + fmt.Fprintln(w, paint(w, ansiBold+ansiCyan, fmt.Sprintf("sc — SuperCache CLI (%s)", version))) + fmt.Fprintln(w) + printHelpHeading(w, "Usage") + fmt.Fprintln(w, " sc [flags] [args]") + fmt.Fprintln(w, " sc # REPL on a TTY") + fmt.Fprintln(w) + } + + printHelpHeading(w, "Commands") + printHelpGroups(w, cacheHelpGroups) + fmt.Fprintln(w) + printHelpHeading(w, "REPL") + fmt.Fprintln(w, " keyspace seeds connect ttl json timeout clear help version quit") + + if !repl { + fmt.Fprintln(w) + printHelpHeading(w, "Flags") + fmt.Fprintln(w, " -addr -admin -keyspace -timeout -ttl -file -json -q -raw -base64") + fmt.Fprintln(w, " "+paint(w, ansiDim, "env SC_ADDR SC_ADMIN SC_KEYSPACE SC_TLS_*")) + fmt.Fprintln(w) + printHelpHeading(w, "Examples") + fmt.Fprintln(w, ` sc put greeting "hello"`) + fmt.Fprintln(w, " sc get greeting") + fmt.Fprintln(w, " sc -keyspace set sadd features dark_mode") + } + + fmt.Fprintln(w) + fmt.Fprintln(w, paint(w, ansiDim, "run a command with no args for usage. miss=(nil) mutations=OK ")) +} + +func printHelpHeading(w *os.File, s string) { + fmt.Fprintln(w, paint(w, ansiBold+ansiCyan, s)) +} + +func printHelpGroups(w *os.File, groups []helpGroup) { + width := 0 + for _, g := range groups { + if n := len(g.label); n > width { + width = n + } + } + for _, g := range groups { + label := paint(w, ansiCyan, fmt.Sprintf("%-*s", width, g.label)) + fmt.Fprintf(w, " %s %s\n", label, g.cmds) + } +} diff --git a/cmd/sc/main.go b/cmd/sc/main.go index 93a6db9..72d2e11 100644 --- a/cmd/sc/main.go +++ b/cmd/sc/main.go @@ -63,7 +63,7 @@ func run(args []string) int { if cmd == "" { _ = fs.Parse(flagArgs) if wantHelp || !stdinIsTTY() { - printUsage(fs) + printUsage() if wantHelp { return 0 } @@ -79,15 +79,15 @@ func run(args []string) int { if err := fs.Parse(flagArgs); err != nil { if errors.Is(err, flag.ErrHelp) { - printUsage(fs) + printUsage() return 0 } fmt.Fprintln(os.Stderr, err) - printUsage(fs) + printNote("type help for commands") return 2 } if wantHelp && (cmd == "help" || cmd == "-h" || cmd == "--help") { - printUsage(fs) + printUsage() return 0 } @@ -120,8 +120,8 @@ func run(args []string) int { defer cancel() return dispatch(ctx, sess, cmd, posArgs) default: - fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd) - printUsage(fs) + printUnknown(cmd) + printNote("type help for commands") return 2 } } @@ -161,6 +161,7 @@ func configFromFlags( cfg.ttl = 0 cfg.ttlSet = true } + applyJSONColor(cfg.jsonOut) return cfg, nil } @@ -293,105 +294,3 @@ func splitArgs(args []string) (cmd string, flagArgs, posArgs []string, err error } return cmd, flagArgs, posArgs, nil } - -func printUsage(fs *flag.FlagSet) { - w := os.Stderr - fmt.Fprintf(w, `sc — SuperCache CLI (%s) - -Usage: - sc [flags] [args] - sc [flags] # interactive REPL (TTY) - sc [flags] repl - -Cache commands (gRPC -addr seeds): - get [key...] Get key(s); exit 1 if any missing - put Put string value - put -file Put file bytes (-file - = stdin) - del [key...] Delete key(s) cluster-wide (best-effort) - set ... Alias for put (KV; not ModeSet) - bloom add|test ModeBloom membership - sadd ModeSet add - srem ModeSet remove - sismember ModeSet contains (true/false) - scard ModeSet member count - smembers ModeSet list members - zadd ModeZSet upsert score - zrem ModeZSet remove member - zscore ModeZSet score (or (nil)) - zcard ModeZSet member count - zrange ModeZSet by rank (Redis-style) - zrangebyscore - geoadd ModeGeo upsert - georem ModeGeo remove - geopos ModeGeo lon lat (or (nil)) - geocard ModeGeo member count - geodist ModeGeo meters (or (nil)) - georadius [limit] - lpush|rpush ModeList push - lpop|rpop ModeList pop (or (nil)) - llen ModeList length - lindex ModeList element - lrange ModeList window - hset ModeHash upsert (value may contain spaces) - hget ModeHash get (or (nil)) - hdel ModeHash delete field - hexists ModeHash exists (true/false) - hlen ModeHash field count - hgetall ModeHash fieldvalue lines - incr [delta] ModeCounter add (default 1); print new value - cget ModeCounter get (or (nil)) - jsonset ModeJSON upsert (quoted JSON: '"Ada"') - jsonget [path] ModeJSON get (or (nil)); omitted path = $ - jsondel [path] ModeJSON delete path; omitted path = $ (clear to {}) - bitset <0|1> ModeBitmap SETBIT - bitget ModeBitmap GETBIT (0/1 or (nil)) - bitcount [start end] ModeBitmap BITCOUNT (byte window; omitted = 0 -1) - bitpos <0|1> [start end] ModeBitmap BITPOS - hlladd ModeHLL add (hashed, not stored) - hllcount ModeHLL estimate (or (nil)) - topkadd ModeTopK observe (+1 per item) - topklist ModeTopK chart (item count lines, or (nil)) - cmsincr [n] ModeCMS increment (n optional, default 1) - cmsquery ModeCMS estimate (or (nil)) - vadd ModeVectorSet upsert - vrem ModeVectorSet remove - vsim [k] ModeVectorSet neighbors - vcard ModeVectorSet member count - vdim ModeVectorSet dim (or missing) - vemb ModeVectorSet vector (or missing) - ping Dial cache seeds (+ admin /healthz) - -Admin commands (HTTP -admin seeds): - peers | keyspaces | metrics | health | ready - -Other: - repl Interactive shell - version | help - -Multi-seed: - -addr host1:9000,host2:9010 try seeds in order; sticky last success; fail over on dial errors - Puts still go to the ring owner via ForwardPut — seeds are only the client entry point. - -Global flags: -`, version) - fs.SetOutput(w) - fs.PrintDefaults() - fmt.Fprintf(w, ` -Environment: SC_ADDR, SC_ADMIN, SC_KEYSPACE, SC_TLS_CA, SC_TLS_CERT, SC_TLS_KEY, SC_TLS_SERVER_NAME - -Examples: - sc put greeting "hello world" - sc get greeting - sc -keyspace set sadd features dark_mode - sc -keyspace set sismember features dark_mode - sc -keyspace zset zadd lb 100 alice - sc -keyspace zset zrange lb 0 -1 - sc -addr 127.0.0.1:9000,127.0.0.1:9010,127.0.0.1:9020 ping - sc -admin 127.0.0.1:8081,127.0.0.1:8082 peers - sc # REPL - sc> put k v - sc> get k - sc> keyspace cacheonly - sc> quit -`) -} diff --git a/cmd/sc/repl.go b/cmd/sc/repl.go index a8650cf..8f3db0c 100644 --- a/cmd/sc/repl.go +++ b/cmd/sc/repl.go @@ -13,14 +13,17 @@ func runREPL(cfg *config) int { sess := newSession(cfg) defer sess.Close() + applyJSONColor(cfg.jsonOut) + // Eager dial so the first prompt shows the connected seed. if _, addr, err := sess.Client(); err != nil { - fmt.Fprintf(os.Stderr, "warning: not connected yet: %v\n", err) + printWarn(fmt.Sprintf("not connected yet: %v", err)) fmt.Fprintf(os.Stderr, "seeds: %s\n", strings.Join(cfg.addrs, ", ")) } else { - fmt.Printf("connected %s keyspace=%s seeds=%d\n", addr, cfg.keyspace, len(cfg.addrs)) + fmt.Printf("%s %s keyspace=%s seeds=%d\n", + paint(os.Stdout, ansiGreen, "connected"), addr, cfg.keyspace, len(cfg.addrs)) } - fmt.Println("type help for commands, quit to exit") + fmt.Println(paint(os.Stdout, ansiDim, "type help for commands, quit to exit")) in := bufio.NewScanner(os.Stdin) // Allow long put lines @@ -31,7 +34,7 @@ func runREPL(cfg *config) int { if addr == "" { addr = "?" } - fmt.Printf("sc %s@%s> ", cfg.keyspace, shortAddr(addr)) + fmt.Print(colorPrompt(cfg.keyspace, shortAddr(addr))) if !in.Scan() { fmt.Println() break @@ -42,7 +45,7 @@ func runREPL(cfg *config) int { } args, err := tokenize(line) if err != nil { - fmt.Fprintf(os.Stderr, "parse: %v\n", err) + printCmdErr("parse", err) continue } if len(args) == 0 { @@ -62,11 +65,11 @@ func runREPL(cfg *config) int { continue case "keyspace", "use", "ks": if len(rest) != 1 { - fmt.Fprintln(os.Stderr, "usage: keyspace ") + printUsageLine("usage: keyspace ") continue } cfg.keyspace = rest[0] - fmt.Printf("keyspace = %s\n", cfg.keyspace) + fmt.Printf("keyspace = %s\n", paint(os.Stdout, ansiBold, cfg.keyspace)) continue case "seeds", "addrs": fmt.Printf("cache seeds (%d):\n", len(cfg.addrs)) @@ -108,14 +111,14 @@ func runREPL(cfg *config) int { sess.seedIdx = found sess.mu.Unlock() } else { - fmt.Fprintf(os.Stderr, "unknown seed %q (use seeds to list)\n", target) + printCmdMsg("connect", fmt.Sprintf("unknown seed %q (use seeds to list)", target)) continue } } if _, addr, err := sess.Client(); err != nil { - fmt.Fprintf(os.Stderr, "connect: %v\n", err) + printCmdErr("connect", err) } else { - fmt.Printf("connected %s\n", addr) + fmt.Printf("%s %s\n", paint(os.Stdout, ansiGreen, "connected"), addr) } continue case "ttl": @@ -135,7 +138,7 @@ func runREPL(cfg *config) int { } d, err := time.ParseDuration(rest[0]) if err != nil { - fmt.Fprintf(os.Stderr, "ttl: %v\n", err) + printCmdErr("ttl", err) continue } cfg.ttl = d @@ -150,17 +153,18 @@ func runREPL(cfg *config) int { case "off", "0", "false": cfg.jsonOut = false default: - fmt.Fprintln(os.Stderr, "usage: json [on|off]") + printUsageLine("usage: json [on|off]") continue } } + applyJSONColor(cfg.jsonOut) fmt.Printf("json = %v\n", cfg.jsonOut) continue case "timeout": if len(rest) == 1 { d, err := time.ParseDuration(rest[0]) if err != nil { - fmt.Fprintf(os.Stderr, "timeout: %v\n", err) + printCmdErr("timeout", err) continue } cfg.timeout = d @@ -180,16 +184,17 @@ func runREPL(cfg *config) int { // Inline flags for a single REPL command: put k -file x, get k -base64, etc. cmdArgs, err := applyREPLFlags(cfg, rest) if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + printCmdErr("sc", err) continue } + applyJSONColor(cfg.jsonOut) ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout) _ = dispatch(ctx, sess, cmd, cmdArgs) cancel() } if err := in.Err(); err != nil { - fmt.Fprintf(os.Stderr, "stdin: %v\n", err) + printCmdErr("stdin", err) return 1 } return 0 @@ -206,40 +211,6 @@ func shortAddr(addr string) string { return addr } -func printREPLHelp() { - fmt.Print(`REPL commands: - get [key...] Get - put Put (quotes ok: put k "hello world") - put -file Put file bytes - del [key...] Delete - sadd|srem|sismember|scard|smembers ModeSet - bloom add|test ModeBloom - zadd|zrem|zscore|zcard|zrange|zrangebyscore ModeZSet - geoadd|georem|geopos|geocard|geodist|georadius ModeGeo - lpush|rpush|lpop|rpop|llen|lindex|lrange ModeList - hset|hget|hdel|hexists|hlen|hgetall ModeHash - incr|cget ModeCounter - jsonset|jsonget|jsondel ModeJSON - bitset|bitget|bitcount|bitpos ModeBitmap - hlladd|hllcount ModeHLL - topkadd|topklist ModeTopK - cmsincr|cmsquery ModeCMS - vadd|vrem|vsim|vcard|vdim|vemb ModeVectorSet - ping Cache + admin health - peers | keyspaces | metrics | health | ready - -Session: - keyspace Switch keyspace (alias: use, ks) - seeds List cache/admin seeds (* = active) - connect [seed] Re-dial (optional seed / substring) - ttl [duration|default] Show/set put TTL - json [on|off] JSON output mode - timeout [duration] Request timeout - clear Clear screen - help | version | quit -`) -} - // applyREPLFlags peels common flags out of REPL args for one command. func applyREPLFlags(cfg *config, args []string) ([]string, error) { var out []string diff --git a/cmd/sc/style.go b/cmd/sc/style.go new file mode 100644 index 0000000..18b07dd --- /dev/null +++ b/cmd/sc/style.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +const ( + ansiReset = "\033[0m" + ansiBold = "\033[1m" + ansiDim = "\033[2m" + ansiRed = "\033[31m" + ansiGreen = "\033[32m" + ansiYellow = "\033[33m" + ansiCyan = "\033[36m" +) + +// disableColor is set when -json is on. NO_COLOR / non-TTY also suppress color. +var disableColor bool + +func applyJSONColor(jsonOut bool) { + disableColor = jsonOut +} + +func colorOn(f *os.File) bool { + if disableColor { + return false + } + if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" { + return false + } + st, err := f.Stat() + if err != nil { + return false + } + return st.Mode()&os.ModeCharDevice != 0 +} + +func paint(f *os.File, code, s string) string { + if !colorOn(f) { + return s + } + return code + s + ansiReset +} + +func printOK(msg string) { + fmt.Println(paint(os.Stdout, ansiGreen+ansiBold, "OK") + " " + msg) +} + +func printNil() { + fmt.Println(paint(os.Stdout, ansiDim, "(nil)")) +} + +func printBool(v bool) { + if v { + fmt.Println(paint(os.Stdout, ansiGreen, "true")) + return + } + fmt.Println(paint(os.Stdout, ansiDim, "false")) +} + +func printCmdErr(cmd string, err error) { + fmt.Fprintf(os.Stderr, "%s: %v\n", paint(os.Stderr, ansiRed+ansiBold, cmd), err) +} + +func printCmdMsg(cmd, msg string) { + fmt.Fprintf(os.Stderr, "%s: %s\n", paint(os.Stderr, ansiRed+ansiBold, cmd), msg) +} + +func printUnknown(cmd string) { + fmt.Fprintf(os.Stderr, "%s: unknown command %q\n", paint(os.Stderr, ansiRed+ansiBold, "sc"), cmd) +} + +func printUsageLine(line string) { + if !strings.HasPrefix(line, "usage:") { + fmt.Fprintln(os.Stderr, line) + return + } + rest := strings.TrimPrefix(line, "usage:") + fmt.Fprintln(os.Stderr, paint(os.Stderr, ansiYellow+ansiBold, "usage:")+rest) +} + +func printWarn(msg string) { + fmt.Fprintln(os.Stderr, paint(os.Stderr, ansiYellow+ansiBold, "warning:")+" "+msg) +} + +func printNote(msg string) { + fmt.Fprintln(os.Stderr, paint(os.Stderr, ansiDim, "# "+msg)) +} + +func colorPrompt(ks, addr string) string { + return paint(os.Stdout, ansiCyan, "sc") + " " + + paint(os.Stdout, ansiBold, ks) + paint(os.Stdout, ansiDim, "@"+addr) + "> " +} diff --git a/cmd/sc/style_test.go b/cmd/sc/style_test.go new file mode 100644 index 0000000..b66ee4a --- /dev/null +++ b/cmd/sc/style_test.go @@ -0,0 +1,206 @@ +package main + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/Code0987/supercache/internal/cacheserver" + "github.com/Code0987/supercache/pkg/engine" + "github.com/Code0987/supercache/pkg/keyspace" +) + +func TestColorOffWhenNoColor(t *testing.T) { + t.Setenv("NO_COLOR", "1") + t.Setenv("TERM", "xterm") + disableColor = false + if colorOn(os.Stdout) { + t.Fatal("NO_COLOR should disable color") + } + if got := paint(os.Stdout, ansiGreen, "OK"); got != "OK" { + t.Fatalf("paint=%q", got) + } +} + +func TestColorOffWhenDumbTerm(t *testing.T) { + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "dumb") + disableColor = false + if colorOn(os.Stdout) { + t.Fatal("TERM=dumb should disable color") + } +} + +func TestColorOffWhenJSON(t *testing.T) { + t.Setenv("NO_COLOR", "") + t.Setenv("TERM", "xterm") + applyJSONColor(true) + t.Cleanup(func() { applyJSONColor(false) }) + if colorOn(os.Stdout) { + t.Fatal("-json should disable color") + } +} + +func TestPrintHelpersPlain(t *testing.T) { + applyJSONColor(false) + out, _ := captureOut(func() int { + printOK("put k (3 bytes)") + printNil() + printBool(true) + printBool(false) + return 0 + }) + got := strings.Split(strings.TrimSpace(out), "\n") + want := []string{"OK put k (3 bytes)", "(nil)", "true", "false"} + if strings.Join(got, "\n") != strings.Join(want, "\n") { + t.Fatalf("out=%q want %q", out, strings.Join(want, "\n")) + } + if strings.Contains(out, "\033") { + t.Fatal("piped stdout must not contain ANSI") + } +} + +func TestUsageAndUnknownText(t *testing.T) { + errOut, code := captureErr(func() int { + return run([]string{"get"}) + }) + if code != 2 { + t.Fatalf("get usage: exit %d", code) + } + if !strings.Contains(errOut, "usage: get [key...]") { + t.Fatalf("get usage: %q", errOut) + } + + errOut, code = captureErr(func() int { + printUnknown("nope") + return 2 + }) + if code != 2 || !strings.Contains(errOut, `sc: unknown command "nope"`) { + t.Fatalf("unknown: exit %d out=%q", code, errOut) + } + + errOut, code = captureErr(func() int { + printCmdMsg("put", "bad base64") + printWarn("peer failed") + printUsageLine("usage: del [key...]") + return 0 + }) + if code != 0 { + t.Fatal(code) + } + if !strings.Contains(errOut, "put: bad base64") || + !strings.Contains(errOut, "warning: peer failed") || + !strings.Contains(errOut, "usage: del [key...]") { + t.Fatalf("stderr=%q", errOut) + } +} + +func TestCompactHelp(t *testing.T) { + errOut, code := captureErr(func() int { + return run([]string{"help"}) + }) + if code != 0 { + t.Fatalf("help: exit %d", code) + } + for _, want := range []string{ + "sc — SuperCache CLI", + "Usage", + "Commands", + "get put del", + "vadd vrem vsim", + "Flags", + "Examples", + } { + if !strings.Contains(errOut, want) { + t.Fatalf("missing %q in help:\n%s", want, errOut) + } + } + for _, no := range []string{ + "ModeZSet upsert", + "Put string value", + "ForwardPut", + "Global flags:", + "sc cacheonly@:9000>", + } { + if strings.Contains(errOut, no) { + t.Fatalf("clutter %q still in help:\n%s", no, errOut) + } + } + if n := strings.Count(errOut, "\n"); n > 45 { + t.Fatalf("help too long: %d lines\n%s", n, errOut) + } +} + +func TestREPLHelpCompact(t *testing.T) { + out, _ := captureOut(func() int { + printREPLHelp() + return 0 + }) + if !strings.Contains(out, "get put del") || !strings.Contains(out, "keyspace") { + t.Fatalf("repl help:\n%s", out) + } + if strings.Contains(out, "Usage") || strings.Contains(out, "-addr") { + t.Fatalf("repl help should skip one-shot usage/flags:\n%s", out) + } +} + +func TestColorPromptPlain(t *testing.T) { + applyJSONColor(false) + got := colorPrompt("cacheonly", ":9000") + if got != "sc cacheonly@:9000> " { + t.Fatalf("prompt=%q", got) + } +} + +func TestGetMissAndPutOKText(t *testing.T) { + eng := engine.New() + defer eng.Close() + if err := eng.UpdateKeySpace(keyspace.Config{ + Name: "cacheonly", Mode: keyspace.ModeCacheOnly, MaxBytes: 1 << 20, TTL: time.Hour, + }); err != nil { + t.Fatal(err) + } + gs, lis, err := cacheserver.ListenAndServe("127.0.0.1:0", eng) + if err != nil { + t.Fatal(err) + } + defer gs.Stop() + addr := lis.Addr().String() + + out, code := captureOut(func() int { + return run([]string{"-addr", addr, "-keyspace", "cacheonly", "get", "missing"}) + }) + if code != 1 || strings.TrimSpace(out) != "(nil)" { + t.Fatalf("get miss: exit %d out=%q", code, out) + } + + out, code = captureOut(func() int { + return run([]string{"-addr", addr, "-keyspace", "cacheonly", "put", "k", "abc"}) + }) + if code != 0 || strings.TrimSpace(out) != "OK put k (3 bytes)" { + t.Fatalf("put: exit %d out=%q", code, out) + } + + out, code = captureOut(func() int { + return run([]string{"-addr", addr, "-keyspace", "cacheonly", "del", "k"}) + }) + if code != 0 || strings.TrimSpace(out) != "OK del k" { + t.Fatalf("del: exit %d out=%q", code, out) + } +} + +func captureErr(fn func() int) (string, int) { + old := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + code := fn() + _ = w.Close() + os.Stderr = old + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + _ = r.Close() + return buf.String(), code +}