Skip to content

Commit

Permalink
utter: add quoting strategy support
Browse files Browse the repository at this point in the history
WIP:
* needs tests
* API needs more thought
  • Loading branch information
kortschak committed Feb 5, 2022
1 parent 1166d33 commit 62cc620
Show file tree
Hide file tree
Showing 4 changed files with 171 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
105 changes: 104 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,107 @@ 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 {
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

0 comments on commit 62cc620

Please sign in to comment.