Skip to content

Commit

Permalink
Improve db command help
Browse files Browse the repository at this point in the history
Add a long-form help text for the db command that is shown
with 'help db'.

Signed-off-by: Jussi Maki <[email protected]>
  • Loading branch information
joamaki committed Nov 7, 2024
1 parent 132edd6 commit ddc4f15
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 21 deletions.
205 changes: 184 additions & 21 deletions script.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,66 @@ import (
"gopkg.in/yaml.v3"
)

func underline(s string) string {
return "\033[4m" + s + "\033[0m"
}

func usageDetails(cmds map[string]script.Cmd, sortedNames []string) (out []string) {
out = strings.Split(`DESCRIPTION
The 'db' command allows inspecting and manipulating
StateDB tables. Here's an example to get you started:
> db tables
Name Object count Zombie objects Indexes ...
example 2 0 id, x ...
> db show example
Name X
one 1
two 2
> db prefix -index=id example o
Name X
one 1
> db insert example three.yaml four.yaml
> db delete example three.yaml
COMMANDS
`, "\n")

for _, name := range sortedNames {
cmd := cmds[name]
u := cmd.Usage()
cmdLine := underline(name) + " " + u.Args
// Do manual line wrapping to indent nicely when wrapped.
const wrap = 60 /* wrap mark in script */ - 8 /* indent */
dots := strings.Repeat(".", len(name))
if len(cmdLine) > wrap {
idx := strings.LastIndex(cmdLine[:wrap], " ")
out = append(out, cmdLine[:idx])
rest := cmdLine[idx:]
for {
if len(rest) < wrap {
out = append(out, dots+rest)
break
}
idx := strings.LastIndex(rest[:wrap], " ")
out = append(out, dots+rest[:idx])
rest = rest[idx:]
}
} else {
out = append(out, cmdLine)
}
out = append(out, "")
out = append(out, u.Detail...)
out = append(out, "")
}

return out
}

func ScriptCommands(db *DB) hive.ScriptCmdOut {
subCmds := map[string]script.Cmd{
"tables": TablesCmd(db),
Expand All @@ -39,26 +99,25 @@ func ScriptCommands(db *DB) hive.ScriptCmdOut {
"watch": WatchCmd(db),
"initialized": InitializedCmd(db),
}
subCmdsList := strings.Join(slices.Collect(maps.Keys(subCmds)), ", ")
subCmdsNames := slices.Sorted(maps.Keys(subCmds))
subCmdsList := strings.Join(subCmdsNames, ", ")
return hive.NewScriptCmd(
"db",
script.Command(
script.CmdUsage{
Summary: "Inspect and manipulate StateDB",
Summary: "Inspect and manipulate StateDB ('help db' for usage)",
Args: "cmd args...",
Detail: []string{
"Supported commands: " + subCmdsList,
},
Detail: usageDetails(subCmds, subCmdsNames),
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
if len(args) < 1 {
return nil, fmt.Errorf("expected command (%s)", subCmdsList)
return nil, fmt.Errorf("expected command (%s), see 'help db'", subCmdsList)
}
cmd, ok := subCmds[args[0]]
if !ok {
return nil, fmt.Errorf("command not found, expected one of %s", subCmdsList)
return nil, fmt.Errorf("command %q not found, expected one of: %s, see 'help db'", args[0], subCmdsList)
}
wf, err := cmd.Run(s, args[1:]...)
wf, err := cmd.Run(s, sortedArgs(args[1:])...)
if errors.Is(err, errUsage) {
s.Logf("usage: db %s %s\n", args[0], cmd.Usage().Args)
}
Expand All @@ -73,14 +132,24 @@ var errUsage = errors.New("bad arguments")
func TablesCmd(db *DB) script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "Show StateDB tables",
Args: "table",
Summary: "List StateDB tables",
Detail: []string{
"List each registered table.",
"The following details are shown:",
"- Name: The name of the table as given to 'NewTable'",
"- Object count: Objects in the table",
"- Zombie objects: Deleted, but not observed objects",
"- Indexes: The indexes specified for the table",
"- Initializers: Pending table initializers",
"- Go type: The Go type, the T in Table[T]",
"- Last WriteTxn: The current/last write against the table",
},
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
txn := db.ReadTxn()
tbls := db.GetTables(txn)
w := newTabWriter(s.LogWriter())
fmt.Fprintf(w, "Name\tObject count\tDeleted objects\tIndexes\tInitializers\tGo type\tLast WriteTxn\n")
fmt.Fprintf(w, "Name\tObject count\tZombie objects\tIndexes\tInitializers\tGo type\tLast WriteTxn\n")
for _, tbl := range tbls {
idxs := strings.Join(tbl.Indexes(), ", ")
fmt.Fprintf(w, "%s\t%d\t%d\t%s\t%v\t%T\t%s\n",
Expand All @@ -103,7 +172,15 @@ func InitializedCmd(db *DB) script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "Wait until all or specific tables have been initialized",
Args: "(-timeout=<duration>) table...",
Args: "[-timeout=<duration>] table...",
Detail: []string{
"Waits until all or specific tables have been marked",
"initialized. The default timeout is 5 seconds.",
"",
"This command is useful in tests where you might need to wait",
"for e.g. a background reflector to have started watching before",
"inserting objects.",
},
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
txn := db.ReadTxn()
Expand Down Expand Up @@ -160,8 +237,21 @@ func InitializedCmd(db *DB) script.Cmd {
func ShowCmd(db *DB) script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "Show table",
Args: "(-o=<file>) (-columns=col1,...) (-format={table,yaml,json}) table",
Summary: "Show the contents of a table",
Args: "[-o=<file>] [-columns=col1,...] [-format={table,yaml,json}] table",
Detail: []string{
"Show the contents of a table.",
"",
"The contents are written to stdout, but can be written to",
"a file instead with the -o flag.",
"",
"By default the table is shown in the table format.",
"For YAML use '-format=yaml' and for JSON use '-format=json'",
"",
"To only show specific columns use the '-columns' flag. The",
"columns are as specified by 'TableHeader()' method.",
"This flag is only supported with 'table' formatting.",
},
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
flags := newCmdFlagSet()
Expand Down Expand Up @@ -209,7 +299,20 @@ func CompareCmd(db *DB) script.Cmd {
return script.Command(
script.CmdUsage{
Summary: "Compare table",
Args: "(-timeout=<dur>) (-grep=<pattern>) table file",
Args: "[-timeout=<dur>] [-grep=<pattern>] table file",
Detail: []string{
"Compare the contents of a table against a file.",
"The comparison is retried until a timeout (1s default).",
"",
"The file should be formatted in the same style as",
"the output from 'db show -format=table'. Indentation",
"does not matter as long as header is aligned with the data.",
"",
"Not all columns need to be specified. Remove the columns",
"from the file you do not want compared.",
"",
"The rows can be filtered with the -grep flag.",
},
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
flags := newCmdFlagSet()
Expand Down Expand Up @@ -321,6 +424,10 @@ func InsertCmd(db *DB) script.Cmd {
script.CmdUsage{
Summary: "Insert object into a table",
Args: "table path...",
Detail: []string{
"Insert one or more objects into a table. The input files",
"are expected to be YAML.",
},
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
return insertOrDelete(true, db, s, args...)
Expand All @@ -333,6 +440,11 @@ func DeleteCmd(db *DB) script.Cmd {
script.CmdUsage{
Summary: "Delete an object from the table",
Args: "table path...",
Detail: []string{
"Delete one or more objects from the table. The input files",
"are expected to be YAML and need to specify enough of the",
"object to construct the primary key",
},
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
return insertOrDelete(false, db, s, args...)
Expand Down Expand Up @@ -391,19 +503,44 @@ func insertOrDelete(insert bool, db *DB, s *script.State, args ...string) (scrip
}

func PrefixCmd(db *DB) script.Cmd {
return queryCmd(db, queryCmdPrefix, "Query table by prefix")
return queryCmd(db,
queryCmdPrefix,
"Query table by prefix",
[]string{
"Show all objects that start with the given " + underline("key") + ".",
},
)
}

func LowerBoundCmd(db *DB) script.Cmd {
return queryCmd(db, queryCmdLowerBound, "Query table by lower bound search")
return queryCmd(db,
queryCmdLowerBound,
"Query table by lower bound search",
[]string{
"Show all objects that have a matching key equal or higher",
"than the query " + underline("key") + ".",
},
)
}

func ListCmd(db *DB) script.Cmd {
return queryCmd(db, queryCmdList, "List objects in the table")
return queryCmd(db,
queryCmdList,
"List objects in the table",
[]string{
"Show all objects matching the query key.",
},
)
}

func GetCmd(db *DB) script.Cmd {
return queryCmd(db, queryCmdGet, "Get the first matching object")
return queryCmd(db,
queryCmdGet,
"Get the first matching object",
[]string{
"Show the first object that matches the query key.",
},
)
}

const (
Expand All @@ -413,11 +550,12 @@ const (
queryCmdGet
)

func queryCmd(db *DB, query int, summary string) script.Cmd {
func queryCmd(db *DB, query int, summary string, detail []string) script.Cmd {
return script.Command(
script.CmdUsage{
Summary: summary,
Args: "(-o=<file>) (-columns=col1,...) (-format={table*,yaml,json}) (-index=<index>) table key",
Args: "[-o=<file>] [-columns=col1,...] [-format={table*,yaml,json}] [-index=<index>] table key",
Detail: detail,
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
return runQueryCmd(query, db, s, args)
Expand Down Expand Up @@ -516,6 +654,10 @@ func WatchCmd(db *DB) script.Cmd {
script.CmdUsage{
Summary: "Watch a table for changes",
Args: "table",
Detail: []string{
"Watch a table for changes. Streams each insert or delete",
"that happens to the table.",
},
},
func(s *script.State, args ...string) (script.WaitFunc, error) {
if len(args) < 1 {
Expand Down Expand Up @@ -829,3 +971,24 @@ func newTabWriter(out io.Writer) *tabwriter.Writer {
)
return tabwriter.NewWriter(out, minWidth, width, padding, padChar, flags)
}

// sortArgs sorts the arguments to bring '-arg' first. Allows mixing
// the argument order. If e.g. key starts with '-', then it'll just
// need to be quoted: "db get foo '-mykey'"
func sortedArgs(args []string) []string {
return slices.SortedStableFunc(
slices.Values(args),
func(a, b string) int {
aIsArg := strings.HasPrefix(a, "-")
bIsArg := strings.HasPrefix(b, "-")
switch {
case aIsArg && !bIsArg:
return -1
case bIsArg && !aIsArg:
return 1
default:
return 0
}
},
)
}
21 changes: 21 additions & 0 deletions script_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package statedb

import (
Expand Down Expand Up @@ -109,3 +112,21 @@ func TestHeaderLine(t *testing.T) {
}
}
}

func TestSortedFlags(t *testing.T) {
cases := []struct{ input, expected string }{
{"foo bar", "foo bar"},
{"bar foo", "bar foo"},
{"foo bar -baz=1", "-baz=1 foo bar"},
{"bar foo -baz=1", "-baz=1 bar foo"},
{"foo -baz=1 bar", "-baz=1 foo bar"},
{"-baz=1 foo bar", "-baz=1 foo bar"},
{"-baz=1 foo -quux=2 bar", "-baz=1 -quux=2 foo bar"},
{"-baz=1 bar foo -quux=2", "-baz=1 -quux=2 bar foo"},
}

for _, tc := range cases {
actual := strings.Join(sortedArgs(strings.Split(tc.input, " ")), " ")
assert.Equal(t, tc.expected, actual)
}
}

0 comments on commit ddc4f15

Please sign in to comment.