Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix CLI tool print entire freelist. Add option to dump leaf element keys/value bytes. #50

Merged
merged 1 commit into from
Sep 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
*.test
*.swp
/bin/
cmd/bolt/bolt
186 changes: 181 additions & 5 deletions cmd/bolt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ func (m *Main) Run(args ...string) error {
return newCompactCommand(m).Run(args[1:]...)
case "dump":
return newDumpCommand(m).Run(args[1:]...)
case "page-item":
return newPageItemCommand(m).Run(args[1:]...)
case "get":
return newGetCommand(m).Run(args[1:]...)
case "info":
Expand Down Expand Up @@ -153,6 +155,7 @@ The commands are:
help print this screen
page print one or more pages in human readable format
pages print list of pages with their types
page-item print the key and value of a page item.
stats iterate over all pages and generate usage stats

Use "bolt [command] -h" for more information about a command.
Expand Down Expand Up @@ -416,9 +419,173 @@ func (cmd *DumpCommand) PrintPage(w io.Writer, r io.ReaderAt, pageID int, pageSi
// Usage returns the help message.
func (cmd *DumpCommand) Usage() string {
return strings.TrimLeft(`
usage: bolt dump -page PAGEID PATH
usage: bolt dump PATH pageid [pageid...]

Dump prints a hexadecimal dump of a single page.
Dump prints a hexadecimal dump of one or more pages.
`, "\n")
}

// PageItemCommand represents the "page-item" command execution.
type PageItemCommand struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}

// newPageItemCommand returns a PageItemCommand.
func newPageItemCommand(m *Main) *PageItemCommand {
return &PageItemCommand{
Stdin: m.Stdin,
Stdout: m.Stdout,
Stderr: m.Stderr,
}
}

type pageItemOptions struct {
help bool
keyOnly bool
valueOnly bool
format string
}

// Run executes the command.
func (cmd *PageItemCommand) Run(args ...string) error {
// Parse flags.
options := &pageItemOptions{}
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.BoolVar(&options.keyOnly, "key-only", false, "Print only the key")
fs.BoolVar(&options.valueOnly, "value-only", false, "Print only the value")
fs.StringVar(&options.format, "format", "ascii-encoded", "Output format. One of: ascii-encoded|hex|bytes")
fs.BoolVar(&options.help, "h", false, "")
if err := fs.Parse(args); err != nil {
return err
} else if options.help {
fmt.Fprintln(cmd.Stderr, cmd.Usage())
return ErrUsage
}

if options.keyOnly && options.valueOnly {
return fmt.Errorf("The --key-only or --value-only flag may be set, but not both.")
}

// Require database path and page id.
path := fs.Arg(0)
if path == "" {
return ErrPathRequired
} else if _, err := os.Stat(path); os.IsNotExist(err) {
return ErrFileNotFound
}

// Read page id.
pageID, err := strconv.Atoi(fs.Arg(1))
if err != nil {
return err
}

// Read item id.
itemID, err := strconv.Atoi(fs.Arg(2))
if err != nil {
return err
}

// Open database file handler.
f, err := os.Open(path)
if err != nil {
return err
}
defer func() { _ = f.Close() }()

// Retrieve page info and page size.
_, buf, err := ReadPage(path, pageID)
if err != nil {
return err
}

if !options.valueOnly {
Copy link
Contributor

@heyitsanthony heyitsanthony Sep 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if !options.valuesOnly { ... }
if !options.keyOnly { ... }

so the output with no flags exactly matches the physical layout since this is a raw dump.

etcdctl's default output mode inserts a newline and in retrospect I think it makes the output really difficult / impossible to parse, but it's too late now. printf("%q\n%q\n",key,val) seems like the best/easiest human-readable way to print binary data, if that's worth pursuing in the future.

Copy link
Contributor Author

@jpbetz jpbetz Sep 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we do something like this:

bolt page-item <dbfile> <pageid> <itemid> 
"<%q encoded key>"
"<%q encoded value>"
bolt page-item --raw-key-only <dbfile> <pageid> <itemid>
<key bytes>
bolt page-item --raw-value-only <dbfile> <pageid> <itemid>
<value bytes>

?

This solves a couple problems:

  • It provides a default output that won't mess up someones terminal by outputting raw bytes, and that prints out the key and value, which, if ASCII, will be rendered nicely.
  • It avoid use of "\n" as a delimiter between binary output.
  • If one needs the raw bytes, they have convenient flags they can use to get the key and value individually.

err := cmd.PrintLeafItemKey(cmd.Stdout, buf, uint16(itemID), options.format)
if err != nil {
return err
}
}
if !options.keyOnly {
err := cmd.PrintLeafItemValue(cmd.Stdout, buf, uint16(itemID), options.format)
if err != nil {
return err
}
}
return nil
}

// leafPageElement retrieves a leaf page element.
func (cmd *PageItemCommand) leafPageElement(pageBytes []byte, index uint16) (*leafPageElement, error) {
p := (*page)(unsafe.Pointer(&pageBytes[0]))
if index >= p.count {
return nil, fmt.Errorf("leafPageElement: expected item index less than %d, but got %d.", p.count, index)
}
if p.Type() != "leaf" {
return nil, fmt.Errorf("leafPageElement: expected page type of 'leaf', but got '%s'", p.Type())
}
return p.leafPageElement(index), nil
}

// writeBytes writes the byte to the writer. Supported formats: ascii-encoded, hex, bytes.
func (cmd *PageItemCommand) writeBytes(w io.Writer, b []byte, format string) error {
switch format {
case "ascii-encoded":
_, err := fmt.Fprintf(w, "%q", b)
if err != nil {
return err
}
_, err = fmt.Fprintf(w, "\n")
return err
case "hex":
_, err := fmt.Fprintf(w, "%x", b)
if err != nil {
return err
}
_, err = fmt.Fprintf(w, "\n")
return err
case "bytes":
_, err := w.Write(b)
return err
default:
return fmt.Errorf("writeBytes: unsupported format: %s", format)
}
}

// PrintLeafItemKey writes the bytes of a leaf element's key.
func (cmd *PageItemCommand) PrintLeafItemKey(w io.Writer, pageBytes []byte, index uint16, format string) error {
e, err := cmd.leafPageElement(pageBytes, index)
if err != nil {
return err
}
return cmd.writeBytes(w, e.key(), format)
}

// PrintLeafItemKey writes the bytes of a leaf element's value.
func (cmd *PageItemCommand) PrintLeafItemValue(w io.Writer, pageBytes []byte, index uint16, format string) error {
e, err := cmd.leafPageElement(pageBytes, index)
if err != nil {
return err
}
return cmd.writeBytes(w, e.value(), format)
}

// Usage returns the help message.
func (cmd *PageItemCommand) Usage() string {
return strings.TrimLeft(`
usage: bolt page-item [options] PATH pageid itemid

Additional options include:

--key-only
Print only the key
--value-only
Print only the value
--format
Output format. One of: ascii-encoded|hex|bytes (default=ascii-encoded)

page-item prints a page item key and value.
`, "\n")
}

Expand Down Expand Up @@ -592,13 +759,22 @@ func (cmd *PageCommand) PrintBranch(w io.Writer, buf []byte) error {
func (cmd *PageCommand) PrintFreelist(w io.Writer, buf []byte) error {
p := (*page)(unsafe.Pointer(&buf[0]))

// Check for overflow and, if present, adjust starting index and actual element count.
idx, count := 0, int(p.count)
if p.count == 0xFFFF {
idx = 1
count = int(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[0])
}

// Print number of items.
fmt.Fprintf(w, "Item Count: %d\n", p.count)
fmt.Fprintf(w, "Item Count: %d\n", count)
fmt.Fprintf(w, "Overflow: %d\n", p.overflow)

fmt.Fprintf(w, "\n")

// Print each page in the freelist.
ids := (*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr))
for i := uint16(0); i < p.count; i++ {
for i := int(idx); i < count; i++ {
fmt.Fprintf(w, "%d\n", ids[i])
}
fmt.Fprintf(w, "\n")
Expand Down Expand Up @@ -653,7 +829,7 @@ func (cmd *PageCommand) PrintPage(w io.Writer, r io.ReaderAt, pageID int, pageSi
// Usage returns the help message.
func (cmd *PageCommand) Usage() string {
return strings.TrimLeft(`
usage: bolt page -page PATH pageid [pageid...]
usage: bolt page PATH pageid [pageid...]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DumpCommand's Usage looks similarly broken, might as well fix that too...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure enough. Fixed.


Page prints one or more pages in human readable format.
`, "\n")
Expand Down