Conversion and IO - dathere/qsv GitHub Wiki
Tier: Intermediate
Commands covered: excel, readstat, writestat, to, json, jsonl, tojsonl, snappy, geoconvert, prompt, clipboard
Note
Per-command flag reference lives in /docs/help/. This page is the workflow layer β when to reach for each command and how they compose.
These commands bridge between CSV and other formats: Excel/ODS, the SAS/Stata/SPSS statistical formats (both directions), JSON (array & JSONL), Parquet, SQLite, PostgreSQL, Snappy-compressed, GeoJSON/SHP, plus the OS clipboard and file picker dialogs.
The clipboard command is also covered in Selection & Inspection β clipboard. The geoconvert command is also covered in Geospatial β geoconvert. This page focuses on the I/O angle.
| If you want to⦠| Use | Notes |
|---|---|---|
| Convert Excel / ODS sheet β CSV | excel |
Pick sheet by name (case-insensitive) or index |
| Convert SAS / Stata / SPSS file β CSV | readstat |
.sas7bdat, .xpt/.xpt5/.xpt8, .dta, .sav/.zsav/.por (new in 23.0.1); --dictionary also writes a JSON Schema data dictionary (24.0.0) |
| Write a CSV as SPSS / Stata / SAS transport | writestat |
.sav/.por, .dta, .xpt β restores labels & missing values from a readstat --dictionary (new in 24.0.0) |
| CSV β Parquet / SQLite / Postgres / Excel / ODS / Data Package | to |
Parquet supports zstd / gzip / snappy / lz4raw |
| JSON array β CSV | json |
Use --jaq for nested / awkward JSON |
| JSONL β CSV | jsonl |
First line determines headers |
| CSV β JSONL with smart type inference | tojsonl |
Uses the stats cache for type inference |
| Streaming Snappy compress / decompress | snappy |
Most qsv commands auto-handle .sz
|
| CSV β GeoJSON / SHP / SVG | geoconvert |
See Geospatial |
| OS file picker for input or output | prompt |
Pairs well with clipboard and lens
|
| Read/write the OS clipboard | clipboard |
See Selection & Inspection |
Export an Excel (.xls, .xlsx, .xlsm, .xlsb) or LibreOffice (.ods) sheet to CSV. The first non-empty row is treated as the header.
Example: export the first sheet of a workbook
qsv excel input.xlsx --output output.csvExample: export by sheet name (case-insensitive)
qsv excel --sheet 'Sheet 3' input.xlsx > sheet3.csvExample: export a sheet by negative index (last sheet)
qsv excel --sheet -1 input.xlsx > last_sheet.csvExample: export every sheet to its own CSV (one shell loop)
for i in $(qsv excel --metadata input.xlsx | qsv select sheet_name | qsv behead); do
qsv excel --sheet "$i" input.xlsx --output "${i}.csv"
doneThe MCP server and Claude Cowork plugin auto-convert Excel inputs via this command β see MCP Server.
See also: /docs/help/excel.md, to xlsx β the inverse (CSV β Excel), sniff.
New in 23.0.1. Convert SAS, Stata & SPSS binary files to CSV.
Decades of public-sector and academic data β health surveys, labor statistics, longitudinal panels, replication archives β are published only in the proprietary formats of the big statistical packages. readstat opens them with no SAS/Stata/SPSS licence required, and without the ReadStat C library or pyreadstat: it is powered by polars-readstat-rs, which is pure Rust.
Warning
EXPERIMENTAL. The readers this command is built on are young. Spot-check the output against your source files before relying on a conversion, and please report anything that looks wrong.
| Package | Extensions | Memory |
|---|---|---|
| SAS | .sas7bdat |
streams β constant memory |
| SAS transport |
.xpt, .xpt5, .xpt8
|
streams β constant memory |
| Stata | .dta |
streams β constant memory |
| SPSS |
.sav, .zsav
|
streams β constant memory |
| SPSS portable | .por |
π€― read whole β see the warning below |
The format is picked from the file extension, so stdin is not supported β readstat needs a real path. Piping out of it works normally. Passing a .sas7bcat (a SAS format catalog, not a dataset) errors with a pointer to the .sas7bdat file instead.
| Flag | Type | Description | Default |
|---|---|---|---|
--metadata <fmt> |
string | Dump variable metadata instead of the data. Valid values: none, csv, json, pretty-json. |
none |
--value-labels |
flag | Decode coded values to their label strings (e.g. 1 becomes "Male") instead of writing the underlying codes. For SAS .sas7bdat files (24.0.0+), the labels come from the format catalog named by --sas7bcat, else <name>.sas7bcat or formats.sas7bcat beside the data file. Not supported for .xpt. (23.0.1 was Stata & SPSS only.) |
|
--sas7bcat <file> |
string |
24.0.0+. Names the SAS format catalog holding a .sas7bdat file's value labels, for --value-labels, --sentinels-as label or --metadata. On its own it is refused for the data. |
|
--dictionary <file> |
string | 24.0.0+. Also write a JSON Schema data dictionary of the CSV, built from the file's own metadata β see below. | |
--compress-numeric |
flag |
24.0.0+. Write float variables that only ever hold whole numbers as integers (3.0 becomes 3). Decided per variable over the rows written, so the file is read twice. |
|
--sentinels-as <mode> |
string |
24.0.0+. Keep user-defined missing values ("sentinels") instead of writing empty cells: value (the code, e.g. .A or 99), label (its value label), or none (empty cells, and skip the dropped-sentinel check). Each eligible variable gets a <name>_null column. |
|
--sentinels-embedded |
flag |
24.0.0+. Write each sentinel into its variable's own column instead of a <name>_null column. Requires --sentinels-as. |
|
--sentinels-columns <list> |
string |
24.0.0+. Only keep sentinels for these variables. Requires --sentinels-as. |
|
--select <cols> |
string |
24.0.0+. Read only these variables, in this order, using qsv's select syntax (names, indices, q1-q20, /regex/, ! to exclude). Skipped variables are never decoded. Also narrows --metadata. |
|
--offset <n> / --limit <n>
|
integer | 24.0.0+. Read a window of rows. Stata, uncompressed SPSS & SAS transport files jump straight to the offset. |
0 / |
--sample <n> / --seed <n>
|
integer |
24.0.0+. Write a random sample of n rows from that window, in file order; --seed makes it reproducible. |
|
-j, --jobs <arg> |
integer | Number of reader threads. Raising it speeds up large uncompressed files at the cost of memory, as out-of-order chunks have to be buffered to keep the rows in source order. Row order is preserved either way. | 1 |
-b, --batch <size> |
integer | Number of rows to read into memory at a time. Does not apply to SPSS portable (.por) files β they have no chunked reader upstream, so they are read whole & memory scales with the file. Since 24.0.0, nor to SPSS .sav/.zsav files while their sentinels are tracked (see below), which also reads them whole. |
50000 |
-o, --output <file> |
string | Write output to <file> instead of stdout. |
|
-d, --delimiter <arg> |
string | The delimiter to use when writing CSV data. Must be a single character. | , |
Example: convert a SAS dataset, then profile it
qsv readstat nhanes.sas7bdat --output nhanes.csv
qsv stats nhanes.csv | qsv tableExample: a coded SPSS survey β codes by default, labels on request
# default: the underlying codes survive, so `frequency`/`stats` see real numbers
qsv readstat survey.sav | qsv frequency --select sex
# --value-labels decodes them to "Male"/"Female" instead (Stata & SPSS; SAS too, 24.0.0+)
qsv readstat --value-labels survey.sav --output survey_labelled.csvExample: dump the variable dictionary as a browsable table
qsv readstat --metadata csv panel.dta | qsv tableExample: straight into a pipeline, no temp file
qsv readstat data.sas7bdat | qsv stats --everythingExample: peek at part of a large file (24.0.0+)
qsv readstat --select id,age,income --limit 1000 survey.sav
qsv readstat --sample 500 --seed 42 data.sas7bdatThese formats store a coded value (1) alongside a value label ("Male") held in a separate dictionary. readstat writes the code, not the label. That is deliberate, and lossless:
-
stats,schema,frequencyanddescribegptthen see the numbers the file actually stores, so type inference is correct and the coding survives a round trip. - The labels are not lost β
--metadatadumps the full value-label map for every variable.
Pass --value-labels when you want the human-readable strings instead. On 23.0.1 it was Stata & SPSS only and errored on a SAS file.
New in 24.0.0: SAS labels are supported (#4626). --value-labels on a .sas7bdat file uses its format catalog: <name>.sas7bcat, then formats.sas7bcat, beside the data file. --sas7bcat <file> names one explicitly. If no catalog is found, the command fails with a hint instead of silently writing codes. --metadata then includes each labeled SAS variable's value_labels. --sentinels-as label uses the same catalog to label only the sentinels .A-.Z & ._, leaving the values as codes unless --value-labels is also given. --metadata lists sentinel labels as MISSING_A, etc.
qsv readstat --sas7bcat formats.sas7bcat survey.sas7bdat --output survey_labelled.csvNote
Numeric variables arrive as floats. These formats store every numeric as a double, so an integer-looking column converts with a decimal point β a YEAR of 1948 becomes 1948.0, and a coded sex of 1 becomes 1.0. QSV_POLARS_FLOAT_PRECISION controls how many decimal places are written (see Environment Variables).
New in 24.0.0: --compress-numeric writes such variables as integers (1948, 1) when every value in them is a whole number (#4661).
--metadata csv|json|pretty-json dumps what these files carry about their variables β variable labels, value labels, missing-value codes, measure & display settings, file encoding β instead of the data itself.
# machine-readable, for a documentation pipeline
qsv readstat --metadata pretty-json panel.dta > panel_dictionary.jsonNote
In the JSON forms the per-variable array is named columns for SAS and variables for Stata/SPSS, and the keys inside it differ by family too. The csv form flattens whichever shape it finds, deriving its columns from the data, so you do not have to branch on the source package.
New in 24.0.0: --dictionary <file> writes, alongside the CSV, a draft 2020-12 JSON Schema data dictionary in the same shape describegpt --dictionary writes β but built from the file's own metadata, with no LLM involved (#4728). Name it <stem>.schema.json after the CSV and viz smart finds it on its own.
qsv readstat --dictionary survey.schema.json survey.sav -o survey.csv
qsv validate survey.csv survey.schema.json # validate against it
qsv viz smart survey.csv # picks up survey.schema.json- Variable labels become each column's
title; value labels go underx-qsv.value_labels; SPSS declared missing values underx-qsv.missing_values; SPSS measure becomesx-qsv.role(measure/dimension). -
enumis added only when the labels cover every value written, sovalidatenever rejects the data the dictionary describes. - It describes the CSV as written β
--value-labels,--compress-numeric,--select/--limit/--sampleall shape it β and records the options used underx-qsv.readstat_flags, which is what letswritestatundo them.
Warning
SPSS portable (.por) files are read whole. There is no chunked reader for .por upstream, so --batch does not bound its memory β peak usage scales with the file size. This is the only reason the command is marked π€―; every other supported format streams with constant memory. For a large .por, convert on a machine with headroom (or ask the publisher for a .sav).
New in 24.0.0: SPSS .sav/.zsav files are also read whole while their user-defined missing values are tracked: with --sentinels-as value|label, or by the default sentinel check below when a variable declares missing values. Pass --sentinels-as none to stream them.
--jobs trades memory, not order. Row order is preserved at any --jobs setting. Above one thread the readers take a chunk-interleaving path, and out-of-order chunks are buffered back into source order β which costs memory, not correctness. The default of 1 therefore keeps memory flat; raise it to speed up large uncompressed files.
Note
Stata extended missing values (.a through .z) rendered as NaN, not as empty cells, on 23.0.1. Ordinary missing values do come out empty.
New in 24.0.0: fixed upstream in polars-readstat-rs 0.23.2 (#4627). Stata .a-.z now come out as empty cells in every storage type. To keep them, use --sentinels-as value|label, which also works for SAS and SPSS user-defined missing values (#4625).
New in 24.0.0: dropped sentinels are reported (#4708). Without --sentinels-as, readstat still writes them as empty cells, but warns at the end how many it dropped, in which variables, with an example:
5 sentinels (user-defined missing values) in 3 variables (income, rating, name) were written as empty cells, e.g. 99 in income. Use --sentinels-as value to keep them, or --sentinels-as none to skip this check.
The output is unchanged. --sentinels-as none skips the check. It is also skipped at --jobs above 1, because tracking sentinels reads the file on a single thread. For SPSS files that declare missing values, the check costs about +76% time and reads the file whole (see above).
Note
readstat is a standalone conversion command (#4542 remains open). SAS/Stata/SPSS files are not yet transparently readable as input by other qsv commands the way .zip / .gz / Parquet inputs are β qsv stats data.sas7bdat will not work. Convert to CSV first. To carry the variable metadata along, use --dictionary (24.0.0+), which validate, viz smart and writestat read.
readstat rides on the polars feature (β¨π€―π»ββοΈπ) β it is in qsv, qsvpy* and qsvmcp, but not in qsvlite or qsvdp, and like viz it is absent from the big-endian targets (s390x, ppc64 BE). See Binary Variants.
See also: /docs/help/readstat.md, excel β the spreadsheet equivalent, stats, describegpt, MCP Server β exposed to LLM clients as the qsv_readstat tool, Recipe: Round-Trip SAS / Stata / SPSS.
New in 24.0.0. The inverse of readstat: write a CSV as an SPSS (.sav, .por), Stata (.dta) or SAS transport (.xpt) file (#4728).
Warning
EXPERIMENTAL. Check the files it writes in the package that will read them before relying on them.
The format comes from the --output extension, or --format sav|por|dta|xpt|xpt5|xpt8 (.xpt is transport v8 by default). --compress writes a bytecode-compressed .sav. Compressed .zsav files and .sas7bdat datasets can't be written. --output is required; the CSV can come from stdin.
The round trip. Hand writestat the dictionary readstat --dictionary wrote, and the file comes back with its metadata: variable labels, value labels, SPSS missing values (discrete, range and string), measures, alignments, display widths & formats, the file label and SAS dataset name β each where the output format can hold it. What readstat did to write the CSV is undone: values decoded by --value-labels go back to their codes, and --sentinels-as sentinels become declared missing values again. Round trips are identical for every SPSS .sav fixture and for the SAS fixtures written as transport v5 and v8.
# SPSS β CSV (+ dictionary) β back to SPSS, metadata intact
qsv readstat --dictionary survey.schema.json survey.sav -o survey.csv
qsv writestat --dictionary survey.schema.json survey.csv -o survey2.sav
# the same data as a Stata file
qsv writestat --dictionary survey.schema.json survey.csv -o survey.dta
# no dictionary: column types are inferred from the CSV
qsv writestat data.csv -o data.xptA dictionary from describegpt --dictionary works too β its titles become variable labels.
For a worked, verified example β edit the CSV in between, prove the metadata survived, and see what --lossy drops going to Stata β see Recipe: Round-Trip SAS / Stata / SPSS.
Refuses rather than silently dropping. Metadata the output format can't hold β value labels & missing values in SAS transport and SPSS portable files, missing values & file labels in Stata, labels too long for SAS transport, column names Stata would rename, Stata tagged missing values β is an error. --lossy writes the file anyway and warns about each thing left out. Names a format can't hold at all (too long, or not valid SPSS variable names in .sav/.por) are always an error; fix them first with rename or safenames.
| Flag | Description |
|---|---|
--dictionary <file> |
JSON Schema data dictionary giving the variable metadata & types |
--format <fmt> |
sav, por, dta, xpt, xpt5, xpt8 β when not implied by --output
|
--lossy |
Write even if metadata has to be left out, warning about each |
--compress |
Bytecode-compress an SPSS .sav
|
--table-name <name> |
Dataset name in a SAS transport file |
writestat is gated with readstat (β¨π€―π»ββοΈ) β the same binaries carry it. See /docs/help/writestat.md.
CSV β other formats. Subcommands: parquet, xlsx, ods, sqlite, postgres, datapackage. Each can take multiple input CSVs (or a directory, or a .infile-list).
Example: convert NYC 311 to Parquet with zstd compression
qsv to parquet --infer-len 0 output_dir/ NYC_311_SR_2010-2020-sample-1M.csv
ls output_dir/
# NYC_311_SR_2010-2020-sample-1M.parquetThe parquet subcommand needs the polars feature. Default compression is zstd; override with --compression {zstd,gzip,snappy,lz4raw,uncompressed} and --compress-level N.
Column types are inferred from the first 1,000 rows by default (or taken from a current <file>.pschema.json, e.g. from qsv schema --polars). If a later value doesn't fit an inferred type, the whole conversion fails β on this file, a ZIP+4 such as 48090-2123 deep in the Incident Zip column, which the first rows make look like integers. --infer-len 0 scans every row before choosing types; a larger --infer-len N is the cheaper middle ground. to parquet also refuses to overwrite an existing .parquet file in the output directory β delete it first when re-running.
Note
25.0.0: --all-strings writes every column as text by reading it as text from the start, so type inference can't fail and leading zeros (00501) are kept; --infer-len, any .pschema.json and --try-parse-dates are not used with it (#4807). In 24.0.0 and earlier, --all-strings converted the columns only after the inferred types were applied, so it failed on the same values.
Example: load wcp.csv into a fresh PostgreSQL database
qsv to postgres 'postgresql://user:pass@host/db' wcp.csv
# Creates a table named "wcp"Example: load multiple related CSVs into a single SQLite database
qsv to sqlite analytics.db nyc311.csv weather.csv stations.csv
# Tables: nyc311, weather, stationsExample: Excel workbook with one sheet per CSV
qsv to xlsx report.xlsx q1.csv q2.csv q3.csv q4.csv
# Sheet names match file stemsExample: Frictionless Data Package β CSV + JSON metadata
qsv to datapackage datapackage.json wcp.csv country_continent.csvExample: include stats in the Data Package
qsv to datapackage --stats datapackage.json wcp.csv--stats runs qsv stats against each CSV and embeds the result in the Data Package descriptor.
See also: /docs/help/to.md, sqlp β read Parquet inline with read_parquet, schema --polars, Integrations: DuckDB.
JSON array β CSV. The input must be a flat array (or single object) β no nested arrays/objects without a --jaq flatten step.
Example: simple JSON array of fruit records
[
{"fruit":"apple","price":2.50,"calories":95},
{"fruit":"banana","price":1.00,"calories":105}
]qsv json fruits.json > fruits.csvExample: extract a nested array with jaq
# response.json has structure: { "data": { "items": [ {...}, {...} ] } }
qsv json --jaq '.data.items' response.json > items.csvSee also: /docs/help/json.md, jsonl, tojsonl, jaq syntax.
JSONL / NDJSON β CSV. The first JSON line determines the headers; subsequent lines must be consistent.
Example: GitHub events stream
qsv jsonl events.jsonl > events.csvExample: pipe an API response through jaq, then jsonl
curl -s 'https://api.github.com/events' \
| jaq -c '.[]' \
| qsv jsonl > events.csvSee also: /docs/help/jsonl.md, fetch β fetch writes JSONL by default.
CSV β JSONL with smart type inference. Runs stats first to determine the right JSON type (string, number, boolean, null) for each column. Reuses an existing stats cache.
Booleans are inferred when cardinality = 2 and values match t/f, 1/0, y/n, or <value>/null patterns.
Boolean inference fixes (Unreleased, after 25.0.0, #4830). The two-value check is case- and whitespace-insensitive, so
True/False/false/TRUEis a boolean column. In 25.0.0 that only worked when no other column had a small number of distinct values: adding one (e.g. ay/ncolumn) leftTrue/Falseas strings. Also, without--trim, a padded value likeTruewas converted tofalse; it now converts totrue.
Example: convert wcp.csv to JSONL with typed values
qsv tojsonl wcp.csv > wcp.jsonl
head -1 wcp.jsonl
# {"Country":"ad","City":"aixas","AccentCity":"AixΓ s","Region":"06","Population":null,"Latitude":42.4833333,"Longitude":1.4666667}Note Population is null (not "") and the coordinates are numbers (not strings).
Example: pre-populate the stats cache, then convert (skips the type-inference scan)
qsv stats --cardinality --infer-dates --stats-jsonl wcp.csv
qsv tojsonl wcp.csv > wcp.jsonlSee also: /docs/help/tojsonl.md, jsonl, stats, Stats Cache & Caching.
Streaming Snappy compression / decompression. Four subcommands: compress, decompress, check (first 50 bytes), validate (whole file).
Most qsv commands already handle .sz files transparently β the snappy command is the explicit interface when you need:
- Multithreaded compression (5β6Γ faster than the auto-compression)
- Compression of non-CSV files (it's format-agnostic)
- Integrity check / validation of an existing
.sz
Example: compress a 15 GB NYC 311 export (multithreaded)
qsv snappy compress nyc311-full.csv > nyc311-full.csv.szExample: decompress to stdin for inline pipelines
qsv snappy decompress nyc311-full.csv.sz | qsv slice --len 100 | qsv tableExample: validate a .sz file is well-formed
qsv snappy validate suspicious.csv.sz && echo "OK" || echo "Corrupt"Example: implicit compression via output filename
qsv slice --len 1000 wcp.csv --output sample.csv.sz
# qsv compresses on write (single-threaded). Use `qsv snappy compress` for multithreaded.See also: /docs/help/snappy.md, Performance Tuning β when streaming compression pays off, Indexing, Compression & Diff.
qsv inspects each input and, when it recognizes a non-CSV "special format," converts it to delimited text before the command runs. As of v21.1.0 this layer became both more convenient and stricter.
Transparent auto-decompression (more convenient). Compressed inputs now just work β no manual unpack step:
-
Local
.zipinputs (and lookup tables) β the first tabular entry is extracted automatically. -
Compressed remote sources (
.gz/.zlib/.zst) for lookup tables andget/dc:ingests β streamed and decompressed on the fly. -
.sz(Snappy) was already auto-handled by most commands.
Zip selection is now consistent. The command-level "extract all entries" path (cat, sqlp, to, validate, scoresql) and the reader-level "first tabular entry" path (every other command) share one zip module with a single rule: entries are returned tabular-first (CSV/TSV/TAB/SSV before other supported formats), so a single-input command and a Config-only command (stats, count, β¦) pick the same entry from a mixed multi-entry zip. A zip with no supported entry now errors clearly.
Conversion failures are now errors (stricter). When a detected special-format input β Avro, Parquet, Arrow IPC, JSON, JSONL, or a gzip/zlib/zstd/snappy-compressed CSV/TSV/SSV β fails to convert, qsv previously fell back to reading the original (often binary) bytes as delimited text, emitting garbage with a success exit code. That silent fallback is gone: a conversion failure is now reported and qsv exits non-zero (matching the long-standing behavior for .zip).
If you genuinely want the old "read raw bytes as delimited text" behavior, opt in explicitly with the QSV_SKIP_FORMAT_CHECK environment variable (see Environment Variables).
See also: Indexing, Compression & Diff β Transparent input decompression.
Convert between CSV (or SVG) and various spatial formats β GeoJSON, SHP, KML, GPX, and more. Pairs nicely with geocode.
Example: GeoJSON β CSV
qsv geoconvert nta_boundaries.geojson geojson csv > nta.csvExample: CSV with WKT geometry β GeoJSON for QGIS
qsv geoconvert parcels.csv csv geojson --geometry geometry > parcels.geojsonExample: stdin input via prompt
qsv prompt -m 'Choose a GeoJSON file' -F geojson | qsv geoconvert - geojson csvSee also: /docs/help/geoconvert.md, Geospatial, geocode.
OS-native file dialog (input or output). Useful for ad-hoc analyses where you don't want to copy-paste paths.
Example: pick a file, summarize it, view results
qsv prompt | qsv stats | qsv lensExample: pick a spreadsheet β CSV β save dialog for the result
qsv prompt -m 'Select a spreadsheet' -F xlsx,xls,ods \
| qsv excel - \
| qsv prompt -m 'Save exported CSV to...' --fd-outputprompt is part of the UI feature group (β¨π»ββοΈπ₯οΈ) β not available in qsvlite / qsvdp / qsvmcp.
See also: /docs/help/prompt.md, clipboard, lens.
See Selection & Inspection β clipboard. TL;DR:
# Read from clipboard
qsv clipboard | qsv stats | qsv table
# Write to clipboard
qsv stats wcp.csv | qsv table | qsv clipboard --save- Command Reference (index)
- Selection & Inspection β clipboard
-
Geospatial β
geocodecompanion togeoconvert -
Binary Variants β which builds include
readstat/writestat -
SQL & Polars β
sqlpreads Parquet / Arrow / JSONL inline -
Stats Cache & Caching β
tojsonlbenefits from the cache -
Integrations β using
to postgreswith DataPusher+, DuckDB workflows - Cookbook β Build a Data Pipeline