From 20b4a730fefef8fed89db89dce91c32fd68d3f16 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Thu, 1 Oct 2015 17:07:26 -0700 Subject: [PATCH 1/8] Support MaxWidth --- README.md | 107 ++++++++++++++++++++++++++++++--------------- columnize.go | 108 +++++++++++++++++++++++++++++++++++++++++++--- columnize_test.go | 81 +++++++++++++++++++++++++++++++++- 3 files changed, 256 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 6852911..fd5494a 100644 --- a/README.md +++ b/README.md @@ -12,32 +12,28 @@ to make each column is a boring problem to solve and eats your valuable time. Here is an example: -```go -package main - -import ( - "fmt" - "github.com/ryanuber/columnize" -) - -func main() { - output := []string{ - "Name | Gender | Age", - "Bob | Male | 38", - "Sally | Female | 26", + package main + + import ( + "fmt" + "github.com/tooda02/columnize" + ) + + func main() { + output := []string{ + "Name | Gender | Age", + "Bob | Male | 38", + "Sally | Female | 26", + } + result := columnize.SimpleFormat(output) + fmt.Println(result) } - result := columnize.SimpleFormat(output) - fmt.Println(result) -} -``` As you can see, you just pass in a list of strings. And the result: -``` -Name Gender Age -Bob Male 38 -Sally Female 26 -``` + Name Gender Age + Bob Male 38 + Sally Female 26 Columnize is tolerant of missing or empty fields, or even empty lines, so passing in extra lines for spacing should show up as you would expect. @@ -49,18 +45,18 @@ Columnize is configured using a `Config`, which can be obtained by calling the `DefaultConfig()` method. You can then tweak the settings in the resulting `Config`: -``` -config := columnize.DefaultConfig() -config.Delim = "|" -config.Glue = " " -config.Prefix = "" -config.Empty = "" -``` + config := columnize.DefaultConfig() + config.Delim = "|" + config.Glue = " " + config.Prefix = "" + config.Empty = "" + config.MaxWidth = []int{10, 0, -80} * `Delim` is the string by which columns of **input** are delimited * `Glue` is the string by which columns of **output** are delimited * `Prefix` is a string by which each line of **output** is prefixed -* `Empty` is a string used to replace blank values found in output +* `Empty` is a string used to replace blank values found in output +* `MaxWidth` is an int slice specifying the maximum width of each column. Columns exceeding their configured width are broken at a word boundary and continued on the next line. See below for details. You can then pass the `Config` in using the `Format` method (signature below) to have text formatted to your liking. @@ -68,8 +64,51 @@ have text formatted to your liking. Usage ===== -```go -SimpleFormat(intput []string) string + SimpleFormat(intput []string) string + + Format(input []string, config *Config) string + +Using MaxWidth +============== +The MaxWidth config element allows you to configure the maximum width of each column. An input line with a data value exceeding the maximum width for a column is formatted into multiple lines, with each long column's data broken at a word boundary and continued on the line below. For example: + + input := []string{ + "Column a | Column b | Column c", + "xx | yy | zz", + "some quite long data | some more data | even longer data for the last column", + "this one will fit | a break | The quick brown fox jumps over the low lazy dog", + "qq | rr | ss", + } + config := Config{MaxWidth: []int{10, 0, 15}} + output := Format(input, &config) + +results in the output: + + Column a Column b Column c + xx yy zz + some quite some more data even longer + long data data for the + last column + this one a break The quick brown + will fit fox jumps over + the low lazy + dog + qq rr ss + +Columns without a MaxWidth value, or columns with a MaxWidth value of zero, expand to the maximum data width as normal. + +If you want to restrict the maximum width of the entire output line, specify a negative value for exactly one of the columns in a MaxWidth specification. The inverse of the negative value is interpreted as the desired output line width, and columnize calculates a width for the designated column so that all output lines fit in the specified width. As a convenience, if you want the last column of data to be the one with the calculated width, specify MaxWidth as a single-element array with a negative value. + +For example, if you want no output line to exceed 80 characters, you could specify: + + config := Config{MaxWidth: []int{0, -80, 15}} + +This specification causes the first column to be the width of the largest piece of data in that column; the last column to be a maximum of 15 characters; and the middle column to have whatever maximum width is required so that no output line exceeds 80 characters in width. + +You could also specify: + + config := Config{MaxWidth: []int{-80}} + +to request that no output line exceed 80 characters, with any line breaks occurring in the last data column. + -Format(input []string, config *Config) string -``` diff --git a/columnize.go b/columnize.go index d877859..8abe070 100644 --- a/columnize.go +++ b/columnize.go @@ -3,6 +3,7 @@ package columnize import ( "fmt" "strings" + "unicode" ) type Config struct { @@ -17,14 +18,21 @@ type Config struct { // A replacement string to replace empty fields Empty string + + // Maximum width of each field + MaxWidth []int + + // Index of a negative value within config.MaxWidth + negIndex int } // Returns a Config with default values. func DefaultConfig() *Config { return &Config{ - Delim: "|", - Glue: " ", - Prefix: "", + Delim: "|", + Glue: " ", + Prefix: "", + MaxWidth: []int{}, } } @@ -51,6 +59,9 @@ func getWidthsFromLines(config *Config, lines []string) []int { elems := getElementsFromLine(config, line) for i := 0; i < len(elems); i++ { l := len(elems[i].(string)) + if i < len(config.MaxWidth) && config.MaxWidth[i] > 0 && config.MaxWidth[i] < l { + l = config.MaxWidth[i] + } if len(widths) <= i { widths = append(widths, l) } else if widths[i] < l { @@ -58,6 +69,27 @@ func getWidthsFromLines(config *Config, lines []string) []int { } } } + + // If one of the columns has a negative width specification, set its width + // so that the entire output line has a width of the absolute value of the spec + + if config.negIndex >= 0 && config.negIndex < len(widths) { + maxOutputWidth := 0 - config.MaxWidth[config.negIndex] + if config.negIndex == 0 && len(config.MaxWidth) == 1 && len(widths) > 1 { + config.negIndex = len(widths) - 1 // Apply single negative value to last field + } + totalLineWidth := len(config.Prefix) + len(config.Glue)*(len(widths)-1) + for i, width := range widths { + if i != config.negIndex { + totalLineWidth += width + } + } + if maxOutputWidth > totalLineWidth { + widths[config.negIndex] = maxOutputWidth - totalLineWidth + } else { + config.negIndex = -1 + } + } return widths } @@ -83,6 +115,7 @@ func (c *Config) getStringFormat(widths []int, columns int) string { // configuration. Values from the right take precedence over the left side. func MergeConfig(a, b *Config) *Config { var result Config = *a + result.negIndex = -1 // Return quickly if either side was nil if a == nil || b == nil { @@ -101,6 +134,25 @@ func MergeConfig(a, b *Config) *Config { if b.Empty != "" { result.Empty = b.Empty } + if len(b.MaxWidth) > 0 { + for i, maxWidth := range b.MaxWidth { + if maxWidth < 0 { + // Negative width adjusts the column width so that the width of + // the entire output line is the abs value of the value specified. + // Only the first such specification is significant; others are ignored. + if result.negIndex < 0 { + result.negIndex = i + } else { + maxWidth = 0 + } + } + if i < len(result.MaxWidth) { + result.MaxWidth[i] = maxWidth + } else { + result.MaxWidth = append(result.MaxWidth, maxWidth) + } + } + } return &result } @@ -116,8 +168,13 @@ func Format(lines []string, config *Config) string { // Create the formatted output using the format string for _, line := range lines { elems := getElementsFromLine(conf, line) - stringfmt := conf.getStringFormat(widths, len(elems)) - result += fmt.Sprintf(stringfmt, elems...) + extensionLineElems := []string{} + isStillDataToFormat := true + for isStillDataToFormat { + isStillDataToFormat = truncateToWidth(&elems, &extensionLineElems, widths) + stringfmt := conf.getStringFormat(widths, len(elems)) + result += fmt.Sprintf(stringfmt, elems...) + } } // Remove trailing newline without removing leading/trailing space @@ -132,3 +189,44 @@ func Format(lines []string, config *Config) string { func SimpleFormat(lines []string) string { return Format(lines, nil) } + +// Truncate any elements exceeding their maximum width, and save their remaining +// data for an extension line. +func truncateToWidth(elems *[]interface{}, extensionLineElems *[]string, widths []int) (isStillDataToFormat bool) { + + // If this an extension line, make its list of elements current + + if len(*extensionLineElems) > 0 { + for i, elem := range *extensionLineElems { + (*elems)[i] = elem + } + *extensionLineElems = []string{} + } + + // Examine each element to determine if it exceeds its maximum allowed width. + // If so, truncate it at the closest whitespace to the limit and save its remaining + // data for the next extension line. + + for i, elem := range *elems { + stringElem := strings.TrimSpace(fmt.Sprintf("%s", elem)) + if len(stringElem) > widths[i] { + isStillDataToFormat = true + splitPoint := widths[i] + for ; splitPoint > 0; splitPoint-- { + if unicode.IsSpace(rune(stringElem[splitPoint])) { + break + } + } + if splitPoint == 0 { + splitPoint = widths[i] + } + (*elems)[i] = strings.TrimSpace(stringElem[:splitPoint]) + if len(*extensionLineElems) == 0 { + (*extensionLineElems) = make([]string, len(*elems)) + } + (*extensionLineElems)[i] = strings.TrimSpace(stringElem[splitPoint:]) + } + + } + return +} diff --git a/columnize_test.go b/columnize_test.go index 7bec390..546b4bb 100644 --- a/columnize_test.go +++ b/columnize_test.go @@ -1,6 +1,11 @@ package columnize -import "testing" +import ( + "fmt" + "math" + "strings" + "testing" +) func TestListOfStringsInput(t *testing.T) { input := []string{ @@ -240,3 +245,77 @@ func TestMergeConfig(t *testing.T) { t.Fatalf("bad: %#v", m) } } + +func TestMaxWidth(t *testing.T) { + input := []string{ + "Column a | Column b | Column c", + "xx | yy | zz", + "some quite long data | some more data | even longer data for the last column", + "this one will fit | a break | The quick brown fox jumps over the low lazy dog", + "qq | rr | ss", + } + config := Config{MaxWidth: []int{10, 0, 15}} + output := Format(input, &config) + expected := "Column a Column b Column c\n" + expected += "xx yy zz\n" + expected += "some quite some more data even longer\n" + expected += "long data data for the\n" + expected += " last column\n" + expected += "this one a break The quick brown\n" + expected += "will fit fox jumps over\n" + expected += " the low lazy\n" + expected += " dog\n" + expected += "qq rr ss" + + if output != expected { + for i, c := range output { + expectedChar := " " + if i < len(expected) { + expectedChar = string(expected[i]) + } + if c != rune(expectedChar[0]) { + nearStart := int(math.Max(0, float64(i-4))) + nearEnd := int(math.Min(float64(i+4), float64(len(output)))) + near := strings.Replace(output[nearStart:nearEnd], "\n", " ", -1) + fmt.Printf("TestMaxWidth difference at column %d near \"%s\": got(%s) expected(%s)\n", i, near, string(c), string(expectedChar)) + } + } + t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output) + } +} + +func TestNegativeMaxWidth(t *testing.T) { + input := []string{ + "Column a | Column b | Column c", + "xx | yy | zz", + "some quite long data | some more data | even longer data for the last column", + "this one will fit | a break | The quick brown fox jumps over the low lazy dog", + "qq | rr | ss", + } + config := Config{MaxWidth: []int{0, 0, -60}} + output := Format(input, &config) + expected := "Column a Column b Column c\n" + expected += "xx yy zz\n" + expected += "some quite long data some more data even longer data for\n" + expected += " the last column\n" + expected += "this one will fit a break The quick brown fox\n" + expected += " jumps over the low\n" + expected += " lazy dog\n" + expected += "qq rr ss" + + if output != expected { + for i, c := range output { + expectedChar := " " + if i < len(expected) { + expectedChar = string(expected[i]) + } + if c != rune(expectedChar[0]) { + nearStart := int(math.Max(0, float64(i-4))) + nearEnd := int(math.Min(float64(i+4), float64(len(output)))) + near := strings.Replace(output[nearStart:nearEnd], "\n", " ", -1) + fmt.Printf("TestNegativeMaxWidth difference at column %d near \"%s\": got(%s) expected(%s)\n", i, near, string(c), string(expectedChar)) + } + } + t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output) + } +} From bd971a71de83c030242e442b26dc3f9573a135f1 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Sun, 3 Jan 2016 16:28:42 -0800 Subject: [PATCH 2/8] Support OutputWidth configuration parm --- README.md | 35 ++++++----- columnize.go | 147 ++++++++++++++++++++++++++++++---------------- columnize_test.go | 21 ++++++- 3 files changed, 135 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index fd5494a..a491435 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,17 @@ Columnize is configured using a `Config`, which can be obtained by calling the config.Glue = " " config.Prefix = "" config.Empty = "" - config.MaxWidth = []int{10, 0, -80} + config.MaxWidth = []int{10, 0, 0} + config.OutputWidth = 80 * `Delim` is the string by which columns of **input** are delimited * `Glue` is the string by which columns of **output** are delimited * `Prefix` is a string by which each line of **output** is prefixed * `Empty` is a string used to replace blank values found in output -* `MaxWidth` is an int slice specifying the maximum width of each column. Columns exceeding their configured width are broken at a word boundary and continued on the next line. See below for details. +* `MaxWidth` is an int slice specifying the maximum width of each column. +* `OutputWidth` is an int specifying the maximum width of an output line. + +If MaxWidth or OutputWidth is specified and output exceeds the configured width, Columnize breaks a column at a word boundary and continues it on the next line. See below for details. You can then pass the `Config` in using the `Format` method (signature below) to have text formatted to your liking. @@ -68,9 +72,14 @@ Usage Format(input []string, config *Config) string -Using MaxWidth -============== -The MaxWidth config element allows you to configure the maximum width of each column. An input line with a data value exceeding the maximum width for a column is formatted into multiple lines, with each long column's data broken at a word boundary and continued on the line below. For example: +Controlling Output Width +======================== +Output exceeding the width of the terminal window - particularly columnized output - can be difficult to read. To address this, Columnize provides two configuration parameters for controlling output width. + +* `MaxWidth` is an int slice specifying the maximum width of each column. If the data for a column exceeds its maximum width, Columnize formats the column into two or more lines by breaking its data at a word boundary and continuing it onto the next line. A zero or missing value for a MaxWidth element specifies that the corresponding column is uncontrolled (no maximum width). +* `OutputWidth` is an int value specifying the maximum width of the entire output line (including prefix and glue). If data width exceeds this value, Columnize sets a MaxWidth for the rightmost uncontrolled column so that the output width satisfies the restriction. You can specify `OutputWidth: columnize.AUTO` to use the actual width of the terminal window for OutputWidth. + +For example: input := []string{ "Column a | Column b | Column c", @@ -95,20 +104,14 @@ results in the output: dog qq rr ss -Columns without a MaxWidth value, or columns with a MaxWidth value of zero, expand to the maximum data width as normal. - -If you want to restrict the maximum width of the entire output line, specify a negative value for exactly one of the columns in a MaxWidth specification. The inverse of the negative value is interpreted as the desired output line width, and columnize calculates a width for the designated column so that all output lines fit in the specified width. As a convenience, if you want the last column of data to be the one with the calculated width, specify MaxWidth as a single-element array with a negative value. +Specify OutputWidth to restrict the entire output line. For example, the configuration: -For example, if you want no output line to exceed 80 characters, you could specify: - - config := Config{MaxWidth: []int{0, -80, 15}} - -This specification causes the first column to be the width of the largest piece of data in that column; the last column to be a maximum of 15 characters; and the middle column to have whatever maximum width is required so that no output line exceeds 80 characters in width. + config := Config{ + OutputWidth: columnize.AUTO, + } -You could also specify: +causes the entire output line to fit in the terminal window. Columnize modifies data lines exceeding the width of the window by setting the appropriate MaxWidth for the last column of data. Columnize adjusts the last uncontrolled column, so if you want it to adjust a column other than the last, specify an explicit MaxWidth for any columns to the right of the one you want Columnize to adjust. - config := Config{MaxWidth: []int{-80}} -to request that no output line exceed 80 characters, with any line breaks occurring in the last data column. diff --git a/columnize.go b/columnize.go index 8abe070..6f593a0 100644 --- a/columnize.go +++ b/columnize.go @@ -2,6 +2,11 @@ package columnize import ( "fmt" + "os" + "os/exec" + "regexp" + "runtime" + "strconv" "strings" "unicode" ) @@ -19,21 +24,36 @@ type Config struct { // A replacement string to replace empty fields Empty string - // Maximum width of each field - MaxWidth []int + // Maximum width of output; set to AUTO to use actual console width + OutputWidth int - // Index of a negative value within config.MaxWidth - negIndex int + // Maximum width of each column + MaxWidth []int } +const ( + AUTO = -1 +) + +var ( + defaultConfig Config = Config{ + Delim: "|", + Glue: " ", + Prefix: "", + OutputWidth: -999, + MaxWidth: []int{}, + } +) + // Returns a Config with default values. func DefaultConfig() *Config { - return &Config{ - Delim: "|", - Glue: " ", - Prefix: "", - MaxWidth: []int{}, - } + var defaultConfigCopy Config = defaultConfig + return &defaultConfigCopy +} + +// Sets the default Config +func SetDefaultConfig(config Config) { + defaultConfig = config } // Returns a list of elements, each representing a single item which will @@ -58,36 +78,56 @@ func getWidthsFromLines(config *Config, lines []string) []int { for _, line := range lines { elems := getElementsFromLine(config, line) for i := 0; i < len(elems); i++ { - l := len(elems[i].(string)) - if i < len(config.MaxWidth) && config.MaxWidth[i] > 0 && config.MaxWidth[i] < l { - l = config.MaxWidth[i] + lenElem := len(elems[i].(string)) + if i < len(config.MaxWidth) { + if config.MaxWidth[i] < 0 { + fmt.Printf("Columnize: negative MaxWidth value not supported - please use OutputWidth\n") + } else if config.MaxWidth[i] > 0 && config.MaxWidth[i] < lenElem { + lenElem = config.MaxWidth[i] + } } if len(widths) <= i { - widths = append(widths, l) - } else if widths[i] < l { - widths[i] = l + widths = append(widths, lenElem) + } else if widths[i] < lenElem { + widths[i] = lenElem } } } - // If one of the columns has a negative width specification, set its width - // so that the entire output line has a width of the absolute value of the spec + // Get output width restriction - if config.negIndex >= 0 && config.negIndex < len(widths) { - maxOutputWidth := 0 - config.MaxWidth[config.negIndex] - if config.negIndex == 0 && len(config.MaxWidth) == 1 && len(widths) > 1 { - config.negIndex = len(widths) - 1 // Apply single negative value to last field + outputWidth := config.OutputWidth + if outputWidth == AUTO { + var e error + if outputWidth, e = GetConsoleWidth(); e != nil { + fmt.Printf("Unable to set AUTO OutputWidth: %s\n", e.Error()) } + } + + // If the output width is restricted and the output line will exceed that width, + // attempt to meet the restriction by adjusting the width of the rightmost + // unrestricted column, or the rightmost column if all columns are restricted. + + if outputWidth > 0 { totalLineWidth := len(config.Prefix) + len(config.Glue)*(len(widths)-1) - for i, width := range widths { - if i != config.negIndex { - totalLineWidth += width - } + for _, width := range widths { + totalLineWidth += width } - if maxOutputWidth > totalLineWidth { - widths[config.negIndex] = maxOutputWidth - totalLineWidth - } else { - config.negIndex = -1 + if totalLineWidth > outputWidth { + adjIndex := -1 + for i := len(widths) - 1; i >= 0; i-- { + if i >= len(config.MaxWidth) || config.MaxWidth[i] <= 0 { + adjIndex = i + break + } + } + if adjIndex < 0 { + adjIndex = len(widths) - 1 + } + adjustedWidth := outputWidth - (totalLineWidth - widths[adjIndex]) + if adjustedWidth > 0 { + widths[adjIndex] = adjustedWidth + } } } return widths @@ -115,7 +155,6 @@ func (c *Config) getStringFormat(widths []int, columns int) string { // configuration. Values from the right take precedence over the left side. func MergeConfig(a, b *Config) *Config { var result Config = *a - result.negIndex = -1 // Return quickly if either side was nil if a == nil || b == nil { @@ -134,24 +173,11 @@ func MergeConfig(a, b *Config) *Config { if b.Empty != "" { result.Empty = b.Empty } + if b.OutputWidth >= 0 || b.OutputWidth == AUTO { + result.OutputWidth = b.OutputWidth + } if len(b.MaxWidth) > 0 { - for i, maxWidth := range b.MaxWidth { - if maxWidth < 0 { - // Negative width adjusts the column width so that the width of - // the entire output line is the abs value of the value specified. - // Only the first such specification is significant; others are ignored. - if result.negIndex < 0 { - result.negIndex = i - } else { - maxWidth = 0 - } - } - if i < len(result.MaxWidth) { - result.MaxWidth[i] = maxWidth - } else { - result.MaxWidth = append(result.MaxWidth, maxWidth) - } - } + result.MaxWidth = b.MaxWidth } return &result @@ -162,7 +188,7 @@ func MergeConfig(a, b *Config) *Config { func Format(lines []string, config *Config) string { var result string - conf := MergeConfig(DefaultConfig(), config) + conf := MergeConfig(&defaultConfig, config) widths := getWidthsFromLines(conf, lines) // Create the formatted output using the format string @@ -230,3 +256,26 @@ func truncateToWidth(elems *[]interface{}, extensionLineElems *[]string, widths } return } + +// Get the width of the console +func GetConsoleWidth() (width int, e error) { + var rxGetWidth *regexp.Regexp + var command []string + if runtime.GOOS == "windows" { + command = []string{"mode", "con"} + rxGetWidth = regexp.MustCompile("Columns:\\s*(\\d+)") + } else { + command = []string{"stty", "size"} + rxGetWidth = regexp.MustCompile("\\d+\\s+(\\d+)") + } + cmd := exec.Command(command[0], command[1]) + cmd.Stdin = os.Stdin + if out, err := cmd.Output(); err != nil { + e = err + } else if match := rxGetWidth.FindSubmatch(out); match != nil { + width, e = strconv.Atoi(string(match[1])) + } else { + e = fmt.Errorf("not in %s %s output\n%s", command[0], command[1], out) + } + return +} diff --git a/columnize_test.go b/columnize_test.go index 546b4bb..8da01a7 100644 --- a/columnize_test.go +++ b/columnize_test.go @@ -284,7 +284,7 @@ func TestMaxWidth(t *testing.T) { } } -func TestNegativeMaxWidth(t *testing.T) { +func TestOutputxWidth(t *testing.T) { input := []string{ "Column a | Column b | Column c", "xx | yy | zz", @@ -292,7 +292,7 @@ func TestNegativeMaxWidth(t *testing.T) { "this one will fit | a break | The quick brown fox jumps over the low lazy dog", "qq | rr | ss", } - config := Config{MaxWidth: []int{0, 0, -60}} + config := Config{OutputWidth: 60} output := Format(input, &config) expected := "Column a Column b Column c\n" expected += "xx yy zz\n" @@ -313,9 +313,24 @@ func TestNegativeMaxWidth(t *testing.T) { nearStart := int(math.Max(0, float64(i-4))) nearEnd := int(math.Min(float64(i+4), float64(len(output)))) near := strings.Replace(output[nearStart:nearEnd], "\n", " ", -1) - fmt.Printf("TestNegativeMaxWidth difference at column %d near \"%s\": got(%s) expected(%s)\n", i, near, string(c), string(expectedChar)) + fmt.Printf("TestOutputWidth difference at column %d near \"%s\": got(%s) expected(%s)\n", i, near, string(c), string(expectedChar)) } } t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output) } } + +func TestGetConsoleWidth(t *testing.T) { + if width, e := GetConsoleWidth(); e != nil { + t.Fatalf("Error getting console width: %s", e.Error()) + } else { + fmt.Printf("Console width is %d\n", width) + } + config := Config{OutputWidth: AUTO} + input := []string{ + "Column A | Column B | Column C", + "This is column A data | This is column B data | This is column C data that should wrap if the terminal width is too small for it", + } + output := Format(input, &config) + fmt.Printf("%s\n", output) +} From 898fbb73976bce35d12fca77915087100f2ca6f9 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Sun, 3 Jan 2016 16:33:42 -0800 Subject: [PATCH 3/8] Correct typo in function name --- columnize_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/columnize_test.go b/columnize_test.go index 8da01a7..55bb06b 100644 --- a/columnize_test.go +++ b/columnize_test.go @@ -284,7 +284,7 @@ func TestMaxWidth(t *testing.T) { } } -func TestOutputxWidth(t *testing.T) { +func TestOutputWidth(t *testing.T) { input := []string{ "Column a | Column b | Column c", "xx | yy | zz", From 4c386bd02845e5713ce3391fe5b201892abb0934 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Thu, 7 Jan 2016 14:09:34 -0800 Subject: [PATCH 4/8] Include command output with console width error --- columnize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/columnize.go b/columnize.go index 6f593a0..5703725 100644 --- a/columnize.go +++ b/columnize.go @@ -271,7 +271,7 @@ func GetConsoleWidth() (width int, e error) { cmd := exec.Command(command[0], command[1]) cmd.Stdin = os.Stdin if out, err := cmd.Output(); err != nil { - e = err + e = fmt.Errorf("%s\n%s", out, err.Error()) } else if match := rxGetWidth.FindSubmatch(out); match != nil { width, e = strconv.Atoi(string(match[1])) } else { From af5ae020f295d509459f8cf69bf32c253f5214e7 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Thu, 7 Jan 2016 14:27:36 -0800 Subject: [PATCH 5/8] Don't fail test when base console width command fails --- columnize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/columnize.go b/columnize.go index 5703725..83f97f8 100644 --- a/columnize.go +++ b/columnize.go @@ -271,7 +271,7 @@ func GetConsoleWidth() (width int, e error) { cmd := exec.Command(command[0], command[1]) cmd.Stdin = os.Stdin if out, err := cmd.Output(); err != nil { - e = fmt.Errorf("%s\n%s", out, err.Error()) + fmt.Printf("Unable to get console width: %s (%s)", err.Error(), out) } else if match := rxGetWidth.FindSubmatch(out); match != nil { width, e = strconv.Atoi(string(match[1])) } else { From c6f7b21fc948a7538126ce25e00542dad2ef8d35 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Thu, 7 Jan 2016 14:39:53 -0800 Subject: [PATCH 6/8] Add missing newline --- columnize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/columnize.go b/columnize.go index 83f97f8..ee58be6 100644 --- a/columnize.go +++ b/columnize.go @@ -271,7 +271,7 @@ func GetConsoleWidth() (width int, e error) { cmd := exec.Command(command[0], command[1]) cmd.Stdin = os.Stdin if out, err := cmd.Output(); err != nil { - fmt.Printf("Unable to get console width: %s (%s)", err.Error(), out) + fmt.Printf("Unable to get console width: %s (%s)\n", err.Error(), out) } else if match := rxGetWidth.FindSubmatch(out); match != nil { width, e = strconv.Atoi(string(match[1])) } else { From c0537650f5a8f37ce874b7952aeac20dc189d3c8 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Tue, 19 Jan 2016 21:30:16 -0800 Subject: [PATCH 7/8] Refactor getWidthsFromLines --- columnize.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/columnize.go b/columnize.go index ee58be6..90b2cb0 100644 --- a/columnize.go +++ b/columnize.go @@ -73,8 +73,14 @@ func getElementsFromLine(config *Config, line string) []interface{} { // Examines a list of strings and determines how wide each column should be // considering all of the elements that need to be printed within it. func getWidthsFromLines(config *Config, lines []string) []int { - var widths []int + widths := calculateColumnWidths(config, lines) + outputWidth := getOutputWidth(config) + widths = adjustWidths(config, widths, outputWidth) + return widths +} +// Calculate column widths by comparing data width and MaxWidth +func calculateColumnWidths(config *Config, lines []string) (widths []int) { for _, line := range lines { elems := getElementsFromLine(config, line) for i := 0; i < len(elems); i++ { @@ -93,21 +99,25 @@ func getWidthsFromLines(config *Config, lines []string) []int { } } } + return +} - // Get output width restriction - - outputWidth := config.OutputWidth +// Get output width specification +func getOutputWidth(config *Config) (outputWidth int) { + outputWidth = config.OutputWidth if outputWidth == AUTO { var e error if outputWidth, e = GetConsoleWidth(); e != nil { fmt.Printf("Unable to set AUTO OutputWidth: %s\n", e.Error()) } } + return +} - // If the output width is restricted and the output line will exceed that width, - // attempt to meet the restriction by adjusting the width of the rightmost - // unrestricted column, or the rightmost column if all columns are restricted. - +// If the output width is restricted and the output line will exceed that width, +// attempt to meet the restriction by adjusting the width of the rightmost +// unrestricted column, or the rightmost column if all columns are restricted. +func adjustWidths(config *Config, widths []int, outputWidth int) []int { if outputWidth > 0 { totalLineWidth := len(config.Prefix) + len(config.Glue)*(len(widths)-1) for _, width := range widths { From 3a6d63048d32cea71e4b519c7c590207b8c1c1d6 Mon Sep 17 00:00:00 2001 From: David Tootill Date: Sat, 27 Feb 2016 12:00:43 -0800 Subject: [PATCH 8/8] Use termsize package; remove SetDefaultConfig() --- Godeps/Godeps.json | 13 ++++ Godeps/Readme | 5 ++ Godeps/_workspace/.gitignore | 2 + .../DeMille/termsize/.gitattributes | 2 + .../src/github.com/DeMille/termsize/LICENSE | 20 ++++++ .../src/github.com/DeMille/termsize/README.md | 61 ++++++++++++++++ .../github.com/DeMille/termsize/getsize.go | 42 +++++++++++ .../DeMille/termsize/getsize_windows.go | 72 +++++++++++++++++++ .../github.com/DeMille/termsize/termsize.go | 57 +++++++++++++++ columnize.go | 44 +++--------- godep.sh | 3 + 11 files changed, 285 insertions(+), 36 deletions(-) create mode 100644 Godeps/Godeps.json create mode 100644 Godeps/Readme create mode 100644 Godeps/_workspace/.gitignore create mode 100644 Godeps/_workspace/src/github.com/DeMille/termsize/.gitattributes create mode 100644 Godeps/_workspace/src/github.com/DeMille/termsize/LICENSE create mode 100644 Godeps/_workspace/src/github.com/DeMille/termsize/README.md create mode 100644 Godeps/_workspace/src/github.com/DeMille/termsize/getsize.go create mode 100644 Godeps/_workspace/src/github.com/DeMille/termsize/getsize_windows.go create mode 100644 Godeps/_workspace/src/github.com/DeMille/termsize/termsize.go create mode 100644 godep.sh diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 0000000..9f22636 --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,13 @@ +{ + "ImportPath": "github.com/tooda02/columnize", + "GoVersion": "go1.4.2", + "Packages": [ + "." + ], + "Deps": [ + { + "ImportPath": "github.com/DeMille/termsize", + "Rev": "b7100f0f89ccfa4a591ca92363fe5d434f54666f" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 0000000..4cdaa53 --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Godeps/_workspace/.gitignore b/Godeps/_workspace/.gitignore new file mode 100644 index 0000000..f037d68 --- /dev/null +++ b/Godeps/_workspace/.gitignore @@ -0,0 +1,2 @@ +/pkg +/bin diff --git a/Godeps/_workspace/src/github.com/DeMille/termsize/.gitattributes b/Godeps/_workspace/src/github.com/DeMille/termsize/.gitattributes new file mode 100644 index 0000000..85ddc8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/DeMille/termsize/.gitattributes @@ -0,0 +1,2 @@ +# +* text eol=lf \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/DeMille/termsize/LICENSE b/Godeps/_workspace/src/github.com/DeMille/termsize/LICENSE new file mode 100644 index 0000000..9f82fd7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/DeMille/termsize/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Sterling DeMille + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/DeMille/termsize/README.md b/Godeps/_workspace/src/github.com/DeMille/termsize/README.md new file mode 100644 index 0000000..70c9c43 --- /dev/null +++ b/Godeps/_workspace/src/github.com/DeMille/termsize/README.md @@ -0,0 +1,61 @@ +# Termsize + +Termsize is a cross platform go library to get terminal size. + +It is adapted from the syscalls implemented in [termbox-go](https://github.com/nsf/termbox-go), but doesn't require a full blown termbox setup. + +### Usage + +Install with `go get -u github.com/demille/termsize` + +```go +package main + +import ( + "fmt" + "github.com/demille/termsize" +) + +func main() { + if err := termsize.Init(); err != nil { + panic(err) + } + + w, h, err := termsize.Size() + if err != nil { + panic(err) + } + + fmt.Printf("Size: %d X %d \n", w, h) + // Size: 110 X 30 +} +``` + +Boom, terminal size. There isn't anything else to this package. + +### Reference + +[godoc.org/github.com/DeMille/termsize](https://godoc.org/github.com/DeMille/termsize) + +### License + +The MIT License (MIT) + +Copyright (c) 2015 Sterling DeMille <sterlingdemille@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/DeMille/termsize/getsize.go b/Godeps/_workspace/src/github.com/DeMille/termsize/getsize.go new file mode 100644 index 0000000..d4d488a --- /dev/null +++ b/Godeps/_workspace/src/github.com/DeMille/termsize/getsize.go @@ -0,0 +1,42 @@ +// +build !windows + +package termsize + +import ( + "os" + "syscall" + "unsafe" +) + +// +// adapted from termbox-go: +// github.com/nsf/termbox-go +// + +type winsize struct { + Row uint16 + Col uint16 + Xpixel uint16 + Ypixel uint16 +} + +var f *os.File + +func initialize() (err error) { + f, err = os.OpenFile("/dev/tty", syscall.O_WRONLY, 0) + return err +} + +func get_size() (int, int, error) { + ws := &winsize{} + retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, + f.Fd(), + uintptr(syscall.TIOCGWINSZ), + uintptr(unsafe.Pointer(ws))) + + if int(retCode) == -1 { + return 0, 0, errno + } + + return int(ws.Col), int(ws.Row), nil +} diff --git a/Godeps/_workspace/src/github.com/DeMille/termsize/getsize_windows.go b/Godeps/_workspace/src/github.com/DeMille/termsize/getsize_windows.go new file mode 100644 index 0000000..3dda72d --- /dev/null +++ b/Godeps/_workspace/src/github.com/DeMille/termsize/getsize_windows.go @@ -0,0 +1,72 @@ +package termsize + +import ( + "syscall" + "unsafe" +) + +// +// adapted from termbox-go: +// github.com/nsf/termbox-go +// + +type ( + short int16 + word uint16 + + coord struct { + x short + y short + } + + small_rect struct { + left short + top short + right short + bottom short + } + + buffer_info struct { + size coord + cursor_position coord + attributes word + window small_rect + maximum_window_size coord + } +) + +var ( + handle syscall.Handle + tmp_info buffer_info + kernel32 = syscall.NewLazyDLL("kernel32.dll") + proc = kernel32.NewProc("GetConsoleScreenBufferInfo") +) + +func initialize() (err error) { + handle, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0) + return +} + +func get_size() (w, h int, err error) { + err = get_buffer_info(handle, &tmp_info) + if err != nil { + return + } + + w = int(tmp_info.window.right - tmp_info.window.left + 1) + h = int(tmp_info.window.bottom - tmp_info.window.top + 1) + return +} + +func get_buffer_info(h syscall.Handle, info *buffer_info) (err error) { + retCode, _, e1 := syscall.Syscall(proc.Addr(), + 2, uintptr(h), uintptr(unsafe.Pointer(info)), 0) + if int(retCode) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} diff --git a/Godeps/_workspace/src/github.com/DeMille/termsize/termsize.go b/Godeps/_workspace/src/github.com/DeMille/termsize/termsize.go new file mode 100644 index 0000000..ac7bc16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/DeMille/termsize/termsize.go @@ -0,0 +1,57 @@ +/* +Package termsize is a cross platform go library to get terminal size. + +It is adapted from the syscalls implemented in termbox-go, but doesn't require a full blown termbox setup. + + package main + + import ( + "fmt" + "github.com/demille/termsize" + ) + + func main() { + if err := termsize.Init(); err != nil { + panic(err) + } + + w, h, err := termsize.Size() + if err != nil { + panic(err) + } + + fmt.Printf("Size: %d X %d \n", w, h) + // Size: 110 X 30 + } +*/ +package termsize + +// +import ( + "errors" +) + +// Tracks initialization status +var IsInit bool + +// Initializes the termsize package. +// Needs to be called before requesting size. +func Init() (err error) { + err = initialize() + if err != nil { + return + } + + IsInit = true + return +} + +// Returns the terminal size +func Size() (w, h int, err error) { + if !IsInit { + err = errors.New("termsize not yet iniitialied") + return + } + + return get_size() +} diff --git a/columnize.go b/columnize.go index 90b2cb0..9e8b83b 100644 --- a/columnize.go +++ b/columnize.go @@ -2,13 +2,10 @@ package columnize import ( "fmt" - "os" - "os/exec" - "regexp" - "runtime" - "strconv" "strings" "unicode" + + "github.com/tooda02/columnize/Godeps/_workspace/src/github.com/DeMille/termsize" ) type Config struct { @@ -35,25 +32,15 @@ const ( AUTO = -1 ) -var ( - defaultConfig Config = Config{ +// Returns a Config with default values. +func DefaultConfig() *Config { + return &Config{ Delim: "|", Glue: " ", Prefix: "", OutputWidth: -999, MaxWidth: []int{}, } -) - -// Returns a Config with default values. -func DefaultConfig() *Config { - var defaultConfigCopy Config = defaultConfig - return &defaultConfigCopy -} - -// Sets the default Config -func SetDefaultConfig(config Config) { - defaultConfig = config } // Returns a list of elements, each representing a single item which will @@ -198,7 +185,7 @@ func MergeConfig(a, b *Config) *Config { func Format(lines []string, config *Config) string { var result string - conf := MergeConfig(&defaultConfig, config) + conf := MergeConfig(DefaultConfig(), config) widths := getWidthsFromLines(conf, lines) // Create the formatted output using the format string @@ -269,23 +256,8 @@ func truncateToWidth(elems *[]interface{}, extensionLineElems *[]string, widths // Get the width of the console func GetConsoleWidth() (width int, e error) { - var rxGetWidth *regexp.Regexp - var command []string - if runtime.GOOS == "windows" { - command = []string{"mode", "con"} - rxGetWidth = regexp.MustCompile("Columns:\\s*(\\d+)") - } else { - command = []string{"stty", "size"} - rxGetWidth = regexp.MustCompile("\\d+\\s+(\\d+)") - } - cmd := exec.Command(command[0], command[1]) - cmd.Stdin = os.Stdin - if out, err := cmd.Output(); err != nil { - fmt.Printf("Unable to get console width: %s (%s)\n", err.Error(), out) - } else if match := rxGetWidth.FindSubmatch(out); match != nil { - width, e = strconv.Atoi(string(match[1])) - } else { - e = fmt.Errorf("not in %s %s output\n%s", command[0], command[1], out) + if e = termsize.Init(); e == nil { + width, _, e = termsize.Size() } return } diff --git a/godep.sh b/godep.sh new file mode 100644 index 0000000..10a65b4 --- /dev/null +++ b/godep.sh @@ -0,0 +1,3 @@ +#!/bin/sh -x +rm -f Godeps/Godeps.json +godep save -r -t .