diff --git a/cli/command/formatter/treewriter/treewriter.go b/cli/command/formatter/treewriter/treewriter.go new file mode 100644 index 0000000000..7dd0421181 --- /dev/null +++ b/cli/command/formatter/treewriter/treewriter.go @@ -0,0 +1,51 @@ +package treewriter + +import ( + "bytes" + "fmt" + "strings" + + "github.com/docker/cli/cli/command/formatter/tabwriter" +) + +func PrintTree(header []string, rows [][]string) { + // TODO(thaJeztah): using [][]string doesn't work well for this; we should + // create a type for this that has "optional" child records that we can + // recurse over to build the tree. + tree := make([][]string, 0, len(rows)) + tree = append(tree, header) + tree = append(tree, rows...) + + buf := bytes.NewBuffer(nil) + tw := tabwriter.NewWriter(buf, 20, 1, 3, ' ', 0) + for rowNum, cols := range tree { + if len(cols) == 0 { + continue + } + + // The treePrefix is basically what makes it a "tree" when printing. + // We shouldn't need to have "columns" though, only know about nesting + // level, and otherwise have a pre-formatted, tab-terminated string + // for each row. + var treePrefix string + treePrefix, cols = cols[0], cols[1:] + if treePrefix == "" { + // Start of new group + if rowNum > 1 { + // Print an empty line between groups. We need to write a tab + // for each column for the tab-writer to give all groups equal + // widths. + // + // FIXME(thaJeztah): if we pass rows as a pre-formatted string, + // instead of a []string, we need to know the number of columns + // (probably counting number of tabs would do the trick). + _, _ = fmt.Fprintln(tw, strings.Repeat("\t", len(cols))) + } + _, _ = fmt.Fprintln(tw, strings.Join(cols, "\t")) + } else { + _, _ = fmt.Fprintln(tw, treePrefix, strings.Join(cols, "\t")) + } + } + _ = tw.Flush() + fmt.Println(buf.String()) +} diff --git a/cli/command/formatter/treewriter/treewriter_test.go b/cli/command/formatter/treewriter/treewriter_test.go new file mode 100644 index 0000000000..e2ae4ed25d --- /dev/null +++ b/cli/command/formatter/treewriter/treewriter_test.go @@ -0,0 +1,97 @@ +package treewriter + +import ( + "testing" +) + +func TestTree(t *testing.T) { + var rows [][]string + rows = append(rows, [][]string{ + {"", "alpine:latest", "beefdbd8a1da", "13.6MB", "4.09MB"}, + {"├─", "linux/amd64", "33735bd63cf8", "0B", "0B"}, + {"├─", "linux/arm/v6", "50f635c8b04d", "0B", "0B"}, + {"├─", "linux/arm/v7", "f2f82d424957", "0B", "0B"}, + {"├─", "linux/arm64/v8", "9cee2b382fe2", "13.6MB", "4.09MB"}, + {"├─", "linux/386", "b3e87f642f5c", "0B", "0B"}, + {"├─", "linux/ppc64le", "c7a6800e3dc5", "0B", "0B"}, + {"├─", "linux/riscv64", "80cde017a105", "0B", "0B"}, + {"└─", "linux/s390x", "2b5b26e09ca2", "0B", "0B"}, + + {"", "namespace/image", "beefdbd8a1da", "13.6MB", "4.09MB"}, + {"├─", "namespace/image:1", "beefdbd8a1da", "-", "-"}, + {"├─", "namespace/image:1.0", "beefdbd8a1da", "-", "-"}, + {"├─", "namespace/image:1.0.0", "beefdbd8a1da", "-", "-"}, + {"└─", "namespace/image:latest", "beefdbd8a1da", "-", "-"}, + {" ├─", "linux/amd64", "33735bd63cf8", "0B", "0B"}, + {" ├─", "linux/arm/v6", "50f635c8b04d", "0B", "0B"}, + {" ├─", "linux/arm/v7", "f2f82d424957", "0B", "0B"}, + {" ├─", "linux/arm64/v8", "9cee2b382fe2", "13.6MB", "4.09MB"}, + {" ├─", "linux/386", "b3e87f642f5c", "0B", "0B"}, + {" ├─", "linux/ppc64le", "c7a6800e3dc5", "0B", "0B"}, + {" ├─", "linux/riscv64", "80cde017a105", "0B", "0B"}, + {" └─", "linux/s390x", "2b5b26e09ca2", "0B", "0B"}, + + {"", "internal.example.com/namespace/image", "beefdbd8a1da", "13.6MB", "4.09MB"}, + {"├─", "internal.example.com/namespace/image:1", "beefdbd8a1da", "-", "-"}, + {"├─", "internal.example.com/namespace/image:1.0", "beefdbd8a1da", "-", "-"}, + {"├─", "internal.example.com/namespace/image:1.0.0", "beefdbd8a1da", "-", "-"}, + {"└─", "internal.example.com/namespace/image:latest", "beefdbd8a1da", "-", "-"}, + {" ├─", "linux/amd64", "33735bd63cf8", "0B", "0B"}, + {" ├─", "linux/arm/v6", "50f635c8b04d", "0B", "0B"}, + {" ├─", "linux/arm/v7", "f2f82d424957", "0B", "0B"}, + {" ├─", "linux/arm64/v8", "9cee2b382fe2", "13.6MB", "4.09MB"}, + {" ├─", "linux/386", "b3e87f642f5c", "0B", "0B"}, + {" ├─", "linux/ppc64le", "c7a6800e3dc5", "0B", "0B"}, + {" ├─", "linux/riscv64", "80cde017a105", "0B", "0B"}, + {" └─", "linux/s390x", "2b5b26e09ca2", "0B", "0B"}, + }...) + + header := []string{"IMAGE/TAGS", "ID", "DISK USAGE", "CONTENT SIZE", "USED"} + PrintTree(header, rows) +} + +func TestTreeNoTrunc(t *testing.T) { + var rows [][]string + rows = append(rows, [][]string{ + {"", "alpine:latest", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "13.6MB", "4.09MB"}, + {"├─", "linux/amd64", "sha256:33735bd63cf84d7e388d9f6d297d348c523c044410f553bd878c6d7829612735", "0B", "0B"}, + {"├─", "linux/arm/v6", "sha256:50f635c8b04d86dde8a02bcd8d667ba287eb8b318c1c0cf547e5a48ddadea1be", "0B", "0B"}, + {"├─", "linux/arm/v7", "sha256:f2f82d42495723c4dc508fd6b0978a5d7fe4efcca4282e7aae5e00bcf4057086", "0B", "0B"}, + {"├─", "linux/arm64/v8", "sha256:9cee2b382fe2412cd77d5d437d15a93da8de373813621f2e4d406e3df0cf0e7c", "13.6MB", "4.09MB"}, + {"├─", "linux/386", "sha256:b3e87f642f5c48cdc7556c3e03a0d63916bd0055ba6edba7773df3cb1a76f224", "0B", "0B"}, + {"├─", "linux/ppc64le", "sha256:c7a6800e3dc569a2d6e90627a2988f2a7339e6f111cdf6a0054ad1ff833e99b0", "0B", "0B"}, + {"├─", "linux/riscv64", "sha256:80cde017a10529a18a7274f70c687bb07c4969980ddfb35a1b921fda3a020e5b", "0B", "0B"}, + {"└─", "linux/s390x", "sha256:2b5b26e09ca2856f50ac88312348d26c1ac4b8af1df9f580e5cf465fd76e3d4d", "0B", "0B"}, + + {"", "namespace/image", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "13.6MB", "4.09MB"}, + {"├─", "namespace/image:1", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {"├─", "namespace/image:1.0", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {"├─", "namespace/image:1.0.0", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {"└─", "namespace/image:latest", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {" ├─", "linux/amd64", "sha256:33735bd63cf84d7e388d9f6d297d348c523c044410f553bd878c6d7829612735", "0B", "0B"}, + {" ├─", "linux/arm/v6", "sha256:50f635c8b04d86dde8a02bcd8d667ba287eb8b318c1c0cf547e5a48ddadea1be", "0B", "0B"}, + {" ├─", "linux/arm/v7", "sha256:f2f82d42495723c4dc508fd6b0978a5d7fe4efcca4282e7aae5e00bcf4057086", "0B", "0B"}, + {" ├─", "linux/arm64/v8", "sha256:9cee2b382fe2412cd77d5d437d15a93da8de373813621f2e4d406e3df0cf0e7c", "13.6MB", "4.09MB"}, + {" ├─", "linux/386", "sha256:b3e87f642f5c48cdc7556c3e03a0d63916bd0055ba6edba7773df3cb1a76f224", "0B", "0B"}, + {" ├─", "linux/ppc64le", "sha256:c7a6800e3dc569a2d6e90627a2988f2a7339e6f111cdf6a0054ad1ff833e99b0", "0B", "0B"}, + {" ├─", "linux/riscv64", "sha256:80cde017a10529a18a7274f70c687bb07c4969980ddfb35a1b921fda3a020e5b", "0B", "0B"}, + {" └─", "linux/s390x", "sha256:2b5b26e09ca2856f50ac88312348d26c1ac4b8af1df9f580e5cf465fd76e3d4d", "0B", "0B"}, + + {"", "internal.example.com/namespace/image", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "13.6MB", "4.09MB"}, + {"├─", "internal.example.com/namespace/image:1", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {"├─", "internal.example.com/namespace/image:1.0", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {"├─", "internal.example.com/namespace/image:1.0.0", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {"└─", "internal.example.com/namespace/image:latest", "sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d", "-", "-"}, + {" ├─", "linux/amd64", "sha256:33735bd63cf84d7e388d9f6d297d348c523c044410f553bd878c6d7829612735", "0B", "0B"}, + {" ├─", "linux/arm/v6", "sha256:50f635c8b04d86dde8a02bcd8d667ba287eb8b318c1c0cf547e5a48ddadea1be", "0B", "0B"}, + {" ├─", "linux/arm/v7", "sha256:f2f82d42495723c4dc508fd6b0978a5d7fe4efcca4282e7aae5e00bcf4057086", "0B", "0B"}, + {" ├─", "linux/arm64/v8", "sha256:9cee2b382fe2412cd77d5d437d15a93da8de373813621f2e4d406e3df0cf0e7c", "13.6MB", "4.09MB"}, + {" ├─", "linux/386", "sha256:b3e87f642f5c48cdc7556c3e03a0d63916bd0055ba6edba7773df3cb1a76f224", "0B", "0B"}, + {" ├─", "linux/ppc64le", "sha256:c7a6800e3dc569a2d6e90627a2988f2a7339e6f111cdf6a0054ad1ff833e99b0", "0B", "0B"}, + {" ├─", "linux/riscv64", "sha256:80cde017a10529a18a7274f70c687bb07c4969980ddfb35a1b921fda3a020e5b", "0B", "0B"}, + {" └─", "linux/s390x", "sha256:2b5b26e09ca2856f50ac88312348d26c1ac4b8af1df9f580e5cf465fd76e3d4d", "0B", "0B"}, + }...) + + header := []string{"IMAGE/TAGS", "ID", "DISK USAGE", "CONTENT SIZE", "USED"} + PrintTree(header, rows) +}