Short answer: the cut command prints selected parts of every line. cut -d: -f1 /etc/passwd splits each line at : and prints the first field, -c1-10 prints characters 1 to 10, and -b works on bytes. Without -d the delimiter is a tab. Lists like 1,3 and ranges like 2- pick several fields.
cut -d: -f1 /etc/passwd # user names
cut -d' ' -f1 access.log # client IPs from an nginx log
cut -c1-10 app.log # the date of ISO-timestamped lines
cut -d= -f2- .env # everything after the first =Every example below was run on Debian 12 with GNU coreutils 9.1, the version Debian 12 ships, in a scratch directory against sample files; the output shown is copied from those runs.
cut cheat sheet
| Task | Command |
|---|---|
| First field, colon-separated | cut -d: -f1 file |
| Fields 1 and 7 | cut -d: -f1,7 file |
| Fields 1 to 3 | cut -d: -f1-3 file |
| Field 3 to the end of the line | cut -d: -f3- file |
| Everything except field 2 | cut -d: -f2 --complement file |
| Join the output with another separator | cut -d: -f1,6 --output-delimiter=' ' file |
| Tab-separated input (the default) | cut -f2 file.tsv |
| Skip lines that have no delimiter | cut -s -d: -f2 file |
| Characters 1 to 15 | cut -c1-15 file |
| Bytes instead of characters | cut -b1-4 file |
| Read from a pipe | getent passwd | cut -d: -f1 |
-d and -f: cutting fields
-d sets the delimiter, one character, and -f says which fields to print, counted from 1. /etc/passwd is the classic input: seven fields separated by colons (name, password placeholder, UID, GID, comment, home directory, shell).
$ cut -d: -f1 /etc/passwd
root
daemon
www-data
sshd
deploy
$ cut -d: -f1,7 /etc/passwd
root:/bin/bash
daemon:/usr/sbin/nologin
www-data:/usr/sbin/nologin
sshd:/usr/sbin/nologin
deploy:/bin/bashThe selected fields are joined with the input delimiter again, so -f1,7 keeps the colon. A delimiter that the shell treats specially needs quoting: -d' ' for a space, -d, is fine as it is, and -d$'\t' spells out a tab in bash (though tab is the default anyway). For real systems, getent passwd | cut -d: -f1 is the better source, because it also includes accounts from LDAP or other directories; listing users on Linux explains the difference.
Order and duplicates: cut always keeps the input order
$ cut -d: -f7,1 /etc/passwd | head -n 2
root:/bin/bash
daemon:/usr/sbin/nologinAsking for field 7 before field 1 changes nothing: cut writes the selected fields in the order they appear in the line, each once, so -f1,1,1 prints field 1 a single time. To reorder or repeat columns, use awk -F: '{print $7, $1}', covered in the awk command guide.
Lists and ranges
| Write | Selects | Example output on root:x:0:0:root:/root:/bin/bash |
|---|---|---|
-f3 | Field 3 only | 0 |
-f1,3 | Fields 1 and 3 | root:0 |
-f1-3 | Fields 1 to 3 | root:x:0 |
-f6- | Field 6 to the end | /root:/bin/bash |
-f-2 | From the start to field 2 | root:x |
-f1,6- | Combined | root:/root:/bin/bash |
The open range N- is the one to remember for values that may contain the delimiter themselves. In a KEY=value file, a password like s3cr=t loses everything after its own = with -f2, and comes out whole with -f2-:
$ cat .env
DB_HOST=127.0.0.1
DB_PASS=s3cr=t
$ cut -d= -f2 .env
127.0.0.1
s3cr
$ cut -d= -f2- .env
127.0.0.1
s3cr=t-c and -b: characters and bytes
-c selects by position instead of by delimiter, which suits fixed-width text. The classic syslog format starts every line with a 15-character timestamp, as in this sample auth.log:
$ cut -c1-15 auth.log
Oct 21 09:14:02
Oct 21 09:20:45
$ cut -c17- auth.log
web1 sshd[812]: Accepted publickey for deploy from 203.0.113.7 port 52144 ssh2
web1 sshd[901]: Failed password for root from 192.0.2.41 port 40022 ssh2Look at the first line before counting characters. rsyslog can also write ISO 8601 timestamps such as 2026-10-21T09:14:02.123456+00:00, and then cut -d' ' -f1 is the cleaner way to the timestamp. On the Debian 12 host these examples ran on, rsyslog is not installed and there is no /var/log/auth.log at all: SSH logins are in the systemd journal, which journalctl reads.
--complement: everything except
$ cut -d: -f2 --complement /etc/passwd | head -n 2
root:0:0:root:/root:/bin/bash
daemon:1:1:daemon:/usr/sbin:/usr/sbin/nologin
$ echo '2026-10-21T09:14:02Z' | cut -c11 --complement
2026-10-2109:14:02Z--complement inverts the selection, for fields, characters or bytes alike. It is shorter than listing every field you want to keep when you only want to drop one, such as a password or token column before sharing a file.
--output-delimiter: change the separator on the way out
$ cut -d: -f1,6 --output-delimiter=' ' /etc/passwd
root /root
daemon /usr/sbin
www-data /var/www
sshd /run/sshd
deploy /home/deployThe output delimiter can be any string, not only one character, and --output-delimiter=$'\t' in bash turns colon-separated input into tab-separated output. Both --complement and --output-delimiter are GNU extensions; the BSD cut on macOS does not have them, per its man page.
Tabs, -s, and lines without a delimiter
Two defaults surprise people. The default delimiter is a tab, not a space. And a line that does not contain the delimiter at all is printed whole, not skipped:
$ printf 'alpha\tbeta\tgamma\nno tab here\n' > tabbed.txt
$ cut -f2 tabbed.txt
beta
no tab here
$ cut -s -f2 tabbed.txt
beta-s (--only-delimited) drops those lines, which is what you want for input with comments or blank lines mixed in. A field number beyond the last field simply prints an empty line.
cut on log files
nginx and Apache write their access logs in the combined format, where the parts are separated by single spaces and quotes. Against a six-line sample log:
203.0.113.7 - - [21/Oct/2026:09:14:02 +0000] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"$ cut -d' ' -f1 access.log | sort | uniq -c | sort -rn # requests per IP
3 203.0.113.7
2 192.0.2.41
1 198.51.100.23
$ cut -d' ' -f9 access.log | sort | uniq -c # status codes
3 200
1 302
2 404
$ grep '" 404 ' access.log | cut -d' ' -f7 # paths that returned 404
/wp-login.php
/.env
$ cut -d: -f2 access.log | sort | uniq -c # requests per hour (IPv4 clients)
3 09
3 10
$ cut -d'"' -f2 access.log | head -n 2 # the request line
GET / HTTP/1.1
GET /pricing HTTP/1.1The per-hour count works because the first colon in the line is the one after the date; an IPv6 client address has colons of its own and shifts the field. Cutting at " is the robust way to reach the request and the user agent, because those can contain spaces. Two cuts in a row extract text between two different characters, for example the timestamp between the brackets: cut -d'[' -f2 access.log | cut -d']' -f1. To narrow the lines first, grep in front of cut is the usual pairing, as in the 404 example. Two more one-liners that come up on every server:
sha256sum backup.tar.gz | cut -d' ' -f1 # just the hash, without the file name
echo "$PATH" | cut -d: -f1 # the first directory in PATHColumns padded with several spaces
cut treats every single delimiter as a field boundary, so two spaces in a row mean an empty field between them. Output aligned for humans, from df, ps or ls -l, therefore breaks it:
$ cat df.txt
Filesystem Type Size Used Avail Use% Mounted on
/dev/vda1 ext4 79G 31G 45G 41% /
$ cut -d' ' -f2 df.txt # prints empty lines: field 2 is the empty gap
$ tr -s ' ' < df.txt | cut -d' ' -f6
Use%
41%tr -s ' ' squeezes runs of spaces into one, after which cut works. That still fails on right-aligned numbers: a line that starts with spaces gets an empty field 1, so the field numbers shift between lines.
$ cat ps.txt
812 sshd
12345 node
$ tr -s ' ' < ps.txt | cut -d' ' -f2
812
node
$ awk '{print $2}' ps.txt
sshd
nodeawk splits on runs of blanks and ignores leading ones, so it gets this right without any preparation. For anything aligned with spaces, awk is the better tool.
The CSV caveat: quoted fields
cut -d, -f3 looks like the obvious way to read a CSV column, and it works until a value contains a comma inside quotes, which real CSV allows:
$ cat users.csv
id,name,city,plan
1,Anna,Amsterdam,Standard KVM 2
2,"Smith, John",Frankfurt,Hi-CPU KVM 1
3,"O'Neil ""Jr""",Amsterdam,Storage VPS
$ cut -d, -f3 users.csv
city
Amsterdam
John"
AmsterdamRow 2 returns half a name instead of the city, with no error. cut knows nothing about quoting, so use a tool that does. Python’s csv module is on almost every server; gawk’s FPAT handles simple quoted fields too:
$ python3 -c 'import csv,sys; [print(r[2]) for r in csv.reader(sys.stdin)]' < users.csv
city
Amsterdam
Frankfurt
Amsterdam
$ gawk -v FPAT='([^,]*)|("[^"]*")' '{print $3}' users.csv
city
Amsterdam
Frankfurt
AmsterdamDedicated CSV tools such as Miller (mlr) or csvkit do the same for whole pipelines. Files exported on Windows have one more trap: lines end in \r\n, so the last field keeps an invisible carriage return (cat -A shows it as ^M). tr -d '\r' < file.csv | cut -d, -f2 removes it first. To skip a header row, tail -n +2 file.csv | cut -d, -f1.
cut vs awk vs tr
| Tool | Best at | Falls short when |
|---|---|---|
| cut | Fields split by one exact character (:, =, tab, comma without quotes), fixed character positions | Columns padded with several spaces, quoted CSV, reordering columns |
| awk | Whitespace-aligned columns, reordering, conditions on a field, sums and counts | Quoted CSV (gawk FPAT helps), JSON |
| tr | Replacing or deleting single characters: tr ':' '\n', tr -s ' ', tr -d '\r' | Selecting fields: it never splits a line into columns |
| sed | Editing text in place: substitute, delete or insert lines | Arithmetic or per-column logic |
In practice they work together: tr cleans the input, cut picks the field, sort | uniq -c counts. When a job needs a condition on a column or a total, switch to awk; when it needs a file edited in place, the sed command is the tool.
Common cut errors
| Message (GNU coreutils 9.1) | Cause | Fix |
|---|---|---|
you must specify a list of bytes, characters, or fields | No -f, -c or -b given | Add -f with a field list |
the delimiter must be a single character | A multi-character -d such as -d '::' or -d ', ' | Use awk -F for multi-character separators |
fields are numbered from 1 | -f0 | Fields start at 1 |
invalid decreasing range | -f3-1 | Write ranges low to high: -f1-3 |
only one list may be specified | -f and -c together | Pick one mode, or pipe two cuts |
an input delimiter may be specified only when operating on fields | -d together with -c or -b | Drop -d, or use -f |
cut on a server
- It is always there.
cutis part of coreutils, so every Debian, Ubuntu and Rocky Linux install has it, including minimal images and containers based on them. - It is fast on big logs. It does no regex matching and keeps no state, so a
cut | sort | uniq -cpipeline over a large access log is mostly the cost ofsort. - Rotated logs work too.
zcat access.log.2.gz | cut -d' ' -f1reads a compressed rotation without unpacking it to disk.
Frequently asked
How do I get one column with the cut command?
Give the delimiter with -d and the field number with -f: cut -d: -f1 /etc/passwd prints the first colon-separated field of every line. Fields count from 1, -f1,3 selects two fields, -f2- selects field 2 to the end of the line, and without -d the delimiter is a tab.
Why does cut -d' ' print empty lines?
The columns are separated by several spaces, and cut treats every single space as a delimiter, so the field you asked for is an empty gap between two spaces. Squeeze the spaces first with tr -s ' ', or use awk '{print $2}', which splits on runs of whitespace and ignores leading blanks.
Can cut change the order of fields?
No. cut always prints the selected fields in the order they appear in the input, each one once, so -f3,1 gives the same output as -f1,3. Use awk -F: '{print $3, $1}' to reorder or repeat fields.
Can I use cut on CSV files?
Only on simple CSV without quoted fields. A value such as "Smith, John" contains the delimiter, and cut -d, splits it in two without any warning. Use a CSV-aware tool instead: Python's csv module, gawk with FPAT, Miller or csvkit.