mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
* Honor --ors CRLF for CSV output (#1810) The full-CSV record writer validated ORS as newline or carriage-return/newline, but never propagated the choice to the forked Go CSV writer's UseCRLF field, so `--ors '\r\n'` (or `--ors crlf`) silently produced LF line endings. Set UseCRLF from the writer options so CRLF output is honored. Default behavior (LF) is unchanged, and other ORS values are still rejected. The CSV-lite and TSV writers already honored CRLF ORS. Also: fix a copy-paste "for CSV" in the TSV writer's ORS-validation message, add unit tests asserting byte-exact line endings (the regtest harness normalizes CR/LF, so this can't be asserted in test/cases), add CLI-level regression cases, and update the separators documentation. This substantially addresses #1722 as well: RFC-4180-style CRLF output can now be requested on any platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix errcheck lint: check Flush() error in CSV writer test --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c6986f90e9
commit
ac08b072dc
27 changed files with 185 additions and 15 deletions
|
|
@ -840,6 +840,9 @@ This is simply a copy of what you should see on running `man mlr` at a command p
|
|||
* Default line endings (`--irs` and `--ors`) are newline
|
||||
which is interpreted to accept carriage-return/newline files (e.g. on Windows)
|
||||
for input, and to produce platform-appropriate line endings on output.
|
||||
* For CSV, CSV-lite, TSV, and TSV-lite output, ORS may be either newline (the
|
||||
default) or carriage-return/newline: e.g. `--ors crlf` or `--ors '\r\n'`
|
||||
for RFC-4180-style line endings on any platform.
|
||||
|
||||
Notes about all other separators:
|
||||
|
||||
|
|
@ -4045,5 +4048,5 @@ This is simply a copy of what you should see on running `man mlr` at a command p
|
|||
MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
|
||||
https://miller.readthedocs.io
|
||||
|
||||
2026-07-05 4mMILLER24m(1)
|
||||
2026-07-06 4mMILLER24m(1)
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -819,6 +819,9 @@
|
|||
* Default line endings (`--irs` and `--ors`) are newline
|
||||
which is interpreted to accept carriage-return/newline files (e.g. on Windows)
|
||||
for input, and to produce platform-appropriate line endings on output.
|
||||
* For CSV, CSV-lite, TSV, and TSV-lite output, ORS may be either newline (the
|
||||
default) or carriage-return/newline: e.g. `--ors crlf` or `--ors '\r\n'`
|
||||
for RFC-4180-style line endings on any platform.
|
||||
|
||||
Notes about all other separators:
|
||||
|
||||
|
|
@ -4024,4 +4027,4 @@
|
|||
MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
|
||||
https://miller.readthedocs.io
|
||||
|
||||
2026-07-05 4mMILLER24m(1)
|
||||
2026-07-06 4mMILLER24m(1)
|
||||
|
|
|
|||
|
|
@ -451,6 +451,9 @@ Notes about line endings:
|
|||
* Default line endings (`--irs` and `--ors`) are newline
|
||||
which is interpreted to accept carriage-return/newline files (e.g. on Windows)
|
||||
for input, and to produce platform-appropriate line endings on output.
|
||||
* For CSV, CSV-lite, TSV, and TSV-lite output, ORS may be either newline (the
|
||||
default) or carriage-return/newline: e.g. `--ors crlf` or `--ors '\r\n'`
|
||||
for RFC-4180-style line endings on any platform.
|
||||
|
||||
Notes about all other separators:
|
||||
|
||||
|
|
|
|||
|
|
@ -261,8 +261,8 @@ a:4;b:5;c:6;d:>>>,|||;<<<
|
|||
|
||||
Notes:
|
||||
|
||||
* CSV IRS and ORS must be newline, and CSV IFS must be a single character. (CSV-lite does not have these restrictions.)
|
||||
* TSV IRS and ORS must be newline, and TSV IFS must be a tab. (TSV-lite does not have these restrictions.)
|
||||
* CSV IRS must be newline -- CR/LF line endings are accepted on input. CSV ORS must be either newline (the default) or carriage-return/newline -- e.g. `--ors crlf` or `--ors '\r\n'` for RFC-4180-style line endings on any platform. CSV IFS must be a single character. (CSV-lite does not have these restrictions.)
|
||||
* TSV IRS must be newline -- CR/LF line endings are accepted on input. TSV ORS must be either newline (the default) or carriage-return/newline. TSV IFS must be a tab. (TSV-lite does not have these restrictions.)
|
||||
* See the [CSV section](file-formats.md#csvtsvasvusvetc) for information about ASV and USV.
|
||||
* JSON and YAML: ignore separator flags from the command line.
|
||||
* Headerless CSV overlaps quite a bit with NIDX format using comma for IFS. See also the page on [CSV with and without headers](csv-with-and-without-headers.md).
|
||||
|
|
@ -270,8 +270,8 @@ Notes:
|
|||
|
||||
| | **RS** | **FS** | **PS** |
|
||||
|------------|---------|---------|----------|
|
||||
| [**CSV**](file-formats.md#csvtsvasvusvetc) | Always `\n`; not alterable * | Default `,`; must be single-character | None |
|
||||
| [**TSV**](file-formats.md#csvtsvasvusvetc) | Always `\n`; not alterable * | Default `\t`; must be single-character | None |
|
||||
| [**CSV**](file-formats.md#csvtsvasvusvetc) | Default `\n`; may be set to `\r\n` * | Default `,`; must be single-character | None |
|
||||
| [**TSV**](file-formats.md#csvtsvasvusvetc) | Default `\n`; may be set to `\r\n` * | Default `\t`; must be single-character | None |
|
||||
| [**CSV-lite**](file-formats.md#csvtsvasvusvetc) | Default `\n` * | Default `,` | None |
|
||||
| [**TSV-lite**](file-formats.md#csvtsvasvusvetc) | Default `\n` * | Default `\t` | None |
|
||||
| [**JSON**](file-formats.md#json) | N/A; records are between `{` and `}` | Always `,`; not alterable | Always `:`; not alterable |
|
||||
|
|
|
|||
|
|
@ -151,8 +151,8 @@ GENMD-EOF
|
|||
|
||||
Notes:
|
||||
|
||||
* CSV IRS and ORS must be newline, and CSV IFS must be a single character. (CSV-lite does not have these restrictions.)
|
||||
* TSV IRS and ORS must be newline, and TSV IFS must be a tab. (TSV-lite does not have these restrictions.)
|
||||
* CSV IRS must be newline -- CR/LF line endings are accepted on input. CSV ORS must be either newline (the default) or carriage-return/newline -- e.g. `--ors crlf` or `--ors '\r\n'` for RFC-4180-style line endings on any platform. CSV IFS must be a single character. (CSV-lite does not have these restrictions.)
|
||||
* TSV IRS must be newline -- CR/LF line endings are accepted on input. TSV ORS must be either newline (the default) or carriage-return/newline. TSV IFS must be a tab. (TSV-lite does not have these restrictions.)
|
||||
* See the [CSV section](file-formats.md#csvtsvasvusvetc) for information about ASV and USV.
|
||||
* JSON and YAML: ignore separator flags from the command line.
|
||||
* Headerless CSV overlaps quite a bit with NIDX format using comma for IFS. See also the page on [CSV with and without headers](csv-with-and-without-headers.md).
|
||||
|
|
@ -160,8 +160,8 @@ Notes:
|
|||
|
||||
| | **RS** | **FS** | **PS** |
|
||||
|------------|---------|---------|----------|
|
||||
| [**CSV**](file-formats.md#csvtsvasvusvetc) | Always `\n`; not alterable * | Default `,`; must be single-character | None |
|
||||
| [**TSV**](file-formats.md#csvtsvasvusvetc) | Always `\n`; not alterable * | Default `\t`; must be single-character | None |
|
||||
| [**CSV**](file-formats.md#csvtsvasvusvetc) | Default `\n`; may be set to `\r\n` * | Default `,`; must be single-character | None |
|
||||
| [**TSV**](file-formats.md#csvtsvasvusvetc) | Default `\n`; may be set to `\r\n` * | Default `\t`; must be single-character | None |
|
||||
| [**CSV-lite**](file-formats.md#csvtsvasvusvetc) | Default `\n` * | Default `,` | None |
|
||||
| [**TSV-lite**](file-formats.md#csvtsvasvusvetc) | Default `\n` * | Default `\t` | None |
|
||||
| [**JSON**](file-formats.md#json) | N/A; records are between `{` and `}` | Always `,`; not alterable | Always `:`; not alterable |
|
||||
|
|
|
|||
|
|
@ -819,6 +819,9 @@
|
|||
* Default line endings (`--irs` and `--ors`) are newline
|
||||
which is interpreted to accept carriage-return/newline files (e.g. on Windows)
|
||||
for input, and to produce platform-appropriate line endings on output.
|
||||
* For CSV, CSV-lite, TSV, and TSV-lite output, ORS may be either newline (the
|
||||
default) or carriage-return/newline: e.g. `--ors crlf` or `--ors '\r\n'`
|
||||
for RFC-4180-style line endings on any platform.
|
||||
|
||||
Notes about all other separators:
|
||||
|
||||
|
|
@ -4024,4 +4027,4 @@
|
|||
MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
|
||||
https://miller.readthedocs.io
|
||||
|
||||
2026-07-05 4mMILLER24m(1)
|
||||
2026-07-06 4mMILLER24m(1)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
.\" Title: mlr
|
||||
.\" Author: [see the "AUTHOR" section]
|
||||
.\" Generator: ./mkman.rb
|
||||
.\" Date: 2026-07-05
|
||||
.\" Date: 2026-07-06
|
||||
.\" Manual: \ \&
|
||||
.\" Source: \ \&
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "MILLER" "1" "2026-07-05" "\ \&" "\ \&"
|
||||
.TH "MILLER" "1" "2026-07-06" "\ \&" "\ \&"
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * Portability definitions
|
||||
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -987,6 +987,9 @@ Notes about line endings:
|
|||
* Default line endings (`--irs` and `--ors`) are newline
|
||||
which is interpreted to accept carriage-return/newline files (e.g. on Windows)
|
||||
for input, and to produce platform-appropriate line endings on output.
|
||||
* For CSV, CSV-lite, TSV, and TSV-lite output, ORS may be either newline (the
|
||||
default) or carriage-return/newline: e.g. `--ors crlf` or `--ors '\er\en'`
|
||||
for RFC-4180-style line endings on any platform.
|
||||
|
||||
Notes about all other separators:
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ Notes about line endings:
|
|||
* Default line endings (` + "`--irs`" + ` and ` + "`--ors`" + `) are newline
|
||||
which is interpreted to accept carriage-return/newline files (e.g. on Windows)
|
||||
for input, and to produce platform-appropriate line endings on output.
|
||||
* For CSV, CSV-lite, TSV, and TSV-lite output, ORS may be either newline (the
|
||||
default) or carriage-return/newline: e.g. ` + "`--ors crlf`" + ` or ` + "`--ors '\\r\\n'`" + `
|
||||
for RFC-4180-style line endings on any platform.
|
||||
|
||||
Notes about all other separators:
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func NewRecordWriterCSV(writerOptions *cli.TWriterOptions) (*RecordWriterCSV, er
|
|||
return nil, fmt.Errorf("for CSV, OFS can only be a single character")
|
||||
}
|
||||
if writerOptions.ORS != "\n" && writerOptions.ORS != "\r\n" {
|
||||
return nil, fmt.Errorf("for CSV, ORS cannot be altered")
|
||||
return nil, fmt.Errorf("for CSV, ORS must be newline or carriage-return/newline")
|
||||
}
|
||||
writer := &RecordWriterCSV{
|
||||
writerOptions: writerOptions,
|
||||
|
|
@ -59,6 +59,9 @@ func (writer *RecordWriterCSV) Write(
|
|||
if writer.csvWriter == nil {
|
||||
writer.csvWriter = csv.NewWriter(bufferedOutputStream)
|
||||
writer.csvWriter.Comma = rune(writer.writerOptions.OFS[0]) // xxx temp -- needs length-1 OFS
|
||||
// Issues #1810 and #1722: honor `--ors '\r\n'` (or `--ors crlf`) for
|
||||
// CSV output. The default remains "\n".
|
||||
writer.csvWriter.UseCRLF = writer.writerOptions.ORS == "\r\n"
|
||||
}
|
||||
|
||||
if writer.firstRecordKeys == nil {
|
||||
|
|
|
|||
99
pkg/output/record_writer_csv_test.go
Normal file
99
pkg/output/record_writer_csv_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Tests for https://github.com/johnkerl/miller/issues/1810 and
|
||||
// https://github.com/johnkerl/miller/issues/1722: `--ors '\r\n'` (or `--ors
|
||||
// crlf`) should be honored for CSV/CSV-lite/TSV output. These are unit tests
|
||||
// rather than regression-test cases since the regression-test harness
|
||||
// normalizes CR/LF to LF before comparing outputs, and so cannot assert
|
||||
// byte-exact line endings.
|
||||
|
||||
package output
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/johnkerl/miller/v6/pkg/cli"
|
||||
"github.com/johnkerl/miller/v6/pkg/mlrval"
|
||||
)
|
||||
|
||||
func makeTestRecord() *mlrval.Mlrmap {
|
||||
record := mlrval.NewMlrmap()
|
||||
record.PutReference("a", mlrval.FromString("1"))
|
||||
record.PutReference("b", mlrval.FromString("2"))
|
||||
return record
|
||||
}
|
||||
|
||||
// runWriter drives a record-writer with a single a=1,b=2 record and returns
|
||||
// the output bytes.
|
||||
func runWriter(t *testing.T, writer IRecordWriter) string {
|
||||
t.Helper()
|
||||
var buffer bytes.Buffer
|
||||
bufferedOutputStream := bufio.NewWriter(&buffer)
|
||||
err := writer.Write(makeTestRecord(), nil, bufferedOutputStream, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = writer.Write(nil, nil, bufferedOutputStream, false) // end of stream
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := bufferedOutputStream.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func TestCSVWriterLFDefault(t *testing.T) {
|
||||
writer, err := NewRecordWriterCSV(&cli.TWriterOptions{OFS: ",", ORS: "\n"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := runWriter(t, writer)
|
||||
expected := "a,b\n1,2\n"
|
||||
if output != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVWriterCRLF(t *testing.T) {
|
||||
writer, err := NewRecordWriterCSV(&cli.TWriterOptions{OFS: ",", ORS: "\r\n"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := runWriter(t, writer)
|
||||
expected := "a,b\r\n1,2\r\n"
|
||||
if output != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVWriterRejectsOtherORS(t *testing.T) {
|
||||
_, err := NewRecordWriterCSV(&cli.TWriterOptions{OFS: ",", ORS: ";"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for ORS \";\" but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVLiteWriterCRLF(t *testing.T) {
|
||||
writer, err := NewRecordWriterCSVLite(&cli.TWriterOptions{OFS: ",", ORS: "\r\n"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := runWriter(t, writer)
|
||||
expected := "a,b\r\n1,2\r\n"
|
||||
if output != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTSVWriterCRLF(t *testing.T) {
|
||||
writer, err := NewRecordWriterTSV(&cli.TWriterOptions{OFS: "\t", ORS: "\r\n"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := runWriter(t, writer)
|
||||
expected := "a\tb\r\n1\t2\r\n"
|
||||
if output != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, output)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ func NewRecordWriterTSV(writerOptions *cli.TWriterOptions) (*RecordWriterTSV, er
|
|||
return nil, fmt.Errorf("for TSV, OFS cannot be altered")
|
||||
}
|
||||
if writerOptions.ORS != "\n" && writerOptions.ORS != "\r\n" {
|
||||
return nil, fmt.Errorf("for CSV, ORS cannot be altered")
|
||||
return nil, fmt.Errorf("for TSV, ORS must be newline or carriage-return/newline")
|
||||
}
|
||||
return &RecordWriterTSV{
|
||||
writerOptions: writerOptions,
|
||||
|
|
|
|||
1
test/cases/io-csv-ors-crlf/0001/cmd
Normal file
1
test/cases/io-csv-ors-crlf/0001/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr --icsv --ocsv --ors crlf cat test/input/line-term-lf.csv
|
||||
0
test/cases/io-csv-ors-crlf/0001/experr
Normal file
0
test/cases/io-csv-ors-crlf/0001/experr
Normal file
11
test/cases/io-csv-ors-crlf/0001/expout
Normal file
11
test/cases/io-csv-ors-crlf/0001/expout
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
a,b,i,x,y
|
||||
pan,pan,1,0.34679014,0.72680286
|
||||
eks,pan,2,0.75867996,0.52215111
|
||||
wye,wye,3,0.20460331,0.33831853
|
||||
eks,wye,4,0.38139939,0.13418874
|
||||
wye,pan,5,0.57328892,0.86362447
|
||||
zee,pan,6,0.52712616,0.49322129
|
||||
eks,zee,7,0.61178406,0.18788492
|
||||
zee,wye,8,0.59855401,0.97618139
|
||||
hat,wye,9,0.03144188,0.74955076
|
||||
pan,wye,10,0.50262601,0.95261836
|
||||
1
test/cases/io-csv-ors-crlf/0002/cmd
Normal file
1
test/cases/io-csv-ors-crlf/0002/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr --icsv --ocsv --ors crlf cat test/input/line-term-crlf.csv
|
||||
0
test/cases/io-csv-ors-crlf/0002/experr
Normal file
0
test/cases/io-csv-ors-crlf/0002/experr
Normal file
11
test/cases/io-csv-ors-crlf/0002/expout
Normal file
11
test/cases/io-csv-ors-crlf/0002/expout
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
a,b,i,x,y
|
||||
pan,pan,1,0.34679014,0.72680286
|
||||
eks,pan,2,0.75867996,0.52215111
|
||||
wye,wye,3,0.20460331,0.33831853
|
||||
eks,wye,4,0.38139939,0.13418874
|
||||
wye,pan,5,0.57328892,0.86362447
|
||||
zee,pan,6,0.52712616,0.49322129
|
||||
eks,zee,7,0.61178406,0.18788492
|
||||
zee,wye,8,0.59855401,0.97618139
|
||||
hat,wye,9,0.03144188,0.74955076
|
||||
pan,wye,10,0.50262601,0.95261836
|
||||
1
test/cases/io-csv-ors-crlf/0003/cmd
Normal file
1
test/cases/io-csv-ors-crlf/0003/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr --icsv --ocsv --ors lf cat test/input/line-term-crlf.csv
|
||||
0
test/cases/io-csv-ors-crlf/0003/experr
Normal file
0
test/cases/io-csv-ors-crlf/0003/experr
Normal file
11
test/cases/io-csv-ors-crlf/0003/expout
Normal file
11
test/cases/io-csv-ors-crlf/0003/expout
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
a,b,i,x,y
|
||||
pan,pan,1,0.34679014,0.72680286
|
||||
eks,pan,2,0.75867996,0.52215111
|
||||
wye,wye,3,0.20460331,0.33831853
|
||||
eks,wye,4,0.38139939,0.13418874
|
||||
wye,pan,5,0.57328892,0.86362447
|
||||
zee,pan,6,0.52712616,0.49322129
|
||||
eks,zee,7,0.61178406,0.18788492
|
||||
zee,wye,8,0.59855401,0.97618139
|
||||
hat,wye,9,0.03144188,0.74955076
|
||||
pan,wye,10,0.50262601,0.95261836
|
||||
1
test/cases/io-csv-ors-crlf/0004/cmd
Normal file
1
test/cases/io-csv-ors-crlf/0004/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr --icsv --otsv --ors crlf cat test/input/line-term-lf.csv
|
||||
0
test/cases/io-csv-ors-crlf/0004/experr
Normal file
0
test/cases/io-csv-ors-crlf/0004/experr
Normal file
11
test/cases/io-csv-ors-crlf/0004/expout
Normal file
11
test/cases/io-csv-ors-crlf/0004/expout
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
a b i x y
|
||||
pan pan 1 0.34679014 0.72680286
|
||||
eks pan 2 0.75867996 0.52215111
|
||||
wye wye 3 0.20460331 0.33831853
|
||||
eks wye 4 0.38139939 0.13418874
|
||||
wye pan 5 0.57328892 0.86362447
|
||||
zee pan 6 0.52712616 0.49322129
|
||||
eks zee 7 0.61178406 0.18788492
|
||||
zee wye 8 0.59855401 0.97618139
|
||||
hat wye 9 0.03144188 0.74955076
|
||||
pan wye 10 0.50262601 0.95261836
|
||||
1
test/cases/io-csv-ors-crlf/0005/cmd
Normal file
1
test/cases/io-csv-ors-crlf/0005/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr --icsv --ocsv --ors semicolon cat test/input/line-term-lf.csv
|
||||
1
test/cases/io-csv-ors-crlf/0005/experr
Normal file
1
test/cases/io-csv-ors-crlf/0005/experr
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr: for CSV, ORS must be newline or carriage-return/newline
|
||||
0
test/cases/io-csv-ors-crlf/0005/expout
Normal file
0
test/cases/io-csv-ors-crlf/0005/expout
Normal file
0
test/cases/io-csv-ors-crlf/0005/should-fail
Normal file
0
test/cases/io-csv-ors-crlf/0005/should-fail
Normal file
Loading…
Add table
Add a link
Reference in a new issue