cut: Slicing Fixed Columns Out of Text

Per GNU coreutils’ cut(1) man page, cut “remove[s] sections from each line of files” — it prints the parts of each line you ask for and drops the rest. It has two selection modes: by character position (-c) or by delimiter-separated field (-d/-f).

Reach for cut when a line has one fixed delimiter, or truly fixed-width columns, and you just want a field or two back — it’s simpler than awk for that. It stops working once fields are separated by a run of whitespace of varying width, or by a multi-character separator. awk’s default field separator is a single space (per GNU gawk’s man page), but awk treats that as any run of whitespace, not one literal space — cut -d' ' has no equivalent; it matches exactly one delimiter character each time. This post runs cut both ways against a synthetic access log and shows where -c breaks.

The data

All three cut calls below run against the same access.log, 30 lines, one simulated HTTP request per line in a Combined-Log-Format-style layout: client IP, two placeholder dashes, [timestamp], "METHOD PATH HTTP/1.1", status code, response size in bytes — fields separated by single spaces (the quoted request itself contains spaces too, so it splits into three of the ten total fields).

203.0.113.5 - - [24/Aug/2026:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:02 +0000] "GET /index.html HTTP/1.1" 200 532
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
198.51.100.23 - - [24/Aug/2026:10:00:05 +0000] "GET /static/app.js HTTP/1.1" 200 8213
192.0.2.77 - - [24/Aug/2026:10:00:07 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:09 +0000] "POST /login HTTP/1.1" 200 356
192.0.2.77 - - [24/Aug/2026:10:00:11 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.23 - - [24/Aug/2026:10:00:12 +0000] "GET /favicon.ico HTTP/1.1" 404 0
203.0.113.5 - - [24/Aug/2026:10:00:14 +0000] "GET /api/users HTTP/1.1" 200 964
198.51.100.9 - - [24/Aug/2026:10:00:16 +0000] "GET /index.html HTTP/1.1" 200 532
192.0.2.77 - - [24/Aug/2026:10:00:18 +0000] "GET /api/orders HTTP/1.1" 500 89
203.0.113.5 - - [24/Aug/2026:10:00:20 +0000] "DELETE /api/orders/44 HTTP/1.1" 204 0
198.51.100.23 - - [24/Aug/2026:10:00:22 +0000] "GET /old-page HTTP/1.1" 301 178
192.0.2.77 - - [24/Aug/2026:10:00:24 +0000] "GET /api/orders HTTP/1.1" 200 1180
203.0.113.5 - - [24/Aug/2026:10:00:26 +0000] "GET /static/app.js HTTP/1.1" 200 8213
198.51.100.9 - - [24/Aug/2026:10:00:28 +0000] "POST /login HTTP/1.1" 401 128
192.0.2.77 - - [24/Aug/2026:10:00:30 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:32 +0000] "GET /admin HTTP/1.1" 403 96
198.51.100.23 - - [24/Aug/2026:10:00:34 +0000] "GET /index.html HTTP/1.1" 200 532
192.0.2.77 - - [24/Aug/2026:10:00:36 +0000] "PUT /api/users/9 HTTP/1.1" 200 210
203.0.113.5 - - [24/Aug/2026:10:00:38 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.9 - - [24/Aug/2026:10:00:40 +0000] "GET /favicon.ico HTTP/1.1" 404 0
192.0.2.77 - - [24/Aug/2026:10:00:42 +0000] "GET /static/app.js HTTP/1.1" 200 8213
203.0.113.5 - - [24/Aug/2026:10:00:44 +0000] "GET /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:46 +0000] "POST /login HTTP/1.1" 200 356
192.0.2.77 - - [24/Aug/2026:10:00:48 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:50 +0000] "GET /old-page HTTP/1.1" 301 178
198.51.100.9 - - [24/Aug/2026:10:00:52 +0000] "GET /api/orders HTTP/1.1" 500 89
192.0.2.77 - - [24/Aug/2026:10:00:54 +0000] "GET /admin HTTP/1.1" 403 96
203.0.113.5 - - [24/Aug/2026:10:00:56 +0000] "GET /index.html HTTP/1.1" 200 532

The code

#!/usr/bin/env bash
set -e

echo "== -f1: client IPs =="
cut -d' ' -f1 access.log | head -5

echo
echo "== -f1,9: IP and status code =="
cut -d' ' -f1,9 access.log | head -5

echo
echo "== -c1-11: first 11 characters of each line =="
cut -c1-11 access.log | head -5

Per the man page:

  • -d, --delimiter=DELIM — use DELIM instead of tab as the field delimiter (here, a space, since the log is space-separated).
  • -f, --fields=LIST — “select only these fields; also print any line that contains no delimiter character, unless the -s option is specified.” LIST can be a single number or a comma-separated list — -f1 and -f1,9 below.
  • -c, --characters=LIST — “select only these character positions,” independent of any delimiter. -c1-11 below means characters 1 through 11 of every line, whatever they happen to be.

One pitfall: -d only takes a single character. Feed it a multi-character separator and cut won’t guess — it refuses:

$ cut -d'::' -f2 access.log
cut: the delimiter must be a single character

For multi-char or regex delimiters, that’s awk -F or sed territory, not cut.

The man page also lists -b, --bytes=LIST beside -c — byte position instead of character position. On plain ASCII the two behave the same, but they diverge on multi-byte text: cutting café at -c1-4 returns all four characters intact, while -b1-4 slices through the 2-byte é and prints a garbled fourth character. -n, --no-partial fixes that by dropping the partial byte instead of emitting a broken one — -b1-4 -n on the same input returns just caf, three clean bytes rather than a mangled fourth. Default to -c unless a byte offset is specifically what’s needed.

Running it

Real output:

== -f1: client IPs ==
203.0.113.5
198.51.100.23
203.0.113.5
198.51.100.23
192.0.2.77

== -f1,9: IP and status code ==
203.0.113.5 200
198.51.100.23 200
203.0.113.5 401
198.51.100.23 200
192.0.2.77 200

== -c1-11: first 11 characters of each line ==
203.0.113.5
198.51.100.
203.0.113.5
198.51.100.
192.0.2.77 

cut -d' ' -f1 and -f1,9 pulled clean field values regardless of how long each value was — the IP 198.51.100.23 (13 chars) came through whole in the -f output. cut -c1-11, though, cut by raw character position: for the shorter IP 203.0.113.5 it printed the whole address, but for 198.51.100.23 it printed only 198.51.100. — the fixed 11-character window sliced through the middle of that IP.

Takeaway

-f and -c both cut a line down, but on different axes: -f is delimiter-aware, so a field comes through intact no matter how long its value is, while -c is a raw position cut with no idea where a field starts or ends — in this run it sliced a longer IP off mid-string that -f1 had passed through whole. That makes -c only safe on genuinely fixed-width data; the moment column widths vary (like the IPs here) or the delimiter isn’t a single character, -f or awk are the tools that won’t cut a value in half.