Browse code

Implement stringutils.Ellipsis()

This patch implements an Ellipsis utility to
append an ellipsis (...) when truncating
strings in output.

It also fixes the existing Truncate() utility
to be compatible with unicode/multibyte characters.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2016/08/10 22:54:14
Showing 7 changed files
... ...
@@ -124,7 +124,7 @@ func (c *containerContext) Command() string {
124 124
 	c.addHeader(commandHeader)
125 125
 	command := c.c.Command
126 126
 	if c.trunc {
127
-		command = stringutils.Truncate(command, 20)
127
+		command = stringutils.Ellipsis(command, 20)
128 128
 	}
129 129
 	return strconv.Quote(command)
130 130
 }
... ...
@@ -200,7 +200,7 @@ func (c *containerContext) Mounts() string {
200 200
 			name = m.Name
201 201
 		}
202 202
 		if c.trunc {
203
-			name = stringutils.Truncate(name, 15)
203
+			name = stringutils.Ellipsis(name, 15)
204 204
 		}
205 205
 		mounts = append(mounts, name)
206 206
 	}
... ...
@@ -60,12 +60,12 @@ func TestContainerPsContext(t *testing.T) {
60 60
 		{types.Container{
61 61
 			Mounts: []types.MountPoint{
62 62
 				{
63
-					Name:   "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203",
63
+					Name:   "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set",
64 64
 					Driver: "local",
65 65
 					Source: "/a/path",
66 66
 				},
67 67
 			},
68
-		}, true, "733908409c91817", mountsHeader, ctx.Mounts},
68
+		}, true, "this-is-a-lo...", mountsHeader, ctx.Mounts},
69 69
 		{types.Container{
70 70
 			Mounts: []types.MountPoint{
71 71
 				{
... ...
@@ -79,8 +79,8 @@ func runHistory(dockerCli *client.DockerCli, opts historyOptions) error {
79 79
 	for _, entry := range history {
80 80
 		imageID = entry.ID
81 81
 		createdBy = strings.Replace(entry.CreatedBy, "\t", " ", -1)
82
-		if opts.noTrunc == false {
83
-			createdBy = stringutils.Truncate(createdBy, 45)
82
+		if !opts.noTrunc {
83
+			createdBy = stringutils.Ellipsis(createdBy, 45)
84 84
 			imageID = stringid.TruncateID(entry.ID)
85 85
 		}
86 86
 
... ...
@@ -109,8 +109,8 @@ func runSearch(dockerCli *client.DockerCli, opts searchOptions) error {
109 109
 		}
110 110
 		desc := strings.Replace(res.Description, "\n", " ", -1)
111 111
 		desc = strings.Replace(desc, "\r", " ", -1)
112
-		if !opts.noTrunc && len(desc) > 45 {
113
-			desc = stringutils.Truncate(desc, 42) + "..."
112
+		if !opts.noTrunc {
113
+			desc = stringutils.Ellipsis(desc, 45)
114 114
 		}
115 115
 		fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount)
116 116
 		if res.IsOfficial {
... ...
@@ -51,8 +51,8 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
51 51
 	for _, p := range plugins {
52 52
 		desc := strings.Replace(p.Manifest.Description, "\n", " ", -1)
53 53
 		desc = strings.Replace(desc, "\r", " ", -1)
54
-		if !opts.noTrunc && len(desc) > 45 {
55
-			desc = stringutils.Truncate(desc, 42) + "..."
54
+		if !opts.noTrunc {
55
+			desc = stringutils.Ellipsis(desc, 45)
56 56
 		}
57 57
 
58 58
 		fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", p.Name, p.Tag, desc, p.Active)
... ...
@@ -32,12 +32,26 @@ func GenerateRandomASCIIString(n int) string {
32 32
 	return string(res)
33 33
 }
34 34
 
35
+// Ellipsis truncates a string to fit within maxlen, and appends ellipsis (...).
36
+// For maxlen of 3 and lower, no ellipsis is appended.
37
+func Ellipsis(s string, maxlen int) string {
38
+	r := []rune(s)
39
+	if len(r) <= maxlen {
40
+		return s
41
+	}
42
+	if maxlen <= 3 {
43
+		return string(r[:maxlen])
44
+	}
45
+	return string(r[:maxlen-3]) + "..."
46
+}
47
+
35 48
 // Truncate truncates a string to maxlen.
36 49
 func Truncate(s string, maxlen int) string {
37
-	if len(s) <= maxlen {
50
+	r := []rune(s)
51
+	if len(r) <= maxlen {
38 52
 		return s
39 53
 	}
40
-	return s[:maxlen]
54
+	return string(r[:maxlen])
41 55
 }
42 56
 
43 57
 // InSlice tests whether a string is contained in a slice of strings or not.
... ...
@@ -57,24 +57,40 @@ func TestGenerateRandomAsciiStringIsAscii(t *testing.T) {
57 57
 	}
58 58
 }
59 59
 
60
+func TestEllipsis(t *testing.T) {
61
+	str := "t🐳ststring"
62
+	newstr := Ellipsis(str, 3)
63
+	if newstr != "t🐳s" {
64
+		t.Fatalf("Expected t🐳s, got %s", newstr)
65
+	}
66
+	newstr = Ellipsis(str, 8)
67
+	if newstr != "t🐳sts..." {
68
+		t.Fatalf("Expected tests..., got %s", newstr)
69
+	}
70
+	newstr = Ellipsis(str, 20)
71
+	if newstr != "t🐳ststring" {
72
+		t.Fatalf("Expected t🐳ststring, got %s", newstr)
73
+	}
74
+}
75
+
60 76
 func TestTruncate(t *testing.T) {
61
-	str := "teststring"
77
+	str := "t🐳ststring"
62 78
 	newstr := Truncate(str, 4)
63
-	if newstr != "test" {
64
-		t.Fatalf("Expected test, got %s", newstr)
79
+	if newstr != "t🐳st" {
80
+		t.Fatalf("Expected t🐳st, got %s", newstr)
65 81
 	}
66 82
 	newstr = Truncate(str, 20)
67
-	if newstr != "teststring" {
68
-		t.Fatalf("Expected teststring, got %s", newstr)
83
+	if newstr != "t🐳ststring" {
84
+		t.Fatalf("Expected t🐳ststring, got %s", newstr)
69 85
 	}
70 86
 }
71 87
 
72 88
 func TestInSlice(t *testing.T) {
73
-	slice := []string{"test", "in", "slice"}
89
+	slice := []string{"t🐳st", "in", "slice"}
74 90
 
75
-	test := InSlice(slice, "test")
91
+	test := InSlice(slice, "t🐳st")
76 92
 	if !test {
77
-		t.Fatalf("Expected string test to be in slice")
93
+		t.Fatalf("Expected string t🐳st to be in slice")
78 94
 	}
79 95
 	test = InSlice(slice, "SLICE")
80 96
 	if !test {