Source file src/encoding/csv/writer.go
1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package csv 6 7 import ( 8 "bufio" 9 "io" 10 "strings" 11 "unicode" 12 "unicode/utf8" 13 ) 14 15 // A Writer writes records using CSV encoding. 16 // 17 // As returned by [NewWriter], a Writer writes records terminated by a 18 // newline and uses ',' as the field delimiter. The exported fields can be 19 // changed to customize the details before 20 // the first call to [Writer.Write] or [Writer.WriteAll]. 21 // 22 // [Writer.Comma] is the field delimiter. 23 // 24 // If [Writer.UseCRLF] is true, 25 // the Writer ends each output line with \r\n instead of \n. 26 // 27 // The writes of individual records are buffered. 28 // After all data has been written, the client should call the 29 // [Writer.Flush] method to guarantee all data has been forwarded to 30 // the underlying [io.Writer]. Any errors that occurred should 31 // be checked by calling the [Writer.Error] method. 32 type Writer struct { 33 Comma rune // Field delimiter (set to ',' by NewWriter) 34 UseCRLF bool // True to use \r\n as the line terminator 35 w *bufio.Writer 36 } 37 38 // NewWriter returns a new Writer that writes to w. 39 func NewWriter(w io.Writer) *Writer { 40 return &Writer{ 41 Comma: ',', 42 w: bufio.NewWriter(w), 43 } 44 } 45 46 // Write writes a single CSV record to w along with any necessary quoting. 47 // A record is a slice of strings with each string being one field. 48 // Writes are buffered, so [Writer.Flush] must eventually be called to ensure 49 // that the record is written to the underlying [io.Writer]. 50 func (w *Writer) Write(record []string) error { 51 if !validDelim(w.Comma) { 52 return errInvalidDelim 53 } 54 55 for n, field := range record { 56 if n > 0 { 57 if _, err := w.w.WriteRune(w.Comma); err != nil { 58 return err 59 } 60 } 61 62 // If we don't have to have a quoted field then just 63 // write out the field and continue to the next field. 64 if !w.fieldNeedsQuotes(field) { 65 if _, err := w.w.WriteString(field); err != nil { 66 return err 67 } 68 continue 69 } 70 71 if err := w.w.WriteByte('"'); err != nil { 72 return err 73 } 74 for len(field) > 0 { 75 // Search for special characters. 76 i := strings.IndexAny(field, "\"\r\n") 77 if i < 0 { 78 i = len(field) 79 } 80 81 // Copy verbatim everything before the special character. 82 if _, err := w.w.WriteString(field[:i]); err != nil { 83 return err 84 } 85 field = field[i:] 86 87 // Encode the special character. 88 if len(field) > 0 { 89 var err error 90 switch field[0] { 91 case '"': 92 _, err = w.w.WriteString(`""`) 93 case '\r': 94 if !w.UseCRLF { 95 err = w.w.WriteByte('\r') 96 } 97 case '\n': 98 if w.UseCRLF { 99 _, err = w.w.WriteString("\r\n") 100 } else { 101 err = w.w.WriteByte('\n') 102 } 103 } 104 field = field[1:] 105 if err != nil { 106 return err 107 } 108 } 109 } 110 if err := w.w.WriteByte('"'); err != nil { 111 return err 112 } 113 } 114 var err error 115 if w.UseCRLF { 116 _, err = w.w.WriteString("\r\n") 117 } else { 118 err = w.w.WriteByte('\n') 119 } 120 return err 121 } 122 123 // Flush writes any buffered data to the underlying [io.Writer]. 124 // To check if an error occurred during Flush, call [Writer.Error]. 125 func (w *Writer) Flush() { 126 w.w.Flush() 127 } 128 129 // Error reports any error that has occurred during 130 // a previous [Writer.Write] or [Writer.Flush]. 131 func (w *Writer) Error() error { 132 _, err := w.w.Write(nil) 133 return err 134 } 135 136 // WriteAll writes multiple CSV records to w using [Writer.Write] and 137 // then calls [Writer.Flush], returning any error from the Flush. 138 func (w *Writer) WriteAll(records [][]string) error { 139 for _, record := range records { 140 err := w.Write(record) 141 if err != nil { 142 return err 143 } 144 } 145 return w.w.Flush() 146 } 147 148 // fieldNeedsQuotes reports whether our field must be enclosed in quotes. 149 // Fields with a Comma, fields with a quote or newline, and 150 // fields which start with a space must be enclosed in quotes. 151 // We used to quote empty strings, but we do not anymore (as of Go 1.4). 152 // The two representations should be equivalent, but Postgres distinguishes 153 // quoted vs non-quoted empty string during database imports, and it has 154 // an option to force the quoted behavior for non-quoted CSV but it has 155 // no option to force the non-quoted behavior for quoted CSV, making 156 // CSV with quoted empty strings strictly less useful. 157 // Not quoting the empty string also makes this package match the behavior 158 // of Microsoft Excel and Google Drive. 159 // For Postgres, quote the data terminating string `\.`. 160 func (w *Writer) fieldNeedsQuotes(field string) bool { 161 if field == "" { 162 return false 163 } 164 165 if field == `\.` { 166 return true 167 } 168 169 if w.Comma < utf8.RuneSelf { 170 for i := 0; i < len(field); i++ { 171 c := field[i] 172 if c == '\n' || c == '\r' || c == '"' || c == byte(w.Comma) { 173 return true 174 } 175 } 176 } else { 177 if strings.ContainsRune(field, w.Comma) || strings.ContainsAny(field, "\"\r\n") { 178 return true 179 } 180 } 181 182 r1, _ := utf8.DecodeRuneInString(field) 183 return unicode.IsSpace(r1) 184 } 185