5 Reading and Inspecting Files Without an Editor
Opening a file in an editor feels natural. It’s what most of us learned first, and for writing code it’s usually the right call. But for reading — for inspecting, skimming, monitoring, and understanding files — an editor is often the wrong tool. It’s slow to open, it loads the entire file into memory, and it gives you no way to compose what you’re seeing with other commands.
The terminal gives you a different set of primitives for reading files. Some are faster than opening an editor. Some can handle files so large that an editor would choke on them. Some let you watch a file change in real time. And all of them can be connected to the search and processing tools from the previous chapters, turning a simple file read into the first step of a more powerful pipeline.
This chapter covers the tools you’ll use to read and inspect files at the command line — and, more importantly, when to reach for each one.
5.1 cat: Quick Reads and Concatenation
cat is the simplest file-reading tool. It reads a file and prints its contents to standard output — nothing more.
cat src/index.tsFor short files — configuration files, small scripts, environment templates — cat is the fastest way to see the contents without leaving the terminal. It’s also the natural starting point for pipelines: read a file with cat, then pipe the output into grep, jq, awk, or whatever processing you need.
cat access.log | grep "ERROR" | wc -l # count error lines in a log
cat package.json | jq '.scripts' # extract npm scripts5.1.1 Line numbers
When you need to reference specific lines — in a bug report, a code review comment, or a debugging session — the -n flag adds line numbers:
cat -n src/auth.ts5.1.2 Showing invisible characters
Two flags that are surprisingly useful when debugging whitespace issues:
cat -A src/config.yaml # show all non-printing characters
cat -e src/config.yaml # show line endings ($ marks end of each line)If a config file is behaving unexpectedly and you suspect Windows-style line endings (\r\n instead of \n), cat -e will show you the stray ^M characters immediately. This is a small thing that saves a disproportionate amount of debugging time.
5.1.3 When not to use cat
cat reads the entire file before printing anything. For files that are hundreds of megabytes — large log files, database dumps, data exports — this is the wrong tool. Use less (covered next) or tail instead. A file that takes thirty seconds to cat takes milliseconds to inspect with head.
5.3 head and tail: Reading the Edges of Files
Most of the time, you don’t need to read an entire file. You need the beginning, or the end. head and tail give you exactly that, without touching the rest of the file.
5.3.1 head
head src/index.ts # first 10 lines (default)
head -n 50 src/index.ts # first 50 lines
head -n -20 src/index.ts # everything except the last 20 lineshead is most useful for checking file headers, understanding file format before processing it, or quickly confirming you have the right file. It’s also the right tool when you want to preview the beginning of a large data file without reading the whole thing:
head -n 5 data/users.csv # preview the first 5 rows of a CSV5.3.2 tail
tail logs/app.log # last 10 lines (default)
tail -n 100 logs/app.log # last 100 lines
tail -n +50 logs/app.log # everything from line 50 onwardsThe + syntax on -n is subtle but powerful: tail -n +50 means “start from line 50” rather than “show the last 50 lines.” This lets you skip a file header and process everything after it:
tail -n +2 data/users.csv | wc -l # count rows, excluding header5.3.3 tail -f: Following a live file
The -f flag is one of the most-used developer commands in existence:
tail -f logs/application.logThis keeps the file open and prints new lines as they’re written — essential for watching log output from a running service. Hit Ctrl+C to stop.
Following multiple files at once:
tail -f logs/app.log logs/error.log logs/access.logtail will label each line with the filename it came from, so you can monitor several log streams simultaneously.
Combining tail -f with grep to filter the live stream:
tail -f logs/application.log | grep "payment-service"
tail -f logs/application.log | grep -E "ERROR|WARN"This is the pattern you’ll use constantly when debugging a running service: watch only the log lines you care about, filtered in real time.
5.3.4 Watching for a specific event
A more advanced pattern — wait until a specific string appears in a log file, then stop:
tail -f logs/app.log | grep -m 1 "Server started"The -m 1 flag on grep stops after the first match. This is useful in shell scripts that need to wait for a service to finish starting before proceeding.
5.4 wc: Counting Lines, Words, and Characters
wc (word count) is a small tool with a surprisingly wide range of uses in development workflows.
wc -l src/index.ts # number of lines
wc -w src/index.ts # number of words
wc -c src/index.ts # number of bytes
wc -m src/index.ts # number of charactersIn practice, -l is the flag you’ll use almost exclusively — line counts are the relevant metric for source files and logs.
5.4.1 Counting across multiple files
wc -l src/*.ts # line count for each TypeScript file, plus totalThe output includes a per-file count and a total at the bottom — a quick way to get a rough sense of the size of different parts of a codebase.
5.4.2 Combining with find for a codebase summary
find . -name "*.ts" -not -path "*/node_modules/*" | xargs wc -l | sort -rn | head -20This finds all TypeScript files, counts their lines, sorts by line count in descending order, and shows the top 20 largest files. It’s a useful way to identify the most complex parts of a codebase when you’re getting oriented — the largest files are often the ones that have accumulated the most technical debt.
5.4.3 Counting search results
One of the most common uses of wc -l is counting the results of another command:
rg "TODO" -l | wc -l # how many files have TODOs
git log --oneline | wc -l # how many commits in this repo
cat access.log | grep "500" | wc -l # how many 500 errors in the logThe pattern command | wc -l is so common it becomes muscle memory.
5.5 bat: A Better cat
bat is a modern replacement for cat that adds syntax highlighting, line numbers, and Git change indicators — all with sensible defaults that don’t get in the way.
brew install bat # macOS
apt install bat # Ubuntu (may be installed as batcat)bat src/auth.ts # syntax-highlighted file viewThe output is color-coded by language, with line numbers on the left and a subtle header showing the filename. For reading source code at the terminal, it’s dramatically more readable than plain cat.
5.5.1 Git integration
One of bat’s best features is that it shows Git change indicators in the gutter — a + for added lines, ~ for modified lines — so you can see what’s changed in the current working tree without running git diff:
bat src/auth.ts # modified lines are highlighted in the gutter5.5.2 Useful flags
bat -n src/auth.ts # line numbers only, no other decorations
bat -p src/auth.ts # plain output (no decorations, like cat)
bat -A src/auth.ts # show non-printing characters (like cat -A)
bat --line-range 50:100 src/auth.ts # show only lines 50-100The --line-range flag is particularly useful — instead of opening a file in an editor just to look at a specific section, you can read exactly the lines you need.
5.5.3 Using bat in pipelines
When used in a pipeline, bat automatically disables its decorations and behaves like cat:
bat src/auth.ts | grep "export" # works exactly like cat | grepThis means you can alias cat to bat without breaking any pipelines — something many developers do in their shell configuration.
5.6 Reading Compressed and Binary Files
Not every file you need to inspect is a plain text file. A few tools handle the common cases.
5.6.1 Compressed files
Log files and data exports are often compressed with gzip. Rather than decompressing the file to read it, you can use the z-prefixed variants of common tools:
zcat logs/archive.log.gz # like cat, for gzip files
zless logs/archive.log.gz # like less, for gzip files
zgrep "ERROR" logs/archive.log.gz # like grep, for gzip filesThis means you can grep through months of archived logs without decompressing them first — a significant time and disk space saving when you’re searching historical data.
5.6.2 Binary files
For binary files — compiled executables, data files, unknown blobs — xxd produces a hex dump that lets you inspect the raw bytes:
xxd some-binary | head -2000000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 1011 0000 0000 0000 ..>.............
The left column is the byte offset, the middle is the hex representation, and the right is the ASCII interpretation (with . for non-printable bytes). For ELF binaries, the ELF signature is visible immediately — confirming what file told you in Chapter 1.
For a more readable inspection of what strings are embedded in a binary:
strings some-binary | grep -i "version"
strings some-binary | grep -i "error"strings extracts printable character sequences from a binary file. It’s useful for getting a quick sense of what a compiled binary does, what error messages it might produce, or what version it is.
5.7 Comparing Files with diff
Reading files often leads to a related question: how does this file differ from another version? diff is the classic tool for this.
diff file-a.ts file-b.tsThe output shows lines that differ between the two files, with < for lines only in the first file and > for lines only in the second.
5.7.1 Unified diff format
The default output is functional but hard to read quickly. The unified format — which is what Git uses — is much clearer:
diff -u file-a.ts file-b.ts file-a.ts 2024-03-11 17:43:09
+++ file-b.ts 2024-03-12 09:14:22
@@ -12,7 +12,7 @@
function handleAuth(req, res) {
- const token = req.headers.authorization;
+ const token = req.headers['authorization'];
if (!token) {
Lines prefixed with - were removed, lines with + were added, and lines with neither are context.
5.7.2 Comparing directories
diff -r src/ src-backup/ # recursive directory comparison
diff -rq src/ src-backup/ # just show which files differThe -q flag (quiet) only reports which files differ, not the actual differences — useful when you just need a list of changed files between two directory trees.
5.7.3 A practical pattern: comparing config environments
diff -u config/development.yaml config/production.yamlA quick way to audit the differences between environment configs — something that’s useful before a deployment and surprisingly easy to overlook.
5.8 Putting It Together: A Real Workflow
Here’s how these tools work together in a realistic debugging scenario. Imagine a service is throwing errors intermittently and you need to investigate.
# How big is the log file? Is it worth opening in less?
wc -l logs/application.log
# Get a quick look at the most recent entries
tail -n 50 logs/application.log
# Watch the live log stream, filtered to errors only
tail -f logs/application.log | grep -E "ERROR|WARN"
# How many errors happened in the last hour?
# (assuming logs have timestamps in ISO format)
grep "$(date -u +%Y-%m-%dT%H)" logs/application.log | grep "ERROR" | wc -l
# Find the first occurrence of the error to understand when it started
grep -n "NullPointerException" logs/application.log | head -1
# Read the surrounding context at that line number
# (if the first occurrence was at line 4821)
tail -n +4810 logs/application.log | head -n 30
# Check if the error is also in yesterday's archived log
zgrep "NullPointerException" logs/application.log.1.gz | wc -lEach of these commands takes a second or two to run. Together, they give you a detailed picture of the error — when it started, how often it occurs, what the surrounding context looks like — without ever opening a log file in an editor or downloading it to your local machine.
5.9 Chapter Summary
The tools in this chapter — cat, less, head, tail, wc, bat, diff, and the z-prefixed variants — give you a complete toolkit for reading and inspecting files at the terminal. The key is knowing which tool fits which situation.
The key habits to build:
- Use
catfor short files and as the start of pipelines; uselessfor anything you need to scroll through - Keep
tail -f | grep "pattern"in your muscle memory — it’s the fastest way to monitor a live service - Use
wc -las a quick sanity check on results before acting on them - Install
batand consider aliasingcatto it — the syntax highlighting pays for itself immediately - Use
tail -n +2to skip headers when processing structured files like CSVs - Reach for
zcatandzgrepbefore decompressing archived logs
5.10 Exercises
1. Find the largest source file in a codebase using find, xargs, and wc -l. Then use bat --line-range to read just the first 30 lines of it.
2. Start a process that writes to a log file (any web server or background process will do). Use tail -f | grep to watch its output filtered to a specific pattern in real time.
3. Take two config files that differ slightly — for example, a development and production environment config. Use diff -u to produce a clean summary of the differences.
4. Find a gzip-compressed file on your system (check /var/log on Linux, or create one with gzip). Use zless and zgrep to inspect and search it without decompressing it.
5. Use the tail -n +2 pattern to skip the header row of a CSV file and pipe the result into wc -l to get an accurate row count.
5.11 Quick Reference
| Command | What it does |
|---|---|
cat file |
Print file contents to stdout |
cat -n file |
Print with line numbers |
cat -A file |
Show non-printing characters |
less file |
Interactively browse a file |
less -F file |
Follow file (like tail -f, but scrollable) |
less +G file |
Open at end of file |
head -n 50 file |
First 50 lines |
tail -n 100 file |
Last 100 lines |
tail -n +2 file |
Everything from line 2 onwards |
tail -f file |
Follow file in real time |
tail -f file \| grep "pattern" |
Follow and filter live output |
wc -l file |
Count lines |
bat file |
Syntax-highlighted file view |
bat --line-range 50:100 file |
View specific line range |
diff -u file-a file-b |
Unified diff between two files |
diff -rq dir-a/ dir-b/ |
Which files differ between two directories |
zcat file.gz |
cat for gzip-compressed files |
zgrep "pattern" file.gz |
grep for gzip-compressed files |
xxd file \| head |
Hex dump of a binary file |
strings file |
Extract printable strings from a binary |