Fix cmdutil.PrintTable to handle ansi escapes and non-byte glyphs (#8344)
Fixes two bugs in how padding was calculated in PrintTable. Firstly we remove all ANSI escape codes from the string before measuring how wide it is. Secondly we measure glyph count (using rivo/uniseg) not byte or rune count of the string. Together these fix the padding/alignment issues I saw when using PrintTable with plan output. They also slightly change the layout of "pulumi stack", for example the below is printed with current master and has 6 characters of space for padding between SecurityGroup and web-secgrp: ``` Current stack resources (4): TYPE NAME pulumi:pulumi:Stack aws-cs-webserver-test ├─ aws:ec2/securityGroup:SecurityGroup web-secgrp ├─ aws:ec2/instance:Instance web-server-www └─ pulumi:providers:aws default_4_25_0 ``` While printed with this commit you only get 2 characters of space for padding (which is correct, the column gap is set to " "): ``` Current stack resources (4): TYPE NAME pulumi:pulumi:Stack aws-cs-webserver-test ├─ aws:ec2/securityGroup:SecurityGroup web-secgrp ├─ aws:ec2/instance:Instance web-server-www └─ pulumi:providers:aws default_4_25_0 ```
This commit is contained in:
parent
7dd1865575
commit
b58c39476f
|
@ -129,6 +129,7 @@ require (
|
|||
github.com/opentracing/basictracer-go v1.0.0 // indirect
|
||||
github.com/pierrec/lz4 v2.6.0+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 // indirect
|
||||
|
|
|
@ -574,6 +574,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
|||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8=
|
||||
github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
|
|
|
@ -26,6 +26,7 @@ require (
|
|||
github.com/opentracing/basictracer-go v1.0.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rivo/uniseg v0.2.0
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.3.1
|
||||
|
|
|
@ -164,6 +164,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
|||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0=
|
||||
|
|
|
@ -17,10 +17,11 @@ package cmdutil
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rivo/uniseg"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/ciutil"
|
||||
|
@ -139,6 +140,18 @@ func (table Table) String() string {
|
|||
return table.ToStringWithGap(" ")
|
||||
}
|
||||
|
||||
// 7-bit C1 ANSI sequences
|
||||
var ansiEscape = regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
|
||||
|
||||
// MeasureText returns the number of glyphs in a string.
|
||||
// Importantly this also ignores ANSI escape sequences, so can be used to calculate layout of colorized strings.
|
||||
func MeasureText(text string) int {
|
||||
// Strip ansi escape sequences
|
||||
clean := ansiEscape.ReplaceAllString(text, "")
|
||||
// Need to count graphemes not runes or bytes
|
||||
return uniseg.GraphemeClusterCount(clean)
|
||||
}
|
||||
|
||||
func (table *Table) ToStringWithGap(columnGap string) string {
|
||||
columnCount := len(table.Headers)
|
||||
|
||||
|
@ -161,36 +174,36 @@ func (table *Table) ToStringWithGap(columnGap string) string {
|
|||
}
|
||||
|
||||
for columnIndex, val := range columns {
|
||||
preferredColumnWidths[columnIndex] = max(preferredColumnWidths[columnIndex], len(val))
|
||||
preferredColumnWidths[columnIndex] = max(preferredColumnWidths[columnIndex], MeasureText(val))
|
||||
}
|
||||
}
|
||||
|
||||
format := ""
|
||||
for i, maxWidth := range preferredColumnWidths {
|
||||
if i < len(preferredColumnWidths)-1 {
|
||||
format += "%-" + strconv.Itoa(maxWidth+len(columnGap)) + "s"
|
||||
} else {
|
||||
result := ""
|
||||
for _, row := range allRows {
|
||||
result += table.Prefix
|
||||
|
||||
for columnIndex, val := range row.Columns {
|
||||
result += val
|
||||
|
||||
if columnIndex < columnCount-1 {
|
||||
// Work out how much whitespace we need to add to this string to bring it up to the
|
||||
// preferredColumnWidth for this column.
|
||||
|
||||
maxWidth := preferredColumnWidths[columnIndex]
|
||||
padding := maxWidth - MeasureText(val)
|
||||
result += strings.Repeat(" ", padding)
|
||||
|
||||
// Now, ensure we have the requested gap between columns as well.
|
||||
result += columnGap
|
||||
}
|
||||
// do not want whitespace appended to the last column. It would cause wrapping on lines
|
||||
// that were not actually long if some other line was very long.
|
||||
format += "%s"
|
||||
}
|
||||
}
|
||||
format += "\n"
|
||||
result := ""
|
||||
columns := make([]interface{}, columnCount)
|
||||
for _, row := range allRows {
|
||||
for columnIndex, value := range row.Columns {
|
||||
// Now, ensure we have the requested gap between columns as well.
|
||||
if columnIndex < columnCount-1 {
|
||||
value += columnGap
|
||||
}
|
||||
|
||||
columns[columnIndex] = value
|
||||
}
|
||||
result += "\n"
|
||||
|
||||
result += fmt.Sprintf(table.Prefix+format, columns...)
|
||||
if row.AdditionalInfo != "" {
|
||||
result += fmt.Sprint(row.AdditionalInfo)
|
||||
result += row.AdditionalInfo
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
|
116
sdk/go/common/util/cmdutil/console_test.go
Normal file
116
sdk/go/common/util/cmdutil/console_test.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmdutil
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMeasureText(t *testing.T) {
|
||||
cases := []struct {
|
||||
text string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
text: "",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
text: "a",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
text: "├",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
text: "├─ ",
|
||||
expected: 4,
|
||||
},
|
||||
{
|
||||
text: "\x1b[4m\x1b[38;5;12mType\x1b[0m",
|
||||
expected: 4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.text, func(t *testing.T) {
|
||||
count := MeasureText(c.text)
|
||||
assert.Equal(t, c.expected, count)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTablePrinting(t *testing.T) {
|
||||
rows := []TableRow{
|
||||
{Columns: []string{"A", "B", "C"}},
|
||||
{Columns: []string{"Some A", "B", "Some C"}},
|
||||
}
|
||||
|
||||
table := &Table{
|
||||
Headers: []string{"ColumnA", "Long column B", "C"},
|
||||
Rows: rows,
|
||||
Prefix: " ",
|
||||
}
|
||||
|
||||
expected := "" +
|
||||
" ColumnA Long column B C\n" +
|
||||
" A B C\n" +
|
||||
" Some A B Some C\n"
|
||||
|
||||
assert.Equal(t, expected, table.ToStringWithGap(" "))
|
||||
}
|
||||
|
||||
func TestColorTablePrinting(t *testing.T) {
|
||||
|
||||
greenText := func(msg string) string {
|
||||
return colors.Always.Colorize(colors.Green + msg + colors.Reset)
|
||||
}
|
||||
|
||||
rows := []TableRow{
|
||||
{Columns: []string{greenText("+"), "pulumi:pulumi:Stack", "aws-cs-webserver-test", greenText("create")}},
|
||||
{Columns: []string{greenText("+"), "├─ aws:ec2/instance:Instance", "web-server-www", greenText("create")}},
|
||||
{Columns: []string{greenText("+"), "├─ aws:ec2/securityGroup:SecurityGroup", "web-secgrp", greenText("create")}},
|
||||
{Columns: []string{greenText("+"), "└─ pulumi:providers:aws", "default_4_25_0", greenText("create")}},
|
||||
}
|
||||
|
||||
columnHeader := func(msg string) string {
|
||||
return colors.Always.Colorize(colors.Underline + colors.BrightBlue + msg + colors.Reset)
|
||||
}
|
||||
|
||||
table := &Table{
|
||||
Headers: []string{"", columnHeader("Type"), columnHeader("Name"), columnHeader("Plan")},
|
||||
Rows: rows,
|
||||
Prefix: " ",
|
||||
}
|
||||
|
||||
expected := "" +
|
||||
" Type Name Plan\n" +
|
||||
" + pulumi:pulumi:Stack aws-cs-webserver-test create\n" +
|
||||
" + ├─ aws:ec2/instance:Instance web-server-www create\n" +
|
||||
" + ├─ aws:ec2/securityGroup:SecurityGroup web-secgrp create\n" +
|
||||
" + └─ pulumi:providers:aws default_4_25_0 create\n"
|
||||
|
||||
colorTable := table.ToStringWithGap(" ")
|
||||
// 7-bit C1 ANSI sequences
|
||||
ansiEscape := regexp.MustCompile(`\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`)
|
||||
cleanTable := ansiEscape.ReplaceAllString(colorTable, "")
|
||||
|
||||
assert.Equal(t, expected, cleanTable)
|
||||
}
|
|
@ -97,6 +97,7 @@ require (
|
|||
github.com/pierrec/lz4 v2.6.0+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rjeczalik/notify v0.9.2 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 // indirect
|
||||
|
|
|
@ -567,6 +567,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
|||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8=
|
||||
github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
|
|
Loading…
Reference in a new issue