cat
Print files, and join them end to end
cat prints a file. Given several it prints them one after another with nothing in between,
which is where the name comes from.
Most of what gets said about it is a warning. cat access.log | grep 404 starts a process to do
what grep 404 access.log does alone. It is the stock example of shell written by someone who
has not noticed that filters take filenames. The complaint is aimed at the pipe. Joining two
files still needs cat, and so does getting a filter's output without the filename column it
prints when you hand it more than one file.
cat -A shows the bytes a terminal draws as nothing: tabs, trailing spaces, and the carriage
returns a file collects from an editor on Windows. A script that fails with Illegal option -
looks correct wherever you open it and takes one command to diagnose.
Sample files used on this page
Every example below was run against these files. Recreate them to follow along.
access.log the same combined-format log the tail and grep pages use
203.0.113.5 - - [13/Aug/2026:09:12:01] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:12:03] "GET /style.css HTTP/1.1" 200 231
198.51.100.7 - - [13/Aug/2026:09:14:22] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [13/Aug/2026:09:14:25] "GET /missing.html HTTP/1.1" 404 162
203.0.113.5 - - [13/Aug/2026:09:15:47] "GET /index.html HTTP/1.1" 200 512
192.0.2.44 - - [13/Aug/2026:09:16:03] "POST /login HTTP/1.1" 302 0
192.0.2.44 - - [13/Aug/2026:09:16:04] "GET /dashboard HTTP/1.1" 200 4021
198.51.100.7 - - [13/Aug/2026:09:18:51] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:19:10] "GET /api/status HTTP/1.1" 500 89
192.0.2.44 - - [13/Aug/2026:09:20:33] "GET /dashboard HTTP/1.1" 200 4021
access.log.1 the rotated half of the same log, every entry earlier than the ten above
203.0.113.5 - - [12/Aug/2026:23:58:11] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [12/Aug/2026:23:59:02] "GET /about.html HTTP/1.1" 200 1044
192.0.2.44 - - [13/Aug/2026:00:01:40] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:00:04:19] "POST /login HTTP/1.1" 401 0
notes.txt ten lines, four of them blank, in runs of one and two
Release checklist
- bump the version
- run the full replay
- tag and push
Nothing is done yet.
deploy.sh shown through cat -A, because a plain listing hides the carriage returns and the tab this file exists to carry
#!/bin/sh^M$
set -e^M$
if [ -d dist ]; then^M$
^Irsync -a dist/ web@deb1:/srv/www/^M$
fi^M$
Printing a file
With one filename cat copies that file to standard output and stops. No pager, no headers, no numbering unless you ask.
Print a file to the screen
cat access.log
The whole file, in order, with nothing added. For a file long enough to scroll past you want head, tail or less instead.
Show output
203.0.113.5 - - [13/Aug/2026:09:12:01] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:12:03] "GET /style.css HTTP/1.1" 200 231
198.51.100.7 - - [13/Aug/2026:09:14:22] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [13/Aug/2026:09:14:25] "GET /missing.html HTTP/1.1" 404 162
203.0.113.5 - - [13/Aug/2026:09:15:47] "GET /index.html HTTP/1.1" 200 512
192.0.2.44 - - [13/Aug/2026:09:16:03] "POST /login HTTP/1.1" 302 0
192.0.2.44 - - [13/Aug/2026:09:16:04] "GET /dashboard HTTP/1.1" 200 4021
198.51.100.7 - - [13/Aug/2026:09:18:51] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:19:10] "GET /api/status HTTP/1.1" 500 89
192.0.2.44 - - [13/Aug/2026:09:20:33] "GET /dashboard HTTP/1.1" 200 4021
Let the shell open the file instead
cat < access.log.1
Identical output by a different route. cat is handed an already-open standard input and never learns the filename. wc -l < file prints a bare number for the same reason.
Show output
203.0.113.5 - - [12/Aug/2026:23:58:11] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [12/Aug/2026:23:59:02] "GET /about.html HTTP/1.1" 200 1044
192.0.2.44 - - [13/Aug/2026:00:01:40] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:00:04:19] "POST /login HTTP/1.1" 401 0
Ask for a file that is not there
cat missing.txt
The message goes to standard error, so a redirect of standard output does not capture it. Nothing is printed and nothing is created.
Show output
cat: missing.txt: No such file or directory
One missing file out of several is still a failure
cat notes.txt missing.txt > /dev/null; echo "exit $?"
cat prints every file it can read and exits 1 regardless. A script that concatenates a list of files needs to check this, because the output looks complete.
Show output
cat: missing.txt: No such file or directory
exit 1
cat will not print a directory
cat .
It reports the directory and moves on. Use ls for the contents of a directory and cat for the contents of a file.
Show output
cat: .: Is a directory
Read a file whose name begins with a dash
printf 'saved by a typo\n' > ./-output.html && cat -- -output.html
A mistyped curl -o -output.html leaves a file cat would otherwise read as a bundle of flags. -- ends the options, and ./-output.html works too.
Show output
saved by a typo
Joining files
Extra filenames are extra arguments. cat reads them in the order you wrote them and puts nothing between them, so joining a rotated log to a current one is a matter of naming the older file first.
Join a rotated log to the current one
cat access.log.1 access.log | head -6
The four rotated entries come first, then the current log picks up where they left off. head is here only to keep the page short; without it you get all fourteen lines.
Show output
203.0.113.5 - - [12/Aug/2026:23:58:11] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [12/Aug/2026:23:59:02] "GET /about.html HTTP/1.1" 200 1044
192.0.2.44 - - [13/Aug/2026:00:01:40] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:00:04:19] "POST /login HTTP/1.1" 401 0
203.0.113.5 - - [13/Aug/2026:09:12:01] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:12:03] "GET /style.css HTTP/1.1" 200 231
Name them the other way round and the timestamps run backwards
cat access.log access.log.1 | tail -2
cat sorts nothing. The last two lines of this stream are the oldest requests in either file, because access.log.1 was named second.
Show output
192.0.2.44 - - [13/Aug/2026:00:01:40] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:00:04:19] "POST /login HTTP/1.1" 401 0
Write the joined result to a new file
cat access.log.1 access.log > combined.log && wc -l combined.log
Fourteen lines, four from the rotated file and ten from the current one. The redirect is the shell's work. cat only supplies the stream.
Show output
14 combined.log
Append one file to the end of another
cat access.log.1 >> notes.txt && wc -l notes.txt
>> adds to the file instead of replacing it. Ten lines of notes.txt become fourteen. See pipes and redirection for the difference between the two.
Show output
14 notes.txt
Never redirect into a file you are also reading
cat access.log.1 access.log > combined.log && wc -l combined.log && cat access.log.1 combined.log > combined.log; wc -l combined.log
The shell truncates combined.log before cat starts, so the fourteen lines are gone by the time anything is read. GNU cat notices and refuses to read its own output file, which saves you from an infinite file but not from the four lines you are left with. Write to a new name and mv it into place.
Show output
14 combined.log
cat: combined.log: input file is output file
4 combined.log
A file with no final newline runs into the next one
printf 'first file, no trailing newline' > partial.txt && cat partial.txt notes.txt | head -1
cat copies bytes and adds none, so a file that does not end in a newline is joined to the next file mid-line. Most editors do not show the difference. A join like this one is usually where it first appears.
Show output
first file, no trailing newlineRelease checklist
Confirm the missing newline before you join anything
printf 'first file, no trailing newline' > partial.txt && cat -A partial.txt
Every complete line ends $ under -A. This one has none at all: the file ends without a newline.
Show output
first file, no trailing newline
Numbering lines and collapsing blank runs
Number every line
cat -n notes.txt
-n numbers all ten lines, blank ones included. The number is right-aligned in six columns and followed by a tab.
Show output
1 Release checklist
2
3 - bump the version
4
5
6 - run the full replay
7 - tag and push
8
9
10 Nothing is done yet.
Number only the lines with something on them
cat -b notes.txt
-b skips the blanks, so the five real lines are numbered one to five and the file keeps its shape. It overrides -n when both are given.
Show output
1 Release checklist
2 - bump the version
3 - run the full replay
4 - tag and push
5 Nothing is done yet.
Collapse runs of blank lines into one
cat -s notes.txt
-s (--squeeze-blank) leaves a single blank line wherever there was a run of them. Useful on generated files that separate every section with three.
Show output
Release checklist
- bump the version
- run the full replay
- tag and push
Nothing is done yet.
Squeeze first, then number what is left
cat -sn notes.txt
The flags combine, and -s happens first: eight lines survive the squeeze and the numbering stops at eight.
Show output
1 Release checklist
2
3 - bump the version
4
5 - run the full replay
6 - tag and push
7
8 Nothing is done yet.
Numbering carries on across files
cat -n access.log.1 access.log | tail -3
The count runs unbroken across both files, because cat produced one stream. head and tail restart per file and print a header naming each; cat does neither.
Show output
12 198.51.100.7 - - [13/Aug/2026:09:18:51] "GET /index.html HTTP/1.1" 200 512
13 203.0.113.5 - - [13/Aug/2026:09:19:10] "GET /api/status HTTP/1.1" 500 89
14 192.0.2.44 - - [13/Aug/2026:09:20:33] "GET /dashboard HTTP/1.1" 200 4021
Number the output of a pipeline
grep 200 access.log | cat -n
With no filename cat numbers whatever arrives on standard input. grep -n would number the lines by their position in the original file; this numbers the matches.
Show output
1 203.0.113.5 - - [13/Aug/2026:09:12:01] "GET /index.html HTTP/1.1" 200 512
2 203.0.113.5 - - [13/Aug/2026:09:12:03] "GET /style.css HTTP/1.1" 200 231
3 198.51.100.7 - - [13/Aug/2026:09:14:22] "GET /index.html HTTP/1.1" 200 512
4 203.0.113.5 - - [13/Aug/2026:09:15:47] "GET /index.html HTTP/1.1" 200 512
5 192.0.2.44 - - [13/Aug/2026:09:16:04] "GET /dashboard HTTP/1.1" 200 4021
6 198.51.100.7 - - [13/Aug/2026:09:18:51] "GET /index.html HTTP/1.1" 200 512
7 192.0.2.44 - - [13/Aug/2026:09:20:33] "GET /dashboard HTTP/1.1" 200 4021
Seeing the bytes a terminal hides
A terminal draws a tab, a trailing space and a carriage return as nothing at all, so a file that looks correct can still be wrong. cat -A prints those bytes as visible escapes.
A shell script that will not run
sh deploy.sh
The script is ordinary shell, and set -e is rejected anyway. Nothing looks wrong with the file in any editor that opens it.
Show output
deploy.sh: 2: set: Illegal option -
Find out why with -A
cat -A deploy.sh
Every line ends ^M$ rather than $. The ^M is a carriage return, picked up from an editor on Windows. sh reads it as part of the -e argument. ^I on the fourth line is a tab.
Show output
#!/bin/sh^M$
set -e^M$
if [ -d dist ]; then^M$
^Irsync -a dist/ web@deb1:/srv/www/^M$
fi^M$
Strip the carriage returns and check again
sed -i 's/\r$//' deploy.sh && cat -A deploy.sh
sed removes the return at the end of each line. The tab stays, because a tab inside a shell script is legal and was never the problem.
Show output
#!/bin/sh$
set -e$
if [ -d dist ]; then$
^Irsync -a dist/ web@deb1:/srv/www/$
fi$
Mark line ends and nothing else
cat -E notes.txt | head -4
-E (--show-ends) puts a $ at the end of every line. It is the flag for finding trailing spaces, which sit between the last word and the $.
Show output
Release checklist$
$
- bump the version$
$
Mark tabs and nothing else
cat -T deploy.sh
-T (--show-tabs) writes ^I for a tab and leaves everything else alone. This is the one to use on a Makefile, where a line indented with spaces instead of a tab is a syntax error.
Show output
#!/bin/sh
set -e
if [ -d dist ]; then
^Irsync -a dist/ web@deb1:/srv/www/
fi
Mark non-printing characters and nothing else
cat -v deploy.sh
-v shows the carriage returns as ^M but leaves the tab as a tab, so the fourth line is still indented rather than marked. Tabs and newlines are exempt from -v by design, and -T and -E exist to cover them.
Show output
#!/bin/sh^M
set -e^M
if [ -d dist ]; then^M
rsync -a dist/ web@deb1:/srv/www/^M
fi^M
-A is the other three together
cat -vET deploy.sh
-A (--show-all) is defined as -vET, so the two commands print the same thing. -e is -vE and -t is -vT, for when you want two of the three.
Show output
#!/bin/sh^M$
set -e^M$
if [ -d dist ]; then^M$
^Irsync -a dist/ web@deb1:/srv/www/^M$
fi^M$
Writing a file from the terminal
cat with no filename reads standard input. A heredoc supplies several lines of that inline, so a file can be written without another one existing to copy from.
Create a file from a heredoc
cat > todo.txt <<'EOF'
bump the version
run the full replay
EOF
cat todo.txt
Everything between the delimiters becomes the file. Quoting the delimiter as 'EOF' stops the shell expanding $ and backticks inside the block. Leave the delimiter unquoted only when you want that expansion.
Show output
bump the version
run the full replay
Append a heredoc to an existing file
cat > todo.txt <<'EOF'
bump the version
EOF
cat >> todo.txt <<'EOF'
tag and push
EOF
cat todo.txt
>> behaves the same way it does anywhere else: the second heredoc adds to the file instead of replacing it.
Show output
bump the version
tag and push
Splice standard input between two files
cat - access.log.1 <<< "from stdin" | head -3
A bare - is where standard input goes in the argument list, so you can put it before, after or between filenames. Without the - the standard input is ignored entirely.
Show output
from stdin
203.0.113.5 - - [12/Aug/2026:23:58:11] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [12/Aug/2026:23:59:02] "GET /about.html HTTP/1.1" 200 1044
Write a file you do not own
cat notes.txt | sudo tee /etc/motd > /dev/null
sudo cat notes.txt > /etc/motd fails, because the redirect is performed by your shell and your shell was never elevated. tee is the usual way round it. It takes a heredoc in place of the cat just as readily.
When the pipe into cat is the wrong shape
cat file | filter is the most criticised construction in shell. These five examples find or count the same thing, two of them with cat and three without.
The useless use of cat
cat access.log | grep 404
Correct output, one more process than the job needs. grep reads files perfectly well, so the cat and the pipe do nothing but move the bytes.
Show output
198.51.100.7 - - [13/Aug/2026:09:14:25] "GET /missing.html HTTP/1.1" 404 162
The same result, one process
grep 404 access.log
Every standard filter takes filenames: grep, sed, awk, sort, wc. The pipe is for input a filename cannot express.
Show output
198.51.100.7 - - [13/Aug/2026:09:14:25] "GET /missing.html HTTP/1.1" 404 162
Joining two files is a job for cat
cat access.log access.log.1 | grep -c "GET /index.html"
grep -c given two filenames prints a count for each file separately. Joining them first gives one stream and one number.
Show output
6
Losing the filename column a filter would print
wc -l access.log
wc prints the name of every file it was given. A number heading for a variable does not want it.
Show output
10 access.log
A redirect drops the filename without a second process
wc -l < access.log
wc never learns the name, so it prints the count alone. cat access.log | wc -l gives the same answer for the same reason and spends a process doing it.
Show output
10
Compressed, reversed, and binary
Read a gzipped file without unpacking it
gzip -n -c access.log.1 > rotated.gz && zcat rotated.gz | head -2
zcat is cat for gzipped files, and there is a zgrep, a zdiff and a zless beside it. -n on the gzip keeps the original name and timestamp out of the archive.
Show output
203.0.113.5 - - [12/Aug/2026:23:58:11] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [12/Aug/2026:23:59:02] "GET /about.html HTTP/1.1" 200 1044
What plain cat makes of the same file
gzip -n -c access.log.1 > rotated.gz && head -c 4 rotated.gz | cat -v
The first four bytes of any gzip file, escaped by -v so they cannot reach the terminal as control codes. A whole binary file sent to a terminal unescaped leaves it printing garbage afterwards. reset puts it back.
Show output
^_M-^K^H^@
Print a file backwards
tac access.log.1
tac is cat with the lines in reverse. Debian's tail has no -r, so tail -n 20 | tac is the usual way to see the end of a large file newest-first.
Show output
203.0.113.5 - - [13/Aug/2026:00:04:19] "POST /login HTTP/1.1" 401 0
192.0.2.44 - - [13/Aug/2026:00:01:40] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [12/Aug/2026:23:59:02] "GET /about.html HTTP/1.1" 200 1044
203.0.113.5 - - [12/Aug/2026:23:58:11] "GET /index.html HTTP/1.1" 200 512
Read a one-line system file
cat /etc/debian_version
Most of what the system reports about itself is a short file somewhere under /etc, /proc or /sys. cat reads all of them. See list installed packages for the ones that need a real tool instead.
Show output
Your output will differ: the point release moves whenever the machine is updated
13.6