We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
save as csv actually saves tsv when file existing file extension is CSV, i.e.  uppercase - PR available #2269
**Small description**

Slightly hperbolic ;-) Corruption of file format, opening a csv but saving as csv results in TSV data in a file named csv

**Expected result**

CSV, not TSV

**Actual result with screenshot**

If you get an unexpected error, please include the full stack trace that you get with `Ctrl-E`.
No error, contents:

    header1	header2
    1	one
    2	two


**Steps to reproduce with sample data and a .vd**

Datafile, called bug.CSV

    header1,header2
    1,one
    2,two


1. open data file, MUST have uppercase CSV on end (works fine for lower). E.g., `visidata bug.CSV`
2. save (ctrl-s)
3. hit enter to accept current filename
4. hit `y` to overwrite
5. Display will say saving TSV
6. sanity check file contents

First try reproducing without any user configuration by using the flag `-N`.
e.g. `echo "abc" | vd -f txt -N`

Please attach the commandlog (saved with `Ctrl-D`) to show the steps that led to the issue.
See [here](http://visidata.org/docs/save-restore/) for more details.

**Additional context**
Please include the version of VisiData and Python.

Windows:

    (py311csv) C:\code\py>python
    Python 3.11.3 (tags/v3.11.3:f3909b8, Apr  4 2023, 23:49:59) [MSC v.1934 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license" for more information.
    >>> ^Z


    (py311csv) C:\code\py>visidata --version
    saul.pw/VisiData v3.0.2



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `visidata/save.py`
Content:
```
1 import collections
2 import os
3 from copy import copy
4 
5 from visidata import vd
6 from visidata import Sheet, BaseSheet, VisiData, IndexSheet, Path, Progress, TypedExceptionWrapper
7 
8 vd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)
9 vd.option('save_encoding', 'utf-8', 'encoding passed to codecs.open when saving a file', replay=True, help=vd.help_encoding)
10 
11 @Sheet.api
12 def safe_trdict(vs):
13     'returns string.translate dictionary for replacing tabs and newlines'
14     if vs.options.safety_first:
15         delim = vs.options.delimiter
16         return {
17              0: '', #  strip NUL completely
18     ord(delim): vs.options.tsv_safe_tab, # \t
19             10: vs.options.tsv_safe_newline,  # \n
20             13: vs.options.tsv_safe_newline,  # \r
21         }
22     return {}
23 
24 
25 @Sheet.api
26 def iterdispvals(sheet, *cols, format=False):
27     'For each row in sheet, yield OrderedDict of values for given cols.  Values are typed if format=False, or a formatted display string if format=True.'
28     if not cols:
29         cols = sheet.visibleCols
30 
31     transformers = collections.OrderedDict()  # list of transformers for each column in order
32     for col in cols:
33         transformers[col] = [ col.type ]
34         if format:
35             formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))
36             transformers[col].append(formatMaker(col._formatdict))
37         trdict = sheet.safe_trdict()
38         if trdict:
39             transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))
40 
41     options_safe_error = sheet.options.safe_error
42     for r in Progress(sheet.rows):
43         dispvals = collections.OrderedDict()  # [col] -> value
44         for col, transforms in transformers.items():
45             try:
46                 dispval = col.getValue(r)
47 
48             except Exception as e:
49                 vd.exceptionCaught(e)
50                 dispval = options_safe_error or str(e)
51 
52             try:
53                 for t in transforms:
54                     if dispval is None:
55                         break
56                     elif isinstance(dispval, TypedExceptionWrapper):
57                         dispval = options_safe_error or str(dispval)
58                         break
59                     else:
60                         dispval = t(dispval)
61 
62                 if dispval is None and format:
63                     dispval = ''
64             except Exception as e:
65                 dispval = str(dispval)
66 
67             dispvals[col] = dispval
68 
69         yield dispvals
70 
71 
72 @Sheet.api
73 def itervals(sheet, *cols, format=False):
74     for row in sheet.iterdispvals(*cols, format=format):
75         yield [row[c] for c in cols]
76 
77 @BaseSheet.api
78 def getDefaultSaveName(sheet):
79     src = getattr(sheet, 'source', None)
80     if hasattr(src, 'scheme') and src.scheme:
81         return src.name + src.suffix
82     if isinstance(src, Path):
83         if sheet.options.is_set('save_filetype', sheet):
84             # if save_filetype is over-ridden from default, use it as the extension
85             return str(src.with_suffix('')) + '.' + sheet.options.save_filetype
86         return str(src)
87     else:
88         return sheet.name+'.'+getattr(sheet, 'filetype', sheet.options.save_filetype)
89 
90 
91 @VisiData.api
92 def save_cols(vd, cols):
93     sheet = cols[0].sheet
94     vs = copy(sheet)
95     vs.columns = list(cols)
96     vs.rows = sheet.rows
97     if len(cols) == 1:
98         savedcoltxt = cols[0].name + ' column'
99     else:
100         savedcoltxt = '%s columns' % len(cols)
101     path = vd.inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName())
102     vd.saveSheets(path, vs)
103 
104 
105 @VisiData.api
106 def saveSheets(vd, givenpath, *vsheets, confirm_overwrite=True):
107     'Save all *vsheets* to *givenpath*.'
108 
109     if not vsheets: # blank tuple
110         vd.warning('no sheets to save')
111         return
112 
113     filetypes = [givenpath.ext, vd.options.save_filetype]
114 
115     vd.clearCaches()
116 
117     for ft in filetypes:
118         savefunc = getattr(vsheets[0], 'save_' + ft, None) or getattr(vd, 'save_' + ft, None)
119         if savefunc:
120             filetype = ft
121             break
122 
123     if savefunc is None:
124         vd.fail(f'no function to save as {filetype}')
125 
126     if confirm_overwrite:
127         vd.confirmOverwrite(givenpath)
128 
129     vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))
130 
131     if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /
132         for vs in vsheets:
133             vs.hasBeenModified = False
134         # savefuncs(vd, p, *vsheets) will have 2 argcount (*vsheets does not get counted as an arg)
135         # savefuncs(vd, p, vs) will have 3 argcount (vs counts as an arg, along with vd, path)
136         if savefunc.__code__.co_argcount == 3 and len(vsheets) > 1:
137             vd.fail(f'cannot save multiple {filetype} sheets to non-dir')
138         return vd.execAsync(savefunc, givenpath, *vsheets)
139 
140     # path is a dir
141 
142     # save as individual files in the givenpath directory
143     try:
144         os.makedirs(givenpath, exist_ok=True)
145     except FileExistsError:
146         pass
147 
148     if not givenpath.is_dir():
149         vd.fail(f'cannot save multiple {filetype} sheets to non-dir')
150 
151     def _savefiles(vsheets, givenpath, savefunc, filetype):
152         for vs in vsheets:
153             p = Path((givenpath / vs.name).with_suffix('.'+filetype))
154             savefunc(p, vs)
155             vs.hasBeenModified = False
156 
157         vd.status(f'{givenpath} save finished')  #2157
158 
159     return vd.execAsync(_savefiles, vsheets, givenpath, savefunc, filetype)
160 
161 
162 @VisiData.api
163 def save_zip(vd, p, *vsheets):
164     vd.clearCaches()
165 
166     import tempfile
167     import zipfile
168     with tempfile.TemporaryDirectory() as tmpdir:
169         with zipfile.ZipFile(str(p), 'w', zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=9) as zfp:
170             for vs in Progress(vsheets):
171                 filetype = vs.options.save_filetype
172                 tmpp = Path(f'{tmpdir}{vs.name}.{filetype}')
173                 savefunc = getattr(vs, 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)
174                 savefunc(tmpp, vs)
175                 zfp.write(tmpp, f'{vs.name}.{vs.options.save_filetype}')
176 
177 
178 @VisiData.api
179 def save_txt(vd, p, *vsheets):
180     if len(vsheets) == 1 and vsheets[0].nVisibleCols > 1:  #2173
181         return vd.save_tsv(p, vsheets[0])
182 
183     with p.open(mode='w', encoding=vsheets[0].options.save_encoding) as fp:
184         for vs in vsheets:
185             unitsep = vs.options.delimiter
186             rowsep = vs.options.row_delimiter
187             for dispvals in vs.iterdispvals(*vs.visibleCols, format=True):
188                 fp.write(unitsep.join(dispvals.values()))
189                 fp.write(rowsep)
190 
191 
192 @BaseSheet.api
193 def rootSheet(sheet):
194     r = sheet
195     while isinstance(r.source, BaseSheet):
196         r = r.source
197 
198     return r
199 
200 
201 BaseSheet.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath("save to: ", value=getDefaultSaveName()), sheet)', 'save current sheet to filename in format determined by extension (default .tsv)')
202 BaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSaveName()), sheet, confirm_overwrite=False)', 'save current sheet without asking for filename or confirmation')
203 BaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet())', 'save root sheet to its source')
204 BaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath("save all sheets to: "), *vd.stackedSheets)', 'save all sheets to given file or directory)')
205 IndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath("save %d sheets to: " % nSelectedRows, value="_".join(getattr(vs, "name", None) or "blank" for vs in selectedRows)), *selectedRows)', 'save all selected sheets to given file or directory')
206 Sheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')
207 Sheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')
208 
209 vd.addMenuItems('''
210     File > Save > current sheet > save-sheet
211     File > Save > all sheets > save-all
212     File > Save > current column > save-col
213     File > Save > keys and current column > save-col-keys
214 ''')
215
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/visidata/save.py b/visidata/save.py
--- a/visidata/save.py
+++ b/visidata/save.py
@@ -110,7 +110,7 @@
         vd.warning('no sheets to save')
         return
 
-    filetypes = [givenpath.ext, vd.options.save_filetype]
+    filetypes = [givenpath.ext.lower(), vd.options.save_filetype.lower()]
 
     vd.clearCaches()

{"golden_diff": "diff --git a/visidata/save.py b/visidata/save.py\n--- a/visidata/save.py\n+++ b/visidata/save.py\n@@ -110,7 +110,7 @@\n         vd.warning('no sheets to save')\n         return\n \n-    filetypes = [givenpath.ext, vd.options.save_filetype]\n+    filetypes = [givenpath.ext.lower(), vd.options.save_filetype.lower()]\n \n     vd.clearCaches()\n", "issue": "save as csv actually saves tsv when file existing file extension is CSV, i.e.  uppercase - PR available #2269\n**Small description**\r\n\r\nSlightly hperbolic ;-) Corruption of file format, opening a csv but saving as csv results in TSV data in a file named csv\r\n\r\n**Expected result**\r\n\r\nCSV, not TSV\r\n\r\n**Actual result with screenshot**\r\n\r\nIf you get an unexpected error, please include the full stack trace that you get with `Ctrl-E`.\r\nNo error, contents:\r\n\r\n    header1\theader2\r\n    1\tone\r\n    2\ttwo\r\n\r\n\r\n**Steps to reproduce with sample data and a .vd**\r\n\r\nDatafile, called bug.CSV\r\n\r\n    header1,header2\r\n    1,one\r\n    2,two\r\n\r\n\r\n1. open data file, MUST have uppercase CSV on end (works fine for lower). E.g., `visidata bug.CSV`\r\n2. save (ctrl-s)\r\n3. hit enter to accept current filename\r\n4. hit `y` to overwrite\r\n5. Display will say saving TSV\r\n6. sanity check file contents\r\n\r\nFirst try reproducing without any user configuration by using the flag `-N`.\r\ne.g. `echo \"abc\" | vd -f txt -N`\r\n\r\nPlease attach the commandlog (saved with `Ctrl-D`) to show the steps that led to the issue.\r\nSee [here](http://visidata.org/docs/save-restore/) for more details.\r\n\r\n**Additional context**\r\nPlease include the version of VisiData and Python.\r\n\r\nWindows:\r\n\r\n    (py311csv) C:\\code\\py>python\r\n    Python 3.11.3 (tags/v3.11.3:f3909b8, Apr  4 2023, 23:49:59) [MSC v.1934 64 bit (AMD64)] on win32\r\n    Type \"help\", \"copyright\", \"credits\" or \"license\" for more information.\r\n    >>> ^Z\r\n\r\n\r\n    (py311csv) C:\\code\\py>visidata --version\r\n    saul.pw/VisiData v3.0.2\r\n\r\n\n", "before_files": [{"content": "import collections\nimport os\nfrom copy import copy\n\nfrom visidata import vd\nfrom visidata import Sheet, BaseSheet, VisiData, IndexSheet, Path, Progress, TypedExceptionWrapper\n\nvd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)\nvd.option('save_encoding', 'utf-8', 'encoding passed to codecs.open when saving a file', replay=True, help=vd.help_encoding)\n\n@Sheet.api\ndef safe_trdict(vs):\n    'returns string.translate dictionary for replacing tabs and newlines'\n    if vs.options.safety_first:\n        delim = vs.options.delimiter\n        return {\n             0: '', #  strip NUL completely\n    ord(delim): vs.options.tsv_safe_tab, # \\t\n            10: vs.options.tsv_safe_newline,  # \\n\n            13: vs.options.tsv_safe_newline,  # \\r\n        }\n    return {}\n\n\n@Sheet.api\ndef iterdispvals(sheet, *cols, format=False):\n    'For each row in sheet, yield OrderedDict of values for given cols.  Values are typed if format=False, or a formatted display string if format=True.'\n    if not cols:\n        cols = sheet.visibleCols\n\n    transformers = collections.OrderedDict()  # list of transformers for each column in order\n    for col in cols:\n        transformers[col] = [ col.type ]\n        if format:\n            formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))\n            transformers[col].append(formatMaker(col._formatdict))\n        trdict = sheet.safe_trdict()\n        if trdict:\n            transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))\n\n    options_safe_error = sheet.options.safe_error\n    for r in Progress(sheet.rows):\n        dispvals = collections.OrderedDict()  # [col] -> value\n        for col, transforms in transformers.items():\n            try:\n                dispval = col.getValue(r)\n\n            except Exception as e:\n                vd.exceptionCaught(e)\n                dispval = options_safe_error or str(e)\n\n            try:\n                for t in transforms:\n                    if dispval is None:\n                        break\n                    elif isinstance(dispval, TypedExceptionWrapper):\n                        dispval = options_safe_error or str(dispval)\n                        break\n                    else:\n                        dispval = t(dispval)\n\n                if dispval is None and format:\n                    dispval = ''\n            except Exception as e:\n                dispval = str(dispval)\n\n            dispvals[col] = dispval\n\n        yield dispvals\n\n\n@Sheet.api\ndef itervals(sheet, *cols, format=False):\n    for row in sheet.iterdispvals(*cols, format=format):\n        yield [row[c] for c in cols]\n\n@BaseSheet.api\ndef getDefaultSaveName(sheet):\n    src = getattr(sheet, 'source', None)\n    if hasattr(src, 'scheme') and src.scheme:\n        return src.name + src.suffix\n    if isinstance(src, Path):\n        if sheet.options.is_set('save_filetype', sheet):\n            # if save_filetype is over-ridden from default, use it as the extension\n            return str(src.with_suffix('')) + '.' + sheet.options.save_filetype\n        return str(src)\n    else:\n        return sheet.name+'.'+getattr(sheet, 'filetype', sheet.options.save_filetype)\n\n\n@VisiData.api\ndef save_cols(vd, cols):\n    sheet = cols[0].sheet\n    vs = copy(sheet)\n    vs.columns = list(cols)\n    vs.rows = sheet.rows\n    if len(cols) == 1:\n        savedcoltxt = cols[0].name + ' column'\n    else:\n        savedcoltxt = '%s columns' % len(cols)\n    path = vd.inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName())\n    vd.saveSheets(path, vs)\n\n\n@VisiData.api\ndef saveSheets(vd, givenpath, *vsheets, confirm_overwrite=True):\n    'Save all *vsheets* to *givenpath*.'\n\n    if not vsheets: # blank tuple\n        vd.warning('no sheets to save')\n        return\n\n    filetypes = [givenpath.ext, vd.options.save_filetype]\n\n    vd.clearCaches()\n\n    for ft in filetypes:\n        savefunc = getattr(vsheets[0], 'save_' + ft, None) or getattr(vd, 'save_' + ft, None)\n        if savefunc:\n            filetype = ft\n            break\n\n    if savefunc is None:\n        vd.fail(f'no function to save as {filetype}')\n\n    if confirm_overwrite:\n        vd.confirmOverwrite(givenpath)\n\n    vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))\n\n    if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /\n        for vs in vsheets:\n            vs.hasBeenModified = False\n        # savefuncs(vd, p, *vsheets) will have 2 argcount (*vsheets does not get counted as an arg)\n        # savefuncs(vd, p, vs) will have 3 argcount (vs counts as an arg, along with vd, path)\n        if savefunc.__code__.co_argcount == 3 and len(vsheets) > 1:\n            vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n        return vd.execAsync(savefunc, givenpath, *vsheets)\n\n    # path is a dir\n\n    # save as individual files in the givenpath directory\n    try:\n        os.makedirs(givenpath, exist_ok=True)\n    except FileExistsError:\n        pass\n\n    if not givenpath.is_dir():\n        vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n\n    def _savefiles(vsheets, givenpath, savefunc, filetype):\n        for vs in vsheets:\n            p = Path((givenpath / vs.name).with_suffix('.'+filetype))\n            savefunc(p, vs)\n            vs.hasBeenModified = False\n\n        vd.status(f'{givenpath} save finished')  #2157\n\n    return vd.execAsync(_savefiles, vsheets, givenpath, savefunc, filetype)\n\n\n@VisiData.api\ndef save_zip(vd, p, *vsheets):\n    vd.clearCaches()\n\n    import tempfile\n    import zipfile\n    with tempfile.TemporaryDirectory() as tmpdir:\n        with zipfile.ZipFile(str(p), 'w', zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=9) as zfp:\n            for vs in Progress(vsheets):\n                filetype = vs.options.save_filetype\n                tmpp = Path(f'{tmpdir}{vs.name}.{filetype}')\n                savefunc = getattr(vs, 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)\n                savefunc(tmpp, vs)\n                zfp.write(tmpp, f'{vs.name}.{vs.options.save_filetype}')\n\n\n@VisiData.api\ndef save_txt(vd, p, *vsheets):\n    if len(vsheets) == 1 and vsheets[0].nVisibleCols > 1:  #2173\n        return vd.save_tsv(p, vsheets[0])\n\n    with p.open(mode='w', encoding=vsheets[0].options.save_encoding) as fp:\n        for vs in vsheets:\n            unitsep = vs.options.delimiter\n            rowsep = vs.options.row_delimiter\n            for dispvals in vs.iterdispvals(*vs.visibleCols, format=True):\n                fp.write(unitsep.join(dispvals.values()))\n                fp.write(rowsep)\n\n\n@BaseSheet.api\ndef rootSheet(sheet):\n    r = sheet\n    while isinstance(r.source, BaseSheet):\n        r = r.source\n\n    return r\n\n\nBaseSheet.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath(\"save to: \", value=getDefaultSaveName()), sheet)', 'save current sheet to filename in format determined by extension (default .tsv)')\nBaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSaveName()), sheet, confirm_overwrite=False)', 'save current sheet without asking for filename or confirmation')\nBaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet())', 'save root sheet to its source')\nBaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath(\"save all sheets to: \"), *vd.stackedSheets)', 'save all sheets to given file or directory)')\nIndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath(\"save %d sheets to: \" % nSelectedRows, value=\"_\".join(getattr(vs, \"name\", None) or \"blank\" for vs in selectedRows)), *selectedRows)', 'save all selected sheets to given file or directory')\nSheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')\nSheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')\n\nvd.addMenuItems('''\n    File > Save > current sheet > save-sheet\n    File > Save > all sheets > save-all\n    File > Save > current column > save-col\n    File > Save > keys and current column > save-col-keys\n''')\n", "path": "visidata/save.py"}], "after_files": [{"content": "import collections\nimport os\nfrom copy import copy\n\nfrom visidata import vd\nfrom visidata import Sheet, BaseSheet, VisiData, IndexSheet, Path, Progress, TypedExceptionWrapper\n\nvd.option('safe_error', '#ERR', 'error string to use while saving', replay=True)\nvd.option('save_encoding', 'utf-8', 'encoding passed to codecs.open when saving a file', replay=True, help=vd.help_encoding)\n\n@Sheet.api\ndef safe_trdict(vs):\n    'returns string.translate dictionary for replacing tabs and newlines'\n    if vs.options.safety_first:\n        delim = vs.options.delimiter\n        return {\n             0: '', #  strip NUL completely\n    ord(delim): vs.options.tsv_safe_tab, # \\t\n            10: vs.options.tsv_safe_newline,  # \\n\n            13: vs.options.tsv_safe_newline,  # \\r\n        }\n    return {}\n\n\n@Sheet.api\ndef iterdispvals(sheet, *cols, format=False):\n    'For each row in sheet, yield OrderedDict of values for given cols.  Values are typed if format=False, or a formatted display string if format=True.'\n    if not cols:\n        cols = sheet.visibleCols\n\n    transformers = collections.OrderedDict()  # list of transformers for each column in order\n    for col in cols:\n        transformers[col] = [ col.type ]\n        if format:\n            formatMaker = getattr(col, 'formatter_'+(col.formatter or sheet.options.disp_formatter))\n            transformers[col].append(formatMaker(col._formatdict))\n        trdict = sheet.safe_trdict()\n        if trdict:\n            transformers[col].append(lambda v,trdict=trdict: v.translate(trdict))\n\n    options_safe_error = sheet.options.safe_error\n    for r in Progress(sheet.rows):\n        dispvals = collections.OrderedDict()  # [col] -> value\n        for col, transforms in transformers.items():\n            try:\n                dispval = col.getValue(r)\n\n            except Exception as e:\n                vd.exceptionCaught(e)\n                dispval = options_safe_error or str(e)\n\n            try:\n                for t in transforms:\n                    if dispval is None:\n                        break\n                    elif isinstance(dispval, TypedExceptionWrapper):\n                        dispval = options_safe_error or str(dispval)\n                        break\n                    else:\n                        dispval = t(dispval)\n\n                if dispval is None and format:\n                    dispval = ''\n            except Exception as e:\n                dispval = str(dispval)\n\n            dispvals[col] = dispval\n\n        yield dispvals\n\n\n@Sheet.api\ndef itervals(sheet, *cols, format=False):\n    for row in sheet.iterdispvals(*cols, format=format):\n        yield [row[c] for c in cols]\n\n@BaseSheet.api\ndef getDefaultSaveName(sheet):\n    src = getattr(sheet, 'source', None)\n    if hasattr(src, 'scheme') and src.scheme:\n        return src.name + src.suffix\n    if isinstance(src, Path):\n        if sheet.options.is_set('save_filetype', sheet):\n            # if save_filetype is over-ridden from default, use it as the extension\n            return str(src.with_suffix('')) + '.' + sheet.options.save_filetype\n        return str(src)\n    else:\n        return sheet.name+'.'+getattr(sheet, 'filetype', sheet.options.save_filetype)\n\n\n@VisiData.api\ndef save_cols(vd, cols):\n    sheet = cols[0].sheet\n    vs = copy(sheet)\n    vs.columns = list(cols)\n    vs.rows = sheet.rows\n    if len(cols) == 1:\n        savedcoltxt = cols[0].name + ' column'\n    else:\n        savedcoltxt = '%s columns' % len(cols)\n    path = vd.inputPath('save %s to: ' % savedcoltxt, value=vs.getDefaultSaveName())\n    vd.saveSheets(path, vs)\n\n\n@VisiData.api\ndef saveSheets(vd, givenpath, *vsheets, confirm_overwrite=True):\n    'Save all *vsheets* to *givenpath*.'\n\n    if not vsheets: # blank tuple\n        vd.warning('no sheets to save')\n        return\n\n    filetypes = [givenpath.ext.lower(), vd.options.save_filetype.lower()]\n\n    vd.clearCaches()\n\n    for ft in filetypes:\n        savefunc = getattr(vsheets[0], 'save_' + ft, None) or getattr(vd, 'save_' + ft, None)\n        if savefunc:\n            filetype = ft\n            break\n\n    if savefunc is None:\n        vd.fail(f'no function to save as {filetype}')\n\n    if confirm_overwrite:\n        vd.confirmOverwrite(givenpath)\n\n    vd.status('saving %s sheets to %s as %s' % (len(vsheets), givenpath.given, filetype))\n\n    if not givenpath.given.endswith('/'):  # forcibly specify save individual files into directory by ending path with /\n        for vs in vsheets:\n            vs.hasBeenModified = False\n        # savefuncs(vd, p, *vsheets) will have 2 argcount (*vsheets does not get counted as an arg)\n        # savefuncs(vd, p, vs) will have 3 argcount (vs counts as an arg, along with vd, path)\n        if savefunc.__code__.co_argcount == 3 and len(vsheets) > 1:\n            vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n        return vd.execAsync(savefunc, givenpath, *vsheets)\n\n    # path is a dir\n\n    # save as individual files in the givenpath directory\n    try:\n        os.makedirs(givenpath, exist_ok=True)\n    except FileExistsError:\n        pass\n\n    if not givenpath.is_dir():\n        vd.fail(f'cannot save multiple {filetype} sheets to non-dir')\n\n    def _savefiles(vsheets, givenpath, savefunc, filetype):\n        for vs in vsheets:\n            p = Path((givenpath / vs.name).with_suffix('.'+filetype))\n            savefunc(p, vs)\n            vs.hasBeenModified = False\n\n        vd.status(f'{givenpath} save finished')  #2157\n\n    return vd.execAsync(_savefiles, vsheets, givenpath, savefunc, filetype)\n\n\n@VisiData.api\ndef save_zip(vd, p, *vsheets):\n    vd.clearCaches()\n\n    import tempfile\n    import zipfile\n    with tempfile.TemporaryDirectory() as tmpdir:\n        with zipfile.ZipFile(str(p), 'w', zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=9) as zfp:\n            for vs in Progress(vsheets):\n                filetype = vs.options.save_filetype\n                tmpp = Path(f'{tmpdir}{vs.name}.{filetype}')\n                savefunc = getattr(vs, 'save_' + filetype, None) or getattr(vd, 'save_' + filetype, None)\n                savefunc(tmpp, vs)\n                zfp.write(tmpp, f'{vs.name}.{vs.options.save_filetype}')\n\n\n@VisiData.api\ndef save_txt(vd, p, *vsheets):\n    if len(vsheets) == 1 and vsheets[0].nVisibleCols > 1:  #2173\n        return vd.save_tsv(p, vsheets[0])\n\n    with p.open(mode='w', encoding=vsheets[0].options.save_encoding) as fp:\n        for vs in vsheets:\n            unitsep = vs.options.delimiter\n            rowsep = vs.options.row_delimiter\n            for dispvals in vs.iterdispvals(*vs.visibleCols, format=True):\n                fp.write(unitsep.join(dispvals.values()))\n                fp.write(rowsep)\n\n\n@BaseSheet.api\ndef rootSheet(sheet):\n    r = sheet\n    while isinstance(r.source, BaseSheet):\n        r = r.source\n\n    return r\n\n\nBaseSheet.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath(\"save to: \", value=getDefaultSaveName()), sheet)', 'save current sheet to filename in format determined by extension (default .tsv)')\nBaseSheet.addCommand('', 'save-sheet-really', 'vd.saveSheets(Path(getDefaultSaveName()), sheet, confirm_overwrite=False)', 'save current sheet without asking for filename or confirmation')\nBaseSheet.addCommand('', 'save-source', 'vd.saveSheets(rootSheet().source, rootSheet())', 'save root sheet to its source')\nBaseSheet.addCommand('g^S', 'save-all', 'vd.saveSheets(inputPath(\"save all sheets to: \"), *vd.stackedSheets)', 'save all sheets to given file or directory)')\nIndexSheet.addCommand('g^S', 'save-selected', 'vd.saveSheets(inputPath(\"save %d sheets to: \" % nSelectedRows, value=\"_\".join(getattr(vs, \"name\", None) or \"blank\" for vs in selectedRows)), *selectedRows)', 'save all selected sheets to given file or directory')\nSheet.addCommand('', 'save-col', 'save_cols([cursorCol])', 'save current column only to filename in format determined by extension (default .tsv)')\nSheet.addCommand('', 'save-col-keys', 'save_cols(keyCols + [cursorCol])', 'save key columns and current column to filename in format determined by extension (default .tsv)')\n\nvd.addMenuItems('''\n    File > Save > current sheet > save-sheet\n    File > Save > all sheets > save-all\n    File > Save > current column > save-col\n    File > Save > keys and current column > save-col-keys\n''')\n", "path": "visidata/save.py"}]}