Previewing a CSV in the Terminal With column and less
Look at a CSV in the terminal with head, column -t, less -S, csvlook, qsv table and Miller, and learn where each one misleads you on quotes and encodings.

The first thing to do with a CSV you did not make is to look at it. A text editor opens a 100 MB file slowly and shows commas, a spreadsheet may change it on the way in, and cat floods the terminal. The terminal tools for looking are quick, and most of them give a misleading picture of the one thing you wanted to check, the quoted fields. This article goes through the ones I use, shows what each does to a small file with a quoted comma, a doubled quote, a line break and an accent, and says which I trust for what. It belongs to the wider tour in working with CSV in the shell.
Start with the top
Never preview a whole file. head reads the beginning and exits, so it costs the same on a 130 MB file as on a small one:
head -n 4 orders-2m.csvorder_id,customer_id,zip,amount,created_at,status,country,note
1,22313,99154,103.1,2025-11-22 13:08:24,shipped,GB,rush order
2,193489,34494,31.59,2025-03-03 08:03:33,failed,BR,leave at door
3,163643,93451,89.98,2025-02-15 11:13:00,pending,JP,call firstThat is the header and three rows of the orders file, in raw form. Raw is useful: you see the delimiter, the quoting, the line ending (with head -n 3 | cat -A if you suspect CRLF) and whether the first row is really a header. It is hard to read once the rows are wide, and the tools below fix that.
column: fast and naive
column -t -s, (the util-linux manual describes -t and -s) splits each line on commas and pads the fields into aligned columns. On a file with no quoting it is exactly what you want:
column -t -s, orders-small.csvid customer city qty price status
1 Ana Silva Lisbon 3 9.50 paid
2 "Smith Jane" Boston 10 2.25 refunded
3 Bo Chen Oslo 2 100.00 paid
4 Cy Dubois Lyon 15.75 pending
5 "Doe ""JD"" John" Rome 7 3.10 paid
6 Ed Z\xc3\xbcrich Bern 1 8.00 failed
7 Flo Kyiv 12 1.05 paid
8 Gus Riga 5 20.00 refunded The table is aligned, and the empty qty on row 4 is preserved as an empty cell in this version of column. Look at row 2, though. "Smith, Jane" became two cells, "Smith and Jane", and every cell after it shifted one column to the right; the status value refunded is now under the wrong heading. Row 5, "Doe ""JD"" John", has no comma inside the quotes and survived with its quotes and doubled quotes showing as characters. column knows nothing about CSV quoting. It splits on the separator you give it, and that is all.
A second file shows the other limit, a field with a line break:
column -t -s, people.csvid name city note
1 Ana Silva Lisbon likes tea
2 "Smith Jane" Boston "said ""hello"""
3 Bo Chen Oslo "line one
line two"
4 Cy Dubois Lyon The note "line one\nline two" printed as two lines, the second one belonging to no column, and the comma in "Smith, Jane" shifted Boston and the note again. For a preview that is merely ugly. For a decision, such as which column holds what, it is wrong.
There is a third trap, in row 6 of the first table: Ed Z\xc3\xbcrich. In this container the locale is the default C, so column prints each byte of a UTF-8 character as an escape. Setting a UTF-8 locale fixes it:
echo "LANG=$LANG LC_ALL=$LC_ALL"; locale -a | head -5
column -t -s, orders-small.csv | sed -n 7p
LC_ALL=C.UTF-8 column -t -s, orders-small.csv | sed -n 7pLANG= LC_ALL=
C
C.utf8
POSIX
6 Ed Z\xc3\xbcrich Bern 1 8.00 failed
6 Ed Zürich Bern 1 8.00 failed The first line of the output is the empty LANG and LC_ALL, then the available locales, then the same row twice: once with Z\xc3\xbcrich and once, with LC_ALL=C.UTF-8, as Zürich. On a normal desktop the locale is already UTF-8, so you may never see this, but you will on a minimal server or in a container, and it is a quick one to recognize.
A last note on the tool: the empty-field behavior is the version's. This column kept empty cells aligned (a,b,c with 1,,3 and ,, came out as separate cells), but older and BSD implementations are reported to merge adjacent separators, which would silently shift the columns, and I did not test one. If a column in your preview looks suspiciously short, test that with a three-line file before you trust it.
less: for width
A table wider than the screen wraps and turns into mush. less -S chops long lines instead of wrapping, and you scroll sideways with the arrow keys:
column -t -s, file.csv | less -Sless is interactive, so I do not show its output. Its -S option is documented in the manual, along with / to search and q to quit. The combination is my default for files without quoting: head -n 200 file.csv | column -t -s, | less -S is instant and shows enough.
csvlook: correct, slow to start
csvkit's csvlook (see the csvkit documentation) is a real CSV parser, so quoting is handled:
csvlook orders-small.csv| id | customer | city | qty | price | status |
| -- | ------------- | ------ | --- | ------ | -------- |
| 1 | Ana Silva | Lisbon | 3 | 9.50 | paid |
| 2 | Smith, Jane | Boston | 10 | 2.25 | refunded |
| 3 | Bo Chen | Oslo | 2 | 100.00 | paid |
| 4 | Cy Dubois | Lyon | | 15.75 | pending |
| 5 | Doe "JD" John | Rome | 7 | 3.10 | paid |
| 6 | Ed Zürich | Bern | 1 | 8.00 | failed |
| 7 | Flo | Kyiv | 12 | 1.05 | paid |
| 8 | Gus | Riga | 5 | 20.00 | refunded |Smith, Jane sits in one cell, Doe "JD" John has lost its CSV escaping and is shown as the person's name, and numbers are right-aligned. The people file shows how it treats a line break:
csvlook people.csv| id | name | city | note |
| -- | ----------- | ------ | ----------------- |
| 1 | Ana Silva | Lisbon | likes tea |
| 2 | Smith, Jane | Boston | said "hello" |
| 3 | Bo Chen | Oslo | line one↵line two |
| 4 | Cy Dubois | Lyon | |The note line one↵line two shows the newline as a ↵ symbol, so the table stays one row per record. That is the right behavior for a preview.
The cost is speed. csvlook reads and type-checks the whole input before it prints, because it needs every value to size the columns. Fed 1,000 lines it took 0.62 s, mostly Python starting up. Pointed at the full 2,000,000 row file with head -n 5 after it, it took about 12 minutes (710 s) on this machine, while other jobs were also running, so treat the number as an order of magnitude and not a benchmark. The lesson is the same either way: cut the file down with head before it goes in, and csvlook is fine.
qsv table and Miller
qsv has a table command that aligns columns, and it is fast because its parser is:
qsv table orders-small.csvid customer city qty price status
1 Ana Silva Lisbon 3 9.50 paid
2 Smith, Jane Boston 10 2.25 refunded
3 Bo Chen Oslo 2 100.00 paid
4 Cy Dubois Lyon 15.75 pending
5 "Doe ""JD"" John" Rome 7 3.10 paid
6 Ed Zürich Bern 1 8.00 failed
7 Flo Kyiv 12 1.05 paid
8 Gus Riga 5 20.00 refundedSmith, Jane is one cell. Row 5 is shown as "Doe ""JD"" John", the CSV escaped form, where csvlook shows the decoded name. Both are defensible, and it is a reminder that "the preview" is a representation. In my run qsv slice -l 5 | qsv table took 0.40 s on the 2,000,000 row file.
Miller can print a table too, in its pretty-print format:
mlr --icsv --opprint head -n 3 orders-small.csvid customer city qty price status
1 Ana Silva Lisbon 3 9.50 paid
2 Smith, Jane Boston 10 2.25 refunded
3 Bo Chen Oslo 2 100.00 paidmlr --icsv --opprint head -n 3 shows Smith, Jane as one cell and left-aligns everything; --barred draws the borders:
mlr --icsv --opprint --barred head -n 3 orders-small.csv+----+-------------+--------+-----+--------+----------+
| id | customer | city | qty | price | status |
+----+-------------+--------+-----+--------+----------+
| 1 | Ana Silva | Lisbon | 3 | 9.50 | paid |
| 2 | Smith, Jane | Boston | 10 | 2.25 | refunded |
| 3 | Bo Chen | Oslo | 2 | 100.00 | paid |
+----+-------------+--------+-----+--------+----------+Because head is a streaming verb, Miller stops reading when it has enough rows. Applied to the full 2,000,000 row file it took 0.03 s.
cd /batches/csv-0001-0200/work
for c in "head -n 1000 orders-2m.csv | column -t -s," "head -n 1000 orders-2m.csv | csvlook" "qsv slice -l 5 orders-2m.csv | qsv table" "mlr --icsv --opprint head -n 5 orders-2m.csv"; do
/usr/bin/time -f "%es %C" bash -c "$c > /dev/null" 2>&1 | tail -1 | cut -c1-90
done0.00s bash -c head -n 1000 orders-2m.csv | column -t -s, > /dev/null
0.62s bash -c head -n 1000 orders-2m.csv | csvlook > /dev/null
0.40s bash -c qsv slice -l 5 orders-2m.csv | qsv table > /dev/null
0.03s bash -c mlr --icsv --opprint head -n 5 orders-2m.csv > /dev/nullThe four timings are for head | column (under 0.01 s), head | csvlook (0.62 s), qsv slice | qsv table (0.40 s) and the Miller command (0.03 s). I would not choose between them on speed alone. Choose on correctness for your file.
What I use
For a file I trust to have no quoted commas, head -n 30 file.csv | column -t -s, | less -S. For a file I do not know, mlr --icsv --opprint head -n 10 file.csv, which is quick and correct, and qsv headers file.csv to see the column names with their positions. For a file that might be broken, I look at the raw head -n 5 | cat -A first, because every pretty printer hides the thing that is actually wrong, like a CRLF or a BOM. The shapes of those problems are in how CSV quoting works and escaping quotes and newlines.
How to tell it worked
A preview is right when the number of columns in every row equals the number in the header and the values sit under the headings you expect. Check the one row you know has a comma, a quote or a line break in it. If a column heading has data from a different column under it, the preview is splitting a quoted field. Compare with a second tool on the same five lines.
When a preview lies to you
column lies whenever a quote contains the delimiter or a line break. Any tool lies when the encoding is not what the terminal expects, and when the file starts with a BOM, which appears as an invisible character glued to the first header. None of them tell you the types: a 00501 that reads as text in the preview may be an integer in the next tool. For anything beyond a first look, a SQL query on the file with DESCRIBE and SUMMARIZE shows more, faster, and shows it correctly.


