diff --git a/common.go b/common.go index 870839f..2065f8e 100644 --- a/common.go +++ b/common.go @@ -153,6 +153,8 @@ func unsafeReflectValue(v reflect.Value) (rv reflect.Value) { // Some constants in the form of bytes to avoid string overhead. This mirrors // the technique used in the fmt package. var ( + backQuoteBytes = []byte("`") + quoteBytes = []byte(`"`) plusBytes = []byte("+") iBytes = []byte("i") trueBytes = []byte("true") diff --git a/config.go b/config.go index 506ff8c..25d03bd 100644 --- a/config.go +++ b/config.go @@ -50,6 +50,9 @@ type ConfigState struct { // a string slice or array. Zero specifies all entries on one line. StringWidth int + // Quoting specifies the quoting strategy to use when printing strings. + Quoting Quoting + // BytesWidth specifies the number of byte columns to use when dumping a // byte slice or array. BytesWidth int @@ -84,6 +87,29 @@ type ConfigState struct { SortKeys bool } +// Quoting describes string quoting strategies. +// +// The numerical values of quoting constants are not guaranteed to be stable. +type Quoting uint + +const ( + // Quote strings with double quotes. + DoubleQuote Quoting = 0 + + // AvoidEscapes quotes strings using backquotes to avoid escape + // sequences if possible, otherwise double quotes are used. + AvoidEscapes Quoting = 1 << iota + + // Backquote always quotes strings using backquotes where possible + // within the string. For sections of strings that can not be + // backquoted, additional double quote syntax is used. + Backquote + + // Force is a modifier of AvoidEscapes that adds additional double + // quote syntax to represent parts that cannot be backquoted. + Force +) + // Config is the active configuration of the top-level functions. // The configuration can be changed by modifying the contents of utter.Config. var Config = ConfigState{ diff --git a/dump.go b/dump.go index 43ac0ff..dd65947 100644 --- a/dump.go +++ b/dump.go @@ -26,6 +26,8 @@ import ( "regexp" "strconv" "strings" + "unicode" + "unicode/utf8" ) var ( @@ -463,7 +465,7 @@ func (d *dumpState) dump(v reflect.Value, wasPtr, static, canElideCompound bool, d.dumpSlice(v, !interfaceContext) case reflect.String: - d.w.Write([]byte(strconv.Quote(v.String()))) + d.writeQuoted(v.String()) case reflect.Interface: // The only time we should get here is for nil interfaces due to @@ -585,6 +587,109 @@ func (d *dumpState) dump(v reflect.Value, wasPtr, static, canElideCompound bool, } } +// writeQuoted writes the string s quoted according to the quoting strategy. +func (d *dumpState) writeQuoted(s string) { + switch d.cs.Quoting { + default: + fallthrough + case DoubleQuote: + d.w.Write([]byte(strconv.Quote(s))) + + case AvoidEscapes: + if !needsEscape(s) || !canBackquoteString(s) { + d.w.Write([]byte(strconv.Quote(s))) + return + } + d.backQuote(s) + + case AvoidEscapes | Force: + if !needsEscape(s) { + d.w.Write([]byte(strconv.Quote(s))) + return + } + + fallthrough + case Backquote, Backquote | Force: + if canBackquoteString(s) { + d.backQuote(s) + return + } + + var last int + inBackquote := true + for i, r := range s { + if canBackquote(r) != inBackquote { + if last != 0 { + d.w.Write(plusBytes) + } + if inBackquote { + if i != last { + d.backQuote(s[last:i]) + } + } else { + d.w.Write([]byte(strconv.Quote(s[last:i]))) + } + last = i + inBackquote = !inBackquote + } + } + if last != len(s) { + if last != 0 { + d.w.Write(plusBytes) + } + if !inBackquote { + d.w.Write([]byte(strconv.Quote(s[last:]))) + return + } + d.backQuote(s[last:]) + } + } +} + +// backQuote writes s backquoted. +func (d *dumpState) backQuote(s string) { + d.w.Write(backQuoteBytes) + d.w.Write([]byte(s)) + d.w.Write(backQuoteBytes) +} + +// needsEscape returns whether the string s needs any escape sequence to be +// double quote printed. +func needsEscape(s string) bool { + for _, r := range s { + if r == '"' || r == '\\' { + return true + } + if !strconv.IsPrint(r) && !strconv.IsGraphic(r) { + return true + } + } + return false +} + +// canBackquoteString returns whether the string s can be represented +// unchanged as a backquoted string without non-space control characters. +func canBackquoteString(s string) bool { + for _, r := range s { + if !canBackquote(r) { + return false + } + } + return true +} + +// canBackquote returns whether the rune r can be represented unchanged as a +// backquoted string without non-space control characters. +func canBackquote(r rune) bool { + if r == utf8.RuneError { + return false + } + if utf8.RuneLen(r) > 1 { + return r != '\ufeff' + } + return (unicode.IsSpace(r) || ' ' < r) && r != '`' && r != '\u007f' +} + // typeString returns the string representation of the reflect.Type with the local // package selector removed. func typeString(typ reflect.Type, local string) string { diff --git a/example_test.go b/example_test.go index dc6e527..e49bd9d 100644 --- a/example_test.go +++ b/example_test.go @@ -139,6 +139,45 @@ func ExampleConfigState() { // } } +// This example demonstrates how to use a Quoting strategy. +func ExampleConfigState_Quoting() { + scs := utter.ConfigState{ + Indent: "\t", ElideType: true, SortKeys: true, + + // Avoid escape sequences when present and force + // use of backquotes even when the complete string is + // not backquotable. + Quoting: utter.AvoidEscapes | utter.Force, + } + + v := map[string]string{ + "1. one": "this\ntext\nspans\nlines\n", + "2. two": "this text doesn't", + "3.\nt\nh\nr\ne\ne\n": "vertical key", + "4. four": "contains \\backslashes\\ and `backquotes`", + } + scs.Dump(v) + + // Output: + // + // map[string]string{ + // "1. one": `this + // text + // spans + // lines + // `, + // "2. two": "this text doesn't", + // `3. + // t + // h + // r + // e + // e + // `: "vertical key", + // "4. four": `contains \backslashes\ and `+"`"+`backquotes`+"`", + // } +} + // This example demonstrates how to use ConfigState.Dump to dump variables to // stdout func ExampleConfigState_Dump() { diff --git a/spew_test.go b/spew_test.go index 436e223..f25e477 100644 --- a/spew_test.go +++ b/spew_test.go @@ -110,6 +110,21 @@ func initSpewTests() { elideTypeDefault := utter.NewDefaultConfig() elideTypeDefault.ElideType = true + // AvoidEscape. + avoidEscape := utter.NewDefaultConfig() + avoidEscape.SortKeys = true + avoidEscape.Quoting = utter.AvoidEscapes + + // AvoidEscape|Force. + avoidEscapeForce := utter.NewDefaultConfig() + avoidEscapeForce.SortKeys = true + avoidEscapeForce.Quoting = utter.AvoidEscapes | utter.Force + + // Backquote. + backquote := utter.NewDefaultConfig() + backquote.SortKeys = true + backquote.Quoting = utter.Backquote + var ( np *int nip = new(interface{}) @@ -265,6 +280,29 @@ func initSpewTests() { " []int{1, 2, 3, 4},\n" + " []string{\"one\", \"two\", \"three\", \"four\", \"five\"},\n}\n", }, + {avoidEscape, fCSFdump, map[string]string{ + "one": "\no\nn\ne\n", + "\nt\nw\no\n": "two", + "three": "`t\th\tr\te\te`", + "codeblock": "```\ncode\n```\n", + }, "map[string]string{\n string(`\nt\nw\no\n`): string(\"two\"),\n string(\"codeblock\"): string(\"```\\ncode\\n```\\n\"),\n string(\"one\"): string(`\no\nn\ne\n`),\n string(\"three\"): string(\"`t\\th\\tr\\te\\te`\"),\n}\n"}, + {avoidEscapeForce, fCSFdump, map[string]string{ + "one": "\no\nn\ne\n", + "\nt\nw\no\n": "two", + "three": "`t\th\tr\te\te`", + "codeblock": "```\ncode\n```\n", + }, "map[string]string{\n string(`\nt\nw\no\n`): string(\"two\"),\n string(\"codeblock\"): string(\"```\"+`\ncode\n`+\"```\"+`\n`),\n string(\"one\"): string(`\no\nn\ne\n`),\n string(\"three\"): string(\"`\"+`t\th\tr\te\te`+\"`\"),\n}\n"}, + {backquote, fCSFdump, map[string]string{ + "one": "\no\nn\ne\n", + "\nt\nw\no\n": "two", + "three": "`t\th\tr\te\te`", + "backquote": "`", + "tabbackquote": "\t`", + "backquotetab": "`\t", + "tabbackquotetab": "\t`\t", + "backquotetabbackquote": "`\t`", + "codeblock": "```\ncode\n```\n", + }, "map[string]string{\n string(`\nt\nw\no\n`): string(`two`),\n string(`backquote`): string(\"`\"),\n string(`backquotetab`): string(\"`\"+`\t`),\n string(`backquotetabbackquote`): string(\"`\"+`\t`+\"`\"),\n string(`codeblock`): string(\"```\"+`\ncode\n`+\"```\"+`\n`),\n string(`one`): string(`\no\nn\ne\n`),\n string(`tabbackquote`): string(`\t`+\"`\"),\n string(`tabbackquotetab`): string(`\t`+\"`\"+`\t`),\n string(`three`): string(\"`\"+`t\th\tr\te\te`+\"`\"),\n}\n"}, } }