BEGINFILE/ENDFILE vs. FNR==1: Choosing Per‑File State Logic in Awk
0 reputation · 28 May 2026, 06:34 UTC
0 reputation · 28 May 2026, 06:34 UTC
Implement per‑file counters and summaries in an awk script that processes many input files.
The script must run on a variety of Unix-like systems where the default awk may be POSIX awk, mawk, BusyBox awk, or gawk. The choice of per‑file logic affects portability, readability, and error handling.
While the FNR==1 idiom works everywhere, it requires explicit checks and can be error‑prone with empty or unreadable files. The gawk extensions BEGINFILE and ENDFILE provide cleaner hooks but lock the script to gawk and have subtle semantics for empty or failed files.
awk is gawk, or could it be a POSIX or minimal awk?FNR==1 idiom acceptable compared to the clarity of BEGINFILE/ENDFILE?32775 reputation · 28 May 2026, 13:39 UTC
Use BEGINFILE/ENDFILE only if you can guarantee gawk on every target system. If awk might be POSIX awk, mawk, or BusyBox awk, use the FNR==1 idiom — it is the only option that works everywhere. The performance difference is negligible; this is purely a portability and error-handling decision.
BEGINFILE and ENDFILE are gawk extensions (since gawk 4.0). They do not exist in POSIX awk, mawk, or BusyBox awk — a script using them fails with a syntax error there.FNR resets to zero at the start of each input file in every standard awk, so FNR==1 is fully portable.FNR never reaches 1, so FNR==1 never fires — per-file initialization is silently skipped unless you add explicit handling.BEGINFILE/ENDFILE with ERRNO set, giving you a hook to report the failure. POSIX-style awks typically just skip the unreadable file with no per-file hook at all.If the deployment environment pins gawk (for example, a controlled container image), the extension version is clearer and safer:
BEGINFILE {
if (ERRNO != "") {
printf "skipping %s: %s\n", FILENAME, ERRNO > "/dev/stderr"
nextfile
}
count = 0
}
{ count++ }
ENDFILE { printf "%s: %d records\n", FILENAME, count }
You get initialization, teardown, and open-failure reporting without extra bookkeeping.
If the script must run anywhere, use FNR==1 and emulate the teardown in END or by tracking filename changes:
FNR == 1 {
if (NR != 1) report(prev, count) # close out previous file
prev = FILENAME; count = 0
}
{ count++ }
END { if (NR > 0) report(prev, count) }
function report(f, c) { printf "%s: %d records\n", f, c }
The costs: empty files produce no report at all (add a record-count check if that matters), and unreadable files are skipped silently unless you wrap the awk call in shell logic that checks exit status or pre-validates the file list.
Do not mix both mechanisms in one script. Under gawk, BEGINFILE and FNR==1 both fire for the first record of each file, causing double initialization or double counting.
Build a test set with a normal file, an empty file, and a file with read permission removed, then run the script under each awk you might encounter:
mkdir t && printf 'a\nb\n' > t/ok.txt && : > t/empty.txt \
&& printf 'x\n' > t/denied.txt && chmod 000 t/denied.txt
for a in gawk mawk busybox\ awk; do
echo "== $a =="; $a -f script.awk t/*.txt
done
Confirm: no syntax errors under any variant, the empty file behaves as intended, and the unreadable file is either reported (gawk + ERRNO) or acceptably skipped.
The deciding question is the first one in your list: is gawk guaranteed? If yes, take the cleaner hooks and free error reporting. If no — or if you cannot answer it with certainty — accept the extra verbosity of FNR==1 plus explicit empty-file handling. Maintenance overhead is a one-time cost; a syntax error on a production minimal system is not.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.