Repository: saulpw/visidata

Issue:
Inconsistent CRLF behaviour between Windows and Linux
**Small description**

If I open a CSV with `LF` line endings in VisiData on Windows, make a change, then save the file, I get `CRCRLF` line endings.
Note that this is not just `CRLF` line endings, which is what you get on Linux (see #1076). Note that there are somehow two `CR` characters before the `LF`!

**Expected result**

The line endings should either be `LF` (preserve original) or `CRLF` (Python and VisiData default for CSV writer on Linux).
The line endings should not be `CRCRLF`...

**Actual result with screenshot**
If you get an unexpected error, please include the full stack trace that you get with `Ctrl-E`.

**Steps to reproduce with sample data and a .vd**

**Original file** (note the LF (`0a`) line ending:
[lf.csv](https://github.com/saulpw/visidata/files/8599982/lf.csv)

```
$ xxd lf.csv
00000000: 612c 620a 312c 320a                      a,b.1,2.
```

**New file** (note the CRCRLF (`0d 0d 0a`) line ending!)
[lf.csv](https://github.com/saulpw/visidata/files/8599988/lf.csv)

```
xxd lf.csv
00000000: 612c 620d 0d0a 332c 340d 0d0a            a,b...3,4...
```

**CommandLog (zipped)**:
[lf.zip](https://github.com/saulpw/visidata/files/8599990/lf.zip)


**Additional context**
Please include the version of VisiData: saul.pw/VisiData v2.7.1



Discussion:
Thanks for the report, @daviewales. You're right, this is incorrect. Ideally it would detect the csv options and set them on the source sheet so it would save in an identical format (though derived sheets wouldn't inherit this format, which may or may not be desired).
The strange thing is that on Linux, it does: `LF` -> `CRLF`. But on Windows we get `LF` -> `CRCRLF`. I haven't checked the source, but there's something strange going on there... (I know that VisiData doesn't exactly support Windows, but it's strange all the same.)
Interesting.  This is due to `options.csv_lineterminator` being set to `'\r\n'` by default, and on windows `\n` is CRLF.  So you can set this option manually to `\x0a` to get LF-only line-endings.  I guess the default should be explicit bytes as well.

Resolving patch:
diff --git a/visidata/loaders/csv.py b/visidata/loaders/csv.py
--- a/visidata/loaders/csv.py
+++ b/visidata/loaders/csv.py
@@ -47,7 +47,7 @@ def iterload(self):
 @VisiData.api
 def save_csv(vd, p, sheet):
     'Save as single CSV file, handling column names as first line.'
-    with p.open_text(mode='w', encoding=sheet.options.encoding) as fp:
+    with p.open_text(mode='w', encoding=sheet.options.encoding, newline='') as fp:
         cw = csv.writer(fp, **options.getall('csv_'))
         colnames = [col.name for col in sheet.visibleCols]
         if ''.join(colnames):
diff --git a/visidata/path.py b/visidata/path.py
--- a/visidata/path.py
+++ b/visidata/path.py
@@ -159,7 +159,7 @@ def __lt__(self, a):
     def __truediv__(self, a):
         return Path(self._path.__truediv__(a))
 
-    def open_text(self, mode='rt', encoding=None):
+    def open_text(self, mode='rt', encoding=None, newline=None):
         'Open path in text mode, using options.encoding and options.encoding_errors.  Return open file-pointer or file-pointer-like.'
         # rfile makes a single-access fp reusable
 
@@ -183,7 +183,7 @@ def open_text(self, mode='rt', encoding=None):
                 vd.error('invalid mode "%s" for Path.open_text()' % mode)
                 return sys.stderr
 
-        return self.open(mode=mode, encoding=encoding or vd.options.encoding, errors=vd.options.encoding_errors)
+        return self.open(mode=mode, encoding=encoding or vd.options.encoding, errors=vd.options.encoding_errors, newline=newline)
 
     @wraps(pathlib.Path.read_text)
     def read_text(self, *args, **kwargs):


Test patch: