Skip to content

Commit

Permalink
utter: add quoting strategy support
Browse files Browse the repository at this point in the history
  • Loading branch information
kortschak committed Feb 5, 2022
1 parent 1166d33 commit c8b05e7
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 1 deletion.
2 changes: 2 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
26 changes: 26 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
107 changes: 106 additions & 1 deletion dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)

var (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 39 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
38 changes: 38 additions & 0 deletions spew_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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"},
}
}

Expand Down

0 comments on commit c8b05e7

Please sign in to comment.