Repository: saulpw/visidata

Issue:
[tsv] add CLI option to use NUL as delimiter
It's useful to parse output from GNU grep's `-Z` option. That produces lines that in Python are `f'{filename}\0{line}\n'`, instead of the usual `f'{filename}:{line}\n'`.

Right now the command line can't be used to specify a NUL delimiter, as in `vd --delimiter="\0"`, because `sys.argv` strings are NUL-terminated and can't ever contain NUL.

My workarounds for now are to use .visidatarc, either add a temporary line:
`vd.option('delimiter', '\x00', 'field delimiter to use for tsv/usv filetype', replay=True)`.
or add a new filetype to allow `vd -f nsv`:
```
@VisiData.api
def open_nsv(vd, p):
    tsv = TsvSheet(p.base_stem, source=p)
    tsv.delimiter = '\x00'
    tsv.reload()
    return tsv
```
Can `open_nsv()` be written without `reload()` right now? I couldn't think of another way to set `delimiter` for TsvSheet.


Discussion:
The way to do this is:

```
@VisiData.api
def open_nsv(vd, p):
    return NsvSheet(p.base_stem, source=p)

class NsvSheet(TsvSheet):
    pass

NsvSheet.options.delimiter = '\x00'
```

Also is it literally not possible to pass a NUL character into Python from the CLI?  Not even with `$'\0'`?  That seems pretty egregious.  Maybe we could make an empty separator mean NUL.
Ah right, thanks.

Yes, it is impossible for the shell to execute a program with arguments that contain a NUL character, as the argv in the`exec*()` system calls (in C) uses NUL to terminate its strings.
Ah, of course. Well, I'd take a PR to make `options.delimiter=''` mean NUL-delimited, if you're up for it.  We'll want to update the (new) docs at [visidata/features/xsv_guide.py](https://github.com/saulpw/visidata/blob/develop/visidata/features/xsv_guide.py) too.  

Resolving patch:
diff --git a/visidata/features/xsv_guide.py b/visidata/features/xsv_guide.py
--- a/visidata/features/xsv_guide.py
+++ b/visidata/features/xsv_guide.py
@@ -13,6 +13,8 @@ class XsvGuide(GuideSheet):
 
     Use `-f usv` for Unicode separators U+241F and U+241E.
     Use `-f lsv` for awk-like records.
+    Use `--delimiter=` (an empty string) to make '\0' the value separator.
+    Use `--row-delimiter=` to make '\0' the row separator.
 
     ## `csv` (Comma Separated Values) for maximum computibility
 
diff --git a/visidata/loaders/tsv.py b/visidata/loaders/tsv.py
--- a/visidata/loaders/tsv.py
+++ b/visidata/loaders/tsv.py
@@ -75,6 +75,14 @@ class TsvSheet(SequenceSheet):
     def iterload(self):
         delim = self.delimiter or self.options.delimiter
         rowdelim = self.row_delimiter or self.options.row_delimiter
+        if delim == '':
+            vd.warning("using '\\x00' as field delimiter")
+            delim = '\x00'  #2272
+        if rowdelim == '':
+            vd.warning("using '\\x00' as row delimiter")
+            rowdelim = '\x00'
+        if delim == rowdelim:
+            vd.fail('field delimiter and row delimiter cannot be the same')
 
         with self.open_text_source() as fp:
                 for line in splitter(adaptive_bufferer(fp), rowdelim):
@@ -95,6 +103,14 @@ def save_tsv(vd, p, vs, delimiter='', row_delimiter=''):
     'Write sheet to file `fn` as TSV.'
     unitsep = delimiter or vs.options.delimiter
     rowsep = row_delimiter or vs.options.row_delimiter
+    if unitsep == '':
+        vd.warning("saving with '\\x00' as field delimiter")
+        unitsep = '\x00'
+    if rowsep == '':
+        vd.warning("saving with '\\x00' as row delimiter")
+        rowsep = '\x00'
+    if unitsep == rowsep:
+        vd.fail('field delimiter and row delimiter cannot be the same')
     trdict = vs.safe_trdict()
 
     with p.open(mode='w', encoding=vs.options.save_encoding) as fp:


Test patch: