cat

Print files, and join them end to end

Updated 2026-08-28

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$
38 outputs, collapsed by default

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

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

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