Skip to content
datahacks.dev
CSV

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.

8 min read
Summarize in AI
Flat illustration of a terminal session, with the GNU Bash logo.

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:

Shell
head -n 4 orders-2m.csv
Text
order_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 first

That 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:

Shell
column -t -s, orders-small.csv
Text
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\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:

Shell
column -t -s, people.csv
Text
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\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:

Shell
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 7p
Text
LANG= 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:

Shell
column -t -s, file.csv | less -S

less 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:

Shell
csvlook orders-small.csv
Text
| 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:

Shell
csvlook people.csv
Text
| 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:

Shell
qsv table orders-small.csv
Text
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 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:

Shell
mlr --icsv --opprint head -n 3 orders-small.csv
Text
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

mlr --icsv --opprint head -n 3 shows Smith, Jane as one cell and left-aligns everything; --barred draws the borders:

Shell
mlr --icsv --opprint --barred head -n 3 orders-small.csv
Text
+----+-------------+--------+-----+--------+----------+
| 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.

Shell
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
done
Text
0.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/null

The 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.

Taggedclihow-to
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.