diff --git a/text_formatter.go b/text_formatter.go index ef799f1..ba88854 100644 --- a/text_formatter.go +++ b/text_formatter.go @@ -7,7 +7,6 @@ import ( "strings" "sync" "time" - "unicode/utf8" ) const ( @@ -53,8 +52,9 @@ type TextFormatter struct { // QuoteEmptyFields will wrap empty fields in quotes if true QuoteEmptyFields bool - // QuoteRune can be set to override the default quote style - QuoteRune rune + // QuoteCharacter can be set to the override the default quoting character " + // with something else. For example: ', or `. + QuoteCharacter string // Whether the logger's out is to a terminal isTerminal bool @@ -62,6 +62,15 @@ type TextFormatter struct { sync.Once } +func (f *TextFormatter) init(entry *Entry) { + if len(f.QuoteCharacter) == 0 { + f.QuoteCharacter = "\"" + } + if entry.Logger != nil { + f.isTerminal = IsTerminal(entry.Logger.Out) + } +} + func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { var b *bytes.Buffer keys := make([]string, 0, len(entry.Data)) @@ -80,14 +89,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { prefixFieldClashes(entry.Data) - f.Do(func() { - if f.QuoteRune == 0 || !utf8.ValidRune(f.QuoteRune) { - f.QuoteRune = '"' - } - if entry.Logger != nil { - f.isTerminal = IsTerminal(entry.Logger.Out) - } - }) + f.Do(func() { f.init(entry) }) isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors @@ -172,14 +174,14 @@ func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { if !f.needsQuoting(value) { b.WriteString(value) } else { - fmt.Fprintf(b, "%c%v%c", f.QuoteRune, value, f.QuoteRune) + fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, value, f.QuoteCharacter) } case error: errmsg := value.Error() if !f.needsQuoting(errmsg) { b.WriteString(errmsg) } else { - fmt.Fprintf(b, "%c%v%c", f.QuoteRune, errmsg, f.QuoteRune) + fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, errmsg, f.QuoteCharacter) } default: fmt.Fprint(b, value) diff --git a/text_formatter_test.go b/text_formatter_test.go index fce4cfe..9793b5f 100644 --- a/text_formatter_test.go +++ b/text_formatter_test.go @@ -14,7 +14,7 @@ func TestQuoting(t *testing.T) { checkQuoting := func(q bool, value interface{}) { b, _ := tf.Format(WithField("test", value)) idx := bytes.Index(b, ([]byte)("test=")) - cont := bytes.ContainsRune(b[idx+5:], tf.QuoteRune) + cont := bytes.Contains(b[idx+5:], []byte(tf.QuoteCharacter)) if cont != q { if q { t.Errorf("quoting expected for: %#v", value) @@ -34,16 +34,23 @@ func TestQuoting(t *testing.T) { checkQuoting(false, errors.New("invalid")) checkQuoting(true, errors.New("invalid argument")) - // Test for custom quote rune. - tf.QuoteRune = '`' + // Test for custom quote character. + tf.QuoteCharacter = "`" checkQuoting(false, "") checkQuoting(false, "abcd") checkQuoting(true, "/foobar") checkQuoting(true, errors.New("invalid argument")) + // Test for multi-character quotes. + tf.QuoteCharacter = "§~±" + checkQuoting(false, "abcd") + checkQuoting(true, errors.New("invalid argument")) + // Test for quoting empty fields. tf.QuoteEmptyFields = true checkQuoting(true, "") + checkQuoting(false, "abcd") + checkQuoting(true, errors.New("invalid argument")) } func TestTimestampFormat(t *testing.T) { @@ -52,11 +59,7 @@ func TestTimestampFormat(t *testing.T) { customStr, _ := customFormatter.Format(WithField("test", "test")) timeStart := bytes.Index(customStr, ([]byte)("time=")) timeEnd := bytes.Index(customStr, ([]byte)("level=")) - timeStr := customStr[timeStart+5 : timeEnd-1] - if timeStr[0] == byte(customFormatter.QuoteRune) && - timeStr[len(timeStr)-1] == byte(customFormatter.QuoteRune) { - timeStr = timeStr[1 : len(timeStr)-1] - } + timeStr := customStr[timeStart+5+len(customFormatter.QuoteCharacter) : timeEnd-1-len(customFormatter.QuoteCharacter)] if format == "" { format = time.RFC3339 }