Viewing File Contents (cat, less, head, tail)

Every file on a Linux system is just a sequence of bytes sitting on disk, and before you can edit, search, or reason about that data you first have to look at it. The cat, less, head, and tail commands are the four core tools for reading a file’s contents from the command line, and each exists because dumping an entire file onto your screen isn’t always the right approach. This lesson covers what each command actually does under the hood, when to reach for which one, and the mistakes that trip up almost everyone the first time they use them.

Overview: How Viewing a File Actually Works

When you run cat notes.txt, the shell doesn’t do the reading itself — it starts a new process for cat, which asks the kernel to open the file by name. The kernel resolves that path through the filesystem, checks that your user has read permission on the file (the "r" bit, one of the nine permission bits every file carries), and if all is well hands back a small integer called a file descriptor — a per-process index into a table of open files. From there, cat repeatedly reads bytes from that descriptor into a buffer and writes those same bytes to file descriptor 1, standard output, normally connected to your terminal. It keeps doing this until it hits end-of-file (EOF), then closes the descriptor and exits with status 0.

That loop is the entire idea behind cat ("concatenate"): it streams a file’s bytes to stdout from start to finish, with no pausing and no memory of where it left off. That’s perfect for short files, for piping data into another command, or for gluing several files into one stream — but it’s a poor choice for a 50,000-line log file, because the whole thing scrolls past your terminal in a fraction of a second, and only the last screenful remains visible.

That’s the problem less solves. less is a pager: instead of writing the whole file to stdout, it reads only enough to fill your terminal window (it discovers your terminal’s size with an ioctl call), displays that one screenful, and waits for a keypress. Press space and it reads and shows the next screenful; press b and it seeks backward and redisplays the previous one. Because it reads lazily and can seek around the file, less can open files far larger than your available RAM almost instantly — it never needs the whole file in memory at once. Its name is a joke on its older, more limited ancestor more: "less is more."

head and tail take a different shortcut: they only care about one end of the file. head reads from the beginning and stops as soon as it has printed the requested number of lines (10 by default), closing the descriptor early rather than reading the rest — so head on a multi-gigabyte file is nearly instant. tail does the opposite: for a regular file, GNU tail can seek directly near the end (using the file’s recorded size) and read backward from there instead of scanning every byte from the start, which is why tail -n 20 huge.log stays fast no matter how large the file is.

tail has one more trick: the -f ("follow") flag turns it into a live monitor. After printing the last lines, it doesn’t exit — it keeps the file descriptor open and watches for new data being appended, printing new lines as they arrive. On Linux, GNU tail does this efficiently using the kernel’s inotify API to get notified the instant the file changes, rather than repeatedly polling it. This is the standard way to watch a log file in real time while a service is running.

Syntax

All four commands follow the same general GNU pattern: a command name, optional flags, then one or more filenames.

cat [OPTION]... [FILE]...
less [OPTION]... [FILE]...
head [OPTION]... [FILE]...
tail [OPTION]... [FILE]...

If no FILE is given, all four read from standard input instead, which is what lets them sit at the end of a pipeline, e.g. grep error app.log | less.

cat options

Flag Meaning
-n Number every output line
-b Number only non-blank output lines
-s Squeeze multiple consecutive blank lines into one
-A Show non-printing characters and line ends (useful for spotting stray tabs or trailing whitespace)
-E Display $ at the end of each line

less options and keys

Flag / Key Meaning
-N Show line numbers in the left margin
-S Don’t wrap long lines; scroll sideways instead
-I Case-insensitive search
space / f Next screenful
b Previous screenful
/pattern Search forward for pattern
n / N Repeat last search forward / backward
g / G Jump to first / last line
q Quit

head and tail options

Flag Meaning
-n NUM Show NUM lines instead of the default 10
-c NUM Show NUM bytes instead of lines
-f (tail only) Follow the file, printing new lines as they’re appended
-F (tail only) Like -f, but also re-attaches if the file is renamed or rotated and recreated
-q Quiet: suppress filename headers when given multiple files

Examples

Example 1: Viewing a short file with cat

cat project-notes.txt

Output:

Learn Bash scripting basics.
Practice redirection and pipes daily.
Review the permissions chapter before the quiz.

cat opened the file, read it start to finish, and wrote every byte straight to your terminal. For a three-line file this is instant and perfectly readable — exactly the case cat is built for.

Example 2: Concatenating several files into one

cat intro.txt chapter1.txt chapter2.txt > book-draft.txt
cat book-draft.txt

Output:

Welcome to the guide.
Chapter 1: Getting started
Chapter 2: Advanced topics

The first command is where cat earns its name: it reads each of the three files in the order given and writes their combined bytes to book-draft.txt via the > redirection operator, which creates the file (or truncates it if it already existed). The second command then reads that new file back to confirm the merge worked.

Example 3: Paging through a large log with less

less /var/log/syslog

Output:

Aug  4 09:12:01 web01 systemd[1]: Starting Daily apt upgrade...
Aug  4 09:12:03 web01 kernel: [12345.678] eth0: link up
Aug  4 09:12:07 web01 sshd[2231]: Accepted publickey for deploy
...
/var/log/syslog lines 1-34/812 (press h for help or q to quit)

less fills the terminal with the first screenful of the file and leaves a status line at the bottom showing your position. Typing /error would jump to the next line containing "error", G would jump straight to the last line of the file, and q exits back to your shell. None of this requires less to have loaded the whole 812-line file into memory up front.

Example 4: Previewing both ends of a log with head and tail

head -n 5 /var/log/nginx/access.log
tail -n 5 /var/log/nginx/access.log

Output:

203.0.113.5 - - [04/Aug/2026:08:59:01 +0000] "GET / HTTP/1.1" 200 512
203.0.113.9 - - [04/Aug/2026:08:59:04 +0000] "GET /favicon.ico HTTP/1.1" 404 209
...
203.0.113.44 - - [04/Aug/2026:09:14:52 +0000] "POST /login HTTP/1.1" 200 348
203.0.113.2 - - [04/Aug/2026:09:14:59 +0000] "GET /dashboard HTTP/1.1" 200 4110

Instead of scrolling through a possibly enormous access log, head shows you the oldest 5 requests and tail shows you the 5 most recent ones — a fast way to sanity-check that logging is working and see what’s happening right now, without ever printing the lines in between.

Example 5: Watching a log file live with tail -f

tail -f /var/log/app.log

Output:

2026-08-04 09:20:11 INFO  worker started
2026-08-04 09:20:44 INFO  processed job 5521
2026-08-04 09:21:02 WARN  retrying job 5522 (attempt 2)
^C

tail -f prints the last 10 lines of app.log and then keeps running, printing each new line the instant your application writes it. The command never exits on its own; the ^C in the output represents pressing Ctrl+C to send it SIGINT and stop watching.

How It Works Step by Step

Walking through what happens when a Bash script or interactive session pages a log file with less, then tails it live, ties the pieces together:

  • The shell forks a child process and that child execs less, replacing itself with the less program image while keeping the same file descriptors (stdin/stdout/stderr still point at your terminal).
  • less opens the target file, queries the terminal’s row/column size, reads just enough bytes to fill one screen, and writes them — it is now waiting on a read() from your keyboard, not from the file.
  • Each keypress (space, b, /pattern) triggers another targeted read from the file at a new offset; less never needs to hold the entire file in memory, which is why it opens multi-gigabyte logs instantly.
  • When you switch to tail -f on the same file, tail first seeks near the end and prints the last N lines, then registers an inotify watch with the kernel on that file’s inode.
  • Every time your application appends new bytes, the kernel notifies tail immediately; tail reads only the newly written bytes and writes them to your terminal — it is event-driven, not constantly polling the disk.
  • Pressing Ctrl+C delivers SIGTERM’s interactive cousin SIGINT to the foreground process, which by default terminates it and returns control to your shell.

Common Mistakes

Mistake 1: Redirecting output back into an input file

cat notes1.txt notes2.txt > notes1.txt

Before cat ever runs, Bash sets up the > redirection by opening notes1.txt for writing, which truncates it to zero bytes immediately. By the time cat tries to read notes1.txt as one of its inputs, it’s already empty — you permanently lose the original content. Always redirect to a different file:

cat notes1.txt notes2.txt > combined-notes.txt

Mistake 2: Forgetting to quote a filename with spaces

cat my notes.txt

Bash splits unquoted text on whitespace before cat ever sees it, so this is passed as two arguments, my and notes.txt — and cat reports that it can’t find a file literally named my. Quote any filename that might contain spaces:

cat "my notes.txt"

Mistake 3: tail -f silently stops after log rotation

tail -f /var/log/app.log

This runs without any syntax error, but it has a real operational gotcha: if a tool like logrotate renames app.log to app.log.1 and creates a fresh, empty app.log in its place, your tail -f process is still watching the old (renamed) file’s descriptor. New log lines keep going into the new file while your terminal appears frozen. Use the capital-F variant, which detects the rename and reopens the new file by name:

tail -F /var/log/app.log

Mistake 4: Running cat on a binary file

cat /usr/bin/ls

Binary files contain raw byte sequences that aren’t printable text. Dumping them straight to your terminal can include escape sequences that change your terminal’s colors, font, or even make it stop echoing your typing — leaving the terminal looking "broken" (usually fixable by typing reset and pressing Enter). Check the file type first, then use less, which detects binary content and warns you instead of printing it raw:

file /usr/bin/ls
less /usr/bin/ls

Best Practices

  • Reach for less, not cat, for anything that might be longer than one screen — it’s just as easy to type and won’t flood your terminal.
  • Use head or tail to sample a huge file before deciding whether you need to look at all of it.
  • Prefer tail -F over tail -f when watching application or service logs that are managed by logrotate.
  • Avoid the "useless use of cat": cat file | grep pattern works, but grep pattern file does the same thing with one fewer process.
  • Quote every filename and variable that holds one ("$file"), especially in scripts, so spaces and glob characters don’t cause silent word-splitting.
  • Use cat -A when a file looks fine but a script that reads it misbehaves — it reveals hidden tabs, trailing spaces, and Windows-style line endings.
  • Double- and triple-check redirection targets before running a command — > destroys existing content instantly, with no confirmation prompt.

Practice Exercises

Exercise 1: Create a file named todo.txt containing three lines of your choosing, view it with cat, then use cat -n to view it again with line numbers. Notice the difference in the output.

Exercise 2: Open a long file on your system (try /var/log/syslog or /etc/services) with less. Practice jumping to the end with G, back to the start with g, and searching for a word with / followed by n to repeat the search. Quit with q.

Exercise 3: In one terminal, run tail -f on a file you create, such as ~/watch-test.log. In a second terminal, append a line to that same file every couple of seconds (a loop using echo "line" >> ~/watch-test.log works well — note the double >> so you append instead of truncating each time). Confirm each new line appears in the first terminal immediately, then stop the follow with Ctrl+C.

Summary

  • cat streams an entire file to stdout in one pass — great for short files and for piping, bad for huge ones.
  • less is a lazy pager that reads only what fits on screen, letting you scroll and search files far larger than your RAM.
  • head shows the start of a file and tail shows the end; both take -n to control how many lines.
  • tail -f follows a growing file in real time using the kernel’s inotify notifications; tail -F also survives log rotation.
  • > truncates its target before anything is read or written — never redirect a command’s output back into one of its own input files.
  • Always quote filenames and variables so spaces don’t get word-split into separate arguments.