Skip to content

Commit 7625e06

Browse files
authored
Merge pull request #1026 from lavishpal/migrate-cobra-get
chore(cmd): migrate `get` command to Cobra style
2 parents 9a9e3a4 + 0d01846 commit 7625e06

File tree

5 files changed

+169
-180
lines changed

5 files changed

+169
-180
lines changed

cmd/bbolt/command_get.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
bolt "go.etcd.io/bbolt"
9+
"go.etcd.io/bbolt/errors"
10+
)
11+
12+
type getOptions struct {
13+
parseFormat string
14+
format string
15+
}
16+
17+
func newGetCommand() *cobra.Command {
18+
var opts getOptions
19+
20+
cmd := &cobra.Command{
21+
Use: "get PATH [BUCKET..] KEY",
22+
Short: "get the value of a key from a (sub)bucket in a bbolt database",
23+
Args: cobra.MinimumNArgs(3),
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
path := args[0]
26+
if path == "" {
27+
return ErrPathRequired
28+
}
29+
buckets := args[1 : len(args)-1]
30+
keyStr := args[len(args)-1]
31+
32+
// validate input parameters
33+
if len(buckets) == 0 {
34+
return fmt.Errorf("bucket is required: %w", ErrBucketRequired)
35+
}
36+
37+
key, err := parseBytes(keyStr, opts.parseFormat)
38+
if err != nil {
39+
return err
40+
}
41+
42+
if len(key) == 0 {
43+
return fmt.Errorf("key is required: %w", errors.ErrKeyRequired)
44+
}
45+
46+
return getFunc(cmd, path, buckets, key, opts)
47+
},
48+
}
49+
50+
cmd.Flags().StringVar(&opts.parseFormat, "parse-format", "ascii-encoded", "Input format one of: ascii-encoded|hex")
51+
cmd.Flags().StringVar(&opts.format, "format", "auto", "Output format one of: "+FORMAT_MODES+" (default: auto)")
52+
53+
return cmd
54+
}
55+
56+
// getFunc opens the BoltDB and retrieves the key value from the bucket path.
57+
func getFunc(cmd *cobra.Command, path string, buckets []string, key []byte, opts getOptions) error {
58+
// check if the source DB path is valid
59+
if _, err := checkSourceDBPath(path); err != nil {
60+
return err
61+
}
62+
63+
// open the database
64+
db, err := bolt.Open(path, 0600, &bolt.Options{ReadOnly: true})
65+
if err != nil {
66+
return err
67+
}
68+
defer db.Close()
69+
70+
// access the database and get the value
71+
return db.View(func(tx *bolt.Tx) error {
72+
lastBucket, err := findLastBucket(tx, buckets)
73+
if err != nil {
74+
return err
75+
}
76+
val := lastBucket.Get(key)
77+
if val == nil {
78+
return fmt.Errorf("Error %w for key: %q hex: \"%x\"", ErrKeyNotFound, key, string(key))
79+
}
80+
return writelnBytes(cmd.OutOrStdout(), val, opts.format)
81+
})
82+
}

cmd/bbolt/command_get_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main_test
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
bolt "go.etcd.io/bbolt"
14+
main "go.etcd.io/bbolt/cmd/bbolt"
15+
"go.etcd.io/bbolt/internal/btesting"
16+
)
17+
18+
func TestGetCommand_Run(t *testing.T) {
19+
testCases := []struct {
20+
name string
21+
printable bool
22+
testBucket string
23+
testKey string
24+
expectedValue string
25+
}{
26+
{
27+
name: "printable data",
28+
printable: true,
29+
testBucket: "foo",
30+
testKey: "foo-1",
31+
expectedValue: "value-foo-1\n",
32+
},
33+
{
34+
name: "non printable data",
35+
printable: false,
36+
testBucket: "bar",
37+
testKey: "100001",
38+
expectedValue: hex.EncodeToString(convertInt64IntoBytes(100001)) + "\n",
39+
},
40+
}
41+
42+
for _, tc := range testCases {
43+
t.Run(tc.name, func(t *testing.T) {
44+
t.Logf("Creating test database for subtest '%s'", tc.name)
45+
db := btesting.MustCreateDB(t)
46+
47+
t.Log("Inserting test data")
48+
err := db.Update(func(tx *bolt.Tx) error {
49+
b, err := tx.CreateBucketIfNotExists([]byte(tc.testBucket))
50+
if err != nil {
51+
return fmt.Errorf("create bucket %q: %w", tc.testBucket, err)
52+
}
53+
54+
if tc.printable {
55+
return b.Put([]byte(tc.testKey), []byte("value-"+tc.testKey))
56+
}
57+
58+
return b.Put([]byte(tc.testKey), convertInt64IntoBytes(100001))
59+
})
60+
require.NoError(t, err)
61+
db.Close()
62+
defer requireDBNoChange(t, dbData(t, db.Path()), db.Path())
63+
64+
t.Log("Running get command")
65+
rootCmd := main.NewRootCommand()
66+
outputBuf := bytes.NewBufferString("")
67+
rootCmd.SetOut(outputBuf)
68+
rootCmd.SetArgs([]string{"get", db.Path(), tc.testBucket, tc.testKey})
69+
err = rootCmd.Execute()
70+
require.NoError(t, err)
71+
72+
t.Log("Checking output")
73+
output, err := io.ReadAll(outputBuf)
74+
require.NoError(t, err)
75+
require.Equalf(t, tc.expectedValue, string(output), "unexpected stdout:\n\n%s", string(output))
76+
})
77+
}
78+
}
79+
80+
func TestGetCommand_NoArgs(t *testing.T) {
81+
expErr := errors.New("requires at least 3 arg(s), only received 0")
82+
rootCmd := main.NewRootCommand()
83+
rootCmd.SetArgs([]string{"get"})
84+
err := rootCmd.Execute()
85+
require.ErrorContains(t, err, expErr.Error())
86+
}

cmd/bbolt/command_root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func NewRootCommand() *cobra.Command {
3131
newPageItemCommand(),
3232
newPageCommand(),
3333
newBenchCommand(),
34+
newGetCommand(),
3435
)
3536

3637
return rootCmd

cmd/bbolt/main.go

Lines changed: 0 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"crypto/sha256"
55
"encoding/hex"
66
"errors"
7-
"flag"
87
"fmt"
98
"io"
109
"os"
@@ -113,8 +112,6 @@ func (m *Main) Run(args ...string) error {
113112
case "help":
114113
fmt.Fprintln(m.Stderr, m.Usage())
115114
return ErrUsage
116-
case "get":
117-
return newGetCommand(m).Run(args[1:]...)
118115
default:
119116
return ErrUnknownCommand
120117
}
@@ -197,96 +194,6 @@ func writelnBytes(w io.Writer, b []byte, format string) error {
197194
return err
198195
}
199196

200-
// getCommand represents the "get" command execution.
201-
type getCommand struct {
202-
baseCommand
203-
}
204-
205-
// newGetCommand returns a getCommand.
206-
func newGetCommand(m *Main) *getCommand {
207-
c := &getCommand{}
208-
c.baseCommand = m.baseCommand
209-
return c
210-
}
211-
212-
// Run executes the command.
213-
func (cmd *getCommand) Run(args ...string) error {
214-
// Parse flags.
215-
fs := flag.NewFlagSet("", flag.ContinueOnError)
216-
var parseFormat string
217-
var format string
218-
fs.StringVar(&parseFormat, "parse-format", "ascii-encoded", "Input format. One of: ascii-encoded|hex (default: ascii-encoded)")
219-
fs.StringVar(&format, "format", "auto", "Output format. One of: "+FORMAT_MODES+" (default: auto)")
220-
help := fs.Bool("h", false, "")
221-
if err := fs.Parse(args); err != nil {
222-
return err
223-
} else if *help {
224-
fmt.Fprintln(cmd.Stderr, cmd.Usage())
225-
return ErrUsage
226-
}
227-
228-
// Require database path, bucket and key.
229-
relevantArgs := fs.Args()
230-
if len(relevantArgs) < 3 {
231-
return ErrNotEnoughArgs
232-
}
233-
path, buckets := relevantArgs[0], relevantArgs[1:len(relevantArgs)-1]
234-
key, err := parseBytes(relevantArgs[len(relevantArgs)-1], parseFormat)
235-
if err != nil {
236-
return err
237-
}
238-
if path == "" {
239-
return ErrPathRequired
240-
} else if _, err := os.Stat(path); os.IsNotExist(err) {
241-
return ErrFileNotFound
242-
} else if len(buckets) == 0 {
243-
return ErrBucketRequired
244-
} else if len(key) == 0 {
245-
return berrors.ErrKeyRequired
246-
}
247-
248-
// Open database.
249-
db, err := bolt.Open(path, 0600, &bolt.Options{ReadOnly: true})
250-
if err != nil {
251-
return err
252-
}
253-
defer db.Close()
254-
255-
// Print value.
256-
return db.View(func(tx *bolt.Tx) error {
257-
// Find bucket.
258-
lastBucket, err := findLastBucket(tx, buckets)
259-
if err != nil {
260-
return err
261-
}
262-
263-
// Find value for given key.
264-
val := lastBucket.Get(key)
265-
if val == nil {
266-
return fmt.Errorf("Error %w for key: %q hex: \"%x\"", ErrKeyNotFound, key, string(key))
267-
}
268-
269-
// TODO: In this particular case, it would be better to not terminate with '\n'
270-
return writelnBytes(cmd.Stdout, val, format)
271-
})
272-
}
273-
274-
// Usage returns the help message.
275-
func (cmd *getCommand) Usage() string {
276-
return strings.TrimLeft(`
277-
usage: bolt get PATH [BUCKET..] KEY
278-
279-
Print the value of the given key in the given (sub)bucket.
280-
281-
Additional options include:
282-
283-
--format
284-
Output format. One of: `+FORMAT_MODES+` (default=auto)
285-
--parse-format
286-
Input format (of key). One of: ascii-encoded|hex (default=ascii-encoded)"
287-
`, "\n")
288-
}
289-
290197
type PageError struct {
291198
ID int
292199
Err error

cmd/bbolt/main_test.go

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -13,77 +13,12 @@ import (
1313
"sync"
1414
"testing"
1515

16-
"github.com/stretchr/testify/assert"
1716
"github.com/stretchr/testify/require"
1817

1918
bolt "go.etcd.io/bbolt"
2019
main "go.etcd.io/bbolt/cmd/bbolt"
21-
"go.etcd.io/bbolt/internal/btesting"
2220
)
2321

24-
// Ensure the "get" command can print the value of a key in a bucket.
25-
func TestGetCommand_Run(t *testing.T) {
26-
testCases := []struct {
27-
name string
28-
printable bool
29-
testBucket string
30-
testKey string
31-
expectedValue string
32-
}{
33-
{
34-
name: "printable data",
35-
printable: true,
36-
testBucket: "foo",
37-
testKey: "foo-1",
38-
expectedValue: "val-foo-1\n",
39-
},
40-
{
41-
name: "non printable data",
42-
printable: false,
43-
testBucket: "bar",
44-
testKey: "100001",
45-
expectedValue: hex.EncodeToString(convertInt64IntoBytes(100001)) + "\n",
46-
},
47-
}
48-
49-
for _, tc := range testCases {
50-
t.Run(tc.name, func(t *testing.T) {
51-
db := btesting.MustCreateDB(t)
52-
53-
if err := db.Update(func(tx *bolt.Tx) error {
54-
b, err := tx.CreateBucket([]byte(tc.testBucket))
55-
if err != nil {
56-
return err
57-
}
58-
if tc.printable {
59-
val := fmt.Sprintf("val-%s", tc.testKey)
60-
if err := b.Put([]byte(tc.testKey), []byte(val)); err != nil {
61-
return err
62-
}
63-
} else {
64-
if err := b.Put([]byte(tc.testKey), convertInt64IntoBytes(100001)); err != nil {
65-
return err
66-
}
67-
}
68-
return nil
69-
}); err != nil {
70-
t.Fatal(err)
71-
}
72-
db.Close()
73-
74-
defer requireDBNoChange(t, dbData(t, db.Path()), db.Path())
75-
76-
// Run the command.
77-
m := NewMain()
78-
if err := m.Run("get", db.Path(), tc.testBucket, tc.testKey); err != nil {
79-
t.Fatal(err)
80-
}
81-
actual := m.Stdout.String()
82-
assert.Equal(t, tc.expectedValue, actual)
83-
})
84-
}
85-
}
86-
8722
type ConcurrentBuffer struct {
8823
m sync.Mutex
8924
buf bytes.Buffer
@@ -127,28 +62,6 @@ func NewMain() *Main {
12762
return m
12863
}
12964

130-
func TestCommands_Run_NoArgs(t *testing.T) {
131-
testCases := []struct {
132-
name string
133-
cmd string
134-
expErr error
135-
}{
136-
{
137-
name: "get",
138-
cmd: "get",
139-
expErr: main.ErrNotEnoughArgs,
140-
},
141-
}
142-
143-
for _, tc := range testCases {
144-
t.Run(tc.name, func(t *testing.T) {
145-
m := NewMain()
146-
err := m.Run(tc.cmd)
147-
require.ErrorIs(t, err, main.ErrNotEnoughArgs)
148-
})
149-
}
150-
}
151-
15265
func fillBucket(b *bolt.Bucket, prefix []byte) error {
15366
n := 10 + rand.Intn(50)
15467
for i := 0; i < n; i++ {

0 commit comments

Comments
 (0)