Viewing and Comparing¶
Most operational text is logs and configuration files, read with head, tail, less and compared with diff. Knowing which tool shows hidden characters, follows a rotated log or verifies a checksum saves time in every incident.
Track: Core · Interview weight: Med
Must-Know Facts¶
| Fact | Value | Verify with |
|---|---|---|
| Page through a file | less (/ search, G end, F follow, q quit) | less /var/log/messages |
| First or last lines | head -n N, tail -n N | tail -n 50 file |
| Follow a log | tail -f follows the open file; tail -F follows the name across rotation | tail -F /var/log/app.log |
| Count | wc -l lines, -w words, -c bytes | wc -l < file |
| Hidden characters | cat -A shows ^M (CR), ^I (tab), $ (line end) | cat -A file |
| Byte view | od -c, xxd, hexdump -C | od -c file |
| Compare files | diff (exit 0 same, 1 different, 2 error); diff -u for patches | diff -u a b |
| Binary compare | cmp reports the first differing byte | cmp a b |
| Compressed logs | zcat, zless, zgrep read .gz without extracting | zgrep ERROR app.log.1.gz |
| Checksums | sha256sum; -c verifies a list | sha256sum -c SHA256SUMS |
| Base64 | Encoding, not encryption | base64 -d |
| Line ranges | sed -n '5,7p' file | sed -n '5,7p' |
Reading Files¶
The examples use a 17-line web access log with documentation IP addresses (192.0.2.0/24 and similar).
head -n 3 access.log
tail -n 2 access.log
wc access.log
wc -l < access.log
sed -n '5,7p' access.log | cut -c1-60
Output:
192.0.2.10 - - [16/Sep/2026:14:10:00 +0000] "GET /api/orders HTTP/1.1" 200 3354 "-" "curl/8.5.0" 1.237
192.0.2.10 - - [16/Sep/2026:14:10:07 +0000] "POST /login HTTP/1.1" 401 118 "-" "Mozilla/5.0" 0.041
192.0.2.10 - - [16/Sep/2026:14:10:14 +0000] "GET /login HTTP/1.1" 200 824 "-" "curl/8.5.0" 0.825
198.51.100.7 - - [16/Sep/2026:14:15:45 +0000] "GET /api/orders/42 HTTP/1.1" 500 441 "-" "curl/8.5.0" 1.828
203.0.113.99 - - [16/Sep/2026:14:15:52 +0000] "POST /login HTTP/1.1" 401 118 "-" "Mozilla/5.0" 0.037
17 221 1730 access.log
17
192.0.2.10 - - [16/Sep/2026:14:11:28 +0000] "GET / HTTP/1.1"
192.0.2.10 - - [16/Sep/2026:14:11:35 +0000] "GET /static/app
198.51.100.23 - - [16/Sep/2026:14:12:42 +0000] "GET /static/
wc -l < file prints the number without the file name, which is handier in scripts.
less key | Action |
|---|---|
/text, ?text | Search forward, backward |
n, N | Next, previous match |
g, G | Start, end of file |
F | Follow new lines (like tail -f); Ctrl+C stops following |
-S (option) | Do not wrap long lines |
-R (option) | Keep colors from grep --color=always |
Hidden Characters¶
A trailing space and a Windows line ending look like nothing in cat but break parsers:
printf 'key=value \r\n\tindented\n' > dos.txt
cat -A dos.txt
od -c dos.txt | head -2
Output:
key=value ^M$
^Iindented$
0000000 k e y = v a l u e \r \n \t i n d
0000020 e n t e d \n
^M is the carriage return (\r) before the line end. cat -n numbers lines; nl numbers only non-empty ones.
Following Logs Through Rotation¶
tail -f keeps reading the file it opened. When logrotate renames the file, tail -f stays on the old one; tail -F reopens the name.
touch /tmp/app.log
(for i in 1 2 3; do sleep 1; echo "line $i" >> /tmp/app.log; done; mv /tmp/app.log /tmp/app.log.1; echo "after rotate" >> /tmp/app.log) &
timeout 7 tail -F /tmp/app.log
Output:
line 1
line 2
line 3
tail: '/tmp/app.log' has become inaccessible: No such file or directory
tail: '/tmp/app.log' has appeared; following new file
after rotate
Use tail -F or journalctl -f for long watches
During a deployment, tail -f on a log that rotates at midnight silently stops showing new lines. tail -F follows the new file; for services that log to the journal, journalctl -fu <unit> avoids the question.
Comparing Files¶
diff app-v1.conf app-v2.conf
echo "rc=$?"
diff -u app-v1.conf app-v2.conf
Output:
2c2
< listen_port = 8080
---
> listen_port = 8443
4c4
< db_pool = 10
---
> db_pool = 20
7c7,8
< feature_x = off
---
> feature_x = on
> tls = enabled
rc=1
--- app-v1.conf 2026-09-16 14:21:05.719410976 +0000
+++ app-v2.conf 2026-09-16 14:21:10.005581444 +0000
@@ -1,7 +1,8 @@
# app configuration
-listen_port = 8080
+listen_port = 8443
db_host = db01.internal
-db_pool = 10
+db_pool = 20
log_level = info
#debug = true
-feature_x = off
+feature_x = on
+tls = enabled
In the default format, 2c2 means "line 2 changed into line 2"; a is added and d deleted. The unified format (-u) is what git diff and patch use.
cmp app-v1.conf app-v2.conf
echo "rc=$?"
diff -q app-v1.conf app-v1.conf; echo "rc=$?"
diff -y -W 60 app-v1.conf app-v2.conf
Output:
app-v1.conf app-v2.conf differ: byte 36, line 2
rc=1
rc=0
# app configuration # app configuration
listen_port = 8080 | listen_port = 8443
db_host = db01.internal db_host = db01.internal
db_pool = 10 | db_pool = 20
log_level = info log_level = info
#debug = true #debug = true
feature_x = off | feature_x = on
> tls = enabled
| Tool | Use |
|---|---|
diff -u a b | Review or patch text changes |
diff -r dir1 dir2 | Compare directory trees |
diff -y / sdiff | Side by side |
cmp | Binary files; first difference only |
diff <(ssh h1 cat f) <(ssh h2 cat f) | Compare a file across two hosts |
vimdiff a b | Interactive merge |
Compressed Logs, Checksums and Base64¶
gzip -k access.log
zcat access.log.gz | head -1
zgrep -c ' 500 ' access.log.gz
sha256sum app-v1.conf app-v2.conf > SHA256SUMS
sha256sum -c SHA256SUMS
echo "tampered" >> app-v2.conf
sha256sum -c SHA256SUMS; echo "rc=$?"
echo -n 'admin:s3cret' | base64
echo 'YWRtaW46czNjcmV0' | base64 -d; echo
Output:
192.0.2.10 - - [16/Sep/2026:14:10:00 +0000] "GET /api/orders HTTP/1.1" 200 3354 "-" "curl/8.5.0" 1.237
4
app-v1.conf: OK
app-v2.conf: OK
app-v1.conf: OK
app-v2.conf: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
rc=1
YWRtaW46czNjcmV0
admin:s3cret
Base64 is not encryption
Kubernetes Secrets, HTTP basic authentication headers and many config files store base64 text, which anyone can decode. echo -n matters: without it the newline is encoded too, and the credential no longer matches.
Common Errors¶
sha256sum: WARNING: 1 computed checksum did NOT match¶
Cause: a file changed after the checksum list was made, or a download is corrupt.
Fix: download again; if it still fails, do not use the file.
gzip: plain.gz: not in gzip format¶
Cause: zcat was given a file that is not gzip data, whatever its name, or one compressed with another tool. zgrep reads plain files as well, so it does not fail the same way.
Fix: file <path>; use xzcat, bzcat or zstdcat to match.
Interview Checkpoints¶
L1: What is the difference between tail -f and tail -F?
Say first: -f follows the file it opened; -F follows the name, so it reopens the log after rotation.
Proof: after mv app.log app.log.1, tail -F prints "has appeared; following new file".
Follow-up: Which rotation method keeps tail -f working? (copytruncate.)
L2: A config file parses on one server and fails on another. Show the invisible difference.
Say first: compare with hidden characters visible.
Proof: diff <(cat -A a.conf) <(cat -A b.conf) shows ^M$ or trailing spaces on one side.
Follow-up: How do you strip the carriage returns?
L2: Verify a downloaded release against its published checksum.
Say first: compute the hash and compare, or use -c with the checksum file.
Proof: sha256sum -c tool_SHA256SUMS --ignore-missing
Follow-up: What does a checksum not protect against? (A compromised download site; signatures do.)
L2: Count 500 errors in today's and yesterday's rotated, compressed logs.
Say first: use the z variants so no extraction is needed.
Proof: zgrep -c ' 500 ' access.log.1.gz; grep -c ' 500 ' access.log
Follow-up: How would you combine both into one count?
L2: Produce a patch of your changes to a config file.
Say first: unified diff against the original.
Proof: diff -u nginx.conf.orig nginx.conf > nginx.patch; apply elsewhere with patch -p0 < nginx.patch.
Follow-up: What exit status does diff return when files differ, and why does that matter under set -e?
L3: During a deployment, tail -f on the application log stopped showing new lines.
Say first: check whether the log was rotated or recreated.
Proof: ls -li app.log* shows a new inode for app.log; lsof -p $(pgrep -n tail) shows tail still holding the old file.
Follow-up: How do you avoid it next time?
Related¶
- grep and Regex: searching instead of reading
- Streams and Redirection: process substitution in
diff <(...) - Inodes and Links: why
tail -fkeeps the old file
Captured on Rocky Linux 10.2 and Ubuntu 24.04.4 LTS (iximiuz Labs microVMs, kernel 6.1.167), 2026-09.