Skip to content
VLSI Mentor

awk Part IV · Module 14 · AWK Debugging, Review, and Engineering Mastery

Debugging Incorrect or Empty Output

awk printed nothing, or printed the wrong thing, and there is an order in which to ask why — because the five common causes have very different fixes and four of them look identical from the output. This chapter establishes that order and gives each cause a test that distinguishes it, including the one a careful engineer is most likely to miss: a carriage return at the end of every line, which breaks every anchored pattern and which a bracketed dump of the raw record does not reveal.

Intermediate18 min readawkPart IVDebuggingChecklistCRLF

Part IV · Module 14 · Chapter 14.1 · Debugging Incorrect or Empty Output

1. Why the Order Matters

Empty output has no diagnostic content. A pattern that never matched, a field index past NF, a program the shell rewrote and a file that does not exist all produce the same nothing — so guessing picks one of four with equal odds and the wrong guess costs a round of edits.

The order below is not arbitrary. Each step either explains the symptom or rules out a whole class of cause, and the steps are ordered so that the cheapest and most decisive come first.

2. The Fixtures

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'Startpoint: u_alu/sum_reg[0]\r\n  slack (VIOLATED)   -0.018\r\n' > dos.rpt
printf 'name\tcells\tarea\nu_alu\t178\t1310.000\nu_ctrl\t170\t1240.000\n' > tabs.tsv
printf 'u_alu     178    1310.000\nu_ctrl    170    1240.000\n' > aligned.txt
: > empty.txt
wc -c dos.rpt tabs.tsv aligned.txt empty.txt | awk 'NF == 2 && $2 != "total" { printf "  %-12s %d byte(s)\n", $2, $1 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  dos.rpt      59 byte(s)
  tabs.tsv     55 byte(s)
  aligned.txt  52 byte(s)
  empty.txt    0 byte(s)

A CRLF report, a tab-separated file, a space-aligned file, and an empty one.

3. Step One: NR, NF, and the Raw Record

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for f in tabs.tsv aligned.txt dos.rpt empty.txt; do
  printf "  --- %s ---\n" "$f"
  awk '{ printf "    NR=%d NF=%d [%s]\n", NR, NF, $0 }' "$f" | head -2
done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  --- tabs.tsv ---
    NR=1 NF=3 [name	cells	area]
    NR=2 NF=3 [u_alu	178	1310.000]
  --- aligned.txt ---
    NR=1 NF=3 [u_alu     178    1310.000]
    NR=2 NF=3 [u_ctrl    170    1240.000]
  --- dos.rpt ---
    NR=1 NF=2 [Startpoint: u_alu/sum_reg[0]
]
    NR=2 NF=3 [  slack (VIOLATED)   -0.018
]
  --- empty.txt ---

This one command answers three questions at once:

  • Did any record arrive? empty.txt produced no output at all, which no pattern or field problem can do.
  • How many fields does awk see? Three on the first three files — so $4 and beyond are empty and $3 is the last real one.
  • What is actually in the record? The brackets delimit it, so leading and trailing spaces are visible.

Run it first, every time. It costs one command and eliminates more than half the possibilities.

4. Step Two: Is the Pattern Matching?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ n = length($0); last = substr($0, n); printf "  NR=%d length=%d last byte=%d%s\n", NR, n, index("\r", last) ? 13 : 0, (index("\r", last) ? "  <- CARRIAGE RETURN" : "") }' dos.rpt
cat -e dos.rpt | sed 's/^/    /'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  NR=1 length=29 last byte=13  <- CARRIAGE RETURN
  NR=2 length=28 last byte=13  <- CARRIAGE RETURN
    Startpoint: u_alu/sum_reg[0]^M$
      slack (VIOLATED)   -0.018^M$

And the consequence:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /VIOLATED\$/ matches : "; awk '/VIOLATED$/ { n++ } END { print n+0 }' dos.rpt
printf "  /VIOLATED/  matches : "; awk '/VIOLATED/ { n++ } END { print n+0 }' dos.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /VIOLATED$/ matches : 0
  /VIOLATED/  matches : 1

Zero against one. $ anchors to the end of the record, and the last character is the carriage return — so every anchored pattern on a CRLF file fails.

The general technique, and the reason this is step two: widen the pattern until it matches, then narrow it back one change at a time. Dropping the $ changed 0 to 1, which localises the fault to the anchor in a single step. Chapter 14.4 covers the sub(/\r$/, "") fix and the other malformed-input cases.

5. Step Three: Is FS Right?

NF from step one already answers this, which is why it comes third rather than first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  default FS on tabs.tsv : NF=%s  \$2=[%s]\n" "$(awk 'NR==2 { print NF }' tabs.tsv)" "$(awk 'NR==2 { print $2 }' tabs.tsv)"
printf "  -F'\\t' on tabs.tsv     : NF=%s  \$2=[%s]\n" "$(awk -F'\t' 'NR==2 { print NF }' tabs.tsv)" "$(awk -F'\t' 'NR==2 { print $2 }' tabs.tsv)"
printf "  -F'\\t' on aligned.txt  : NF=%s  \$2=[%s]\n" "$(awk -F'\t' 'NR==1 { print NF }' aligned.txt)" "$(awk -F'\t' 'NR==1 { print $2 }' aligned.txt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  default FS on tabs.tsv : NF=3  $2=[178]
  -F'	' on tabs.tsv     : NF=3  $2=[178]
  -F'	' on aligned.txt  : NF=1  $2=[]

Three results, and the asymmetry is the useful part:

  • The default FS handles a tab-separated file, because a tab is whitespace. Both forms give NF=3.
  • Forcing -F'\t' on a space-aligned file gives NF=1 and every field reference past $1 is empty.

So the dangerous direction is adding -F'\t' to a file that only looked tab-separated — which Chapter 14.2 takes further, because a column that lines up on screen says nothing about what separates it.

NF=1 on a file with visible columns is the signature, and it is diagnostic on its own.

6. Step Four: Did the Shell Change the Program?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  single-quoted  : "; awk '{ print $1 }' aligned.txt | head -1
printf "  double-quoted  : "; awk "{ print $1 }" aligned.txt | head -1
printf "  the program the shell built: [%s]\n" "$(echo "{ print $1 }")"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  single-quoted  : u_alu
  double-quoted  : u_alu     178    1310.000
  the program the shell built: [{ print  }]

The double-quoted form prints the whole line, because the shell expanded $1 to nothing and print with no argument prints $0. Chapter 12.1 section 3 measured this; here it is in the checklist because the symptom — too much output rather than none — sends people looking at the pattern.

The test is to echo the program. echo "{ print $1 }" shows exactly what awk received, and if it differs from what you typed, nothing else about the program matters yet.

7. Step Five: Is the Action Attached to the Pattern?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /u_alu/ { n++ } END { print n+0 }        -> "; awk '/u_alu/ { n++ } END { print n+0 }' aligned.txt
printf "  /u_alu/; { n++ } END { print n+0 }       -> "; awk '/u_alu/; { n++ } END { print n+0 }' aligned.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /u_alu/ { n++ } END { print n+0 }        -> 1
  /u_alu/; { n++ } END { print n+0 }       -> u_alu     178    1310.000
2

One against two, from a single semicolon — and the second form also printed the matching record, which is the other half of the tell.

/u_alu/; is a complete rule — a pattern with no action, which prints matching records. The { n++ } that follows is then a separate rule with no pattern, so it fires on every record, and n counts the file rather than the matches.

The symptom is a count that is too high and looks plausible, which is why this is a checklist item rather than a footnote. The test is to count what the file has: if n equals NR, the action is unconditional.

8. Step Six: Empty or Unreadable?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for f in empty.txt no-such.txt aligned.txt; do
  printf "  %-14s " "$f"
  awk 'END { printf "NR=%d\n", NR }' "$f" 2>/dev/null || printf "awk exit %d (cannot read)\n" $?
done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  empty.txt      NR=0
  no-such.txt    awk exit 2 (cannot read)
  aligned.txt    NR=2

Three outcomes: a readable empty file gives NR=0 and exit 0; an unreadable one gives exit 2 and no NR at all. Chapter 12.6 measured why these must not be conflated — getline's return values distinguish them and a boolean test gets both wrong.

This step is last in the written order and first in the actual order, because step one already performs it: no output from the NR/NF probe means either an empty file or an unreadable one, and the exit status says which.

9. The Checklist

Six-step diagnostic order for empty or wrong awk output: print NR NF and the raw record, check for a carriage return with length or cat -e, widen the pattern until it matches, check NF against the expected field count, echo the program to see what the shell built, and check whether the action is attached to the pattern.01PRINT NR, NF, [$0]One command: did records arrive, how many fields, what is in them.02CHECK FOR A CARRIAGE RETURNlength() or cat -e. A bracketed dump cannot show it, and it breaksevery anchor.03WIDEN THE PATTERNRemove anchors and classes until it matches, then narrow back onechange at a time.04COMPARE NF WITH WHAT YOU EXPECTNF=1 on a file with visible columns means FS is wrong.05ECHO THE PROGRAMDouble quotes let the shell rewrite it into a different workingprogram.06CHECK THE ACTION IS ATTACHEDA stray semicolon makes the action unconditional; n equal to NR is thetell.
Figure 1 — The order to ask. Each step either explains the symptom or eliminates a class of cause, and the first step answers three questions for the price of one command.

10. All Five Causes, Side by Side

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  %-34s %s\n" "cause" "distinguishing test"
printf "  %-34s %s\n" "----------------------------------" "-------------------"
printf "  %-34s %s\n" "file empty or unreadable" "no output from the NR/NF probe; exit 2 if unreadable"
printf "  %-34s %s\n" "carriage return at line end" "length() one more than visible; cat -e shows ^M"
printf "  %-34s %s\n" "pattern does not match" "widening it changes the count"
printf "  %-34s %s\n" "FS wrong" "NF=1 on a file with visible columns"
printf "  %-34s %s\n" "shell rewrote the program" "echo the program string"
printf "  %-34s %s\n" "action not attached to a pattern" "the count equals NR"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cause                              distinguishing test
  ---------------------------------- -------------------
  file empty or unreadable           no output from the NR/NF probe; exit 2 if unreadable
  carriage return at line end        length() one more than visible; cat -e shows ^M
  pattern does not match             widening it changes the count
  FS wrong                           NF=1 on a file with visible columns
  shell rewrote the program          echo the program string
  action not attached to a pattern   the count equals NR

11. Common Mistakes

Guessing at the pattern first. Four causes look identical from the output — section 1.

Trusting a bracketed dump of $0. A carriage return leaves no visible mark.

Assuming no output means no match. An empty file and an unreadable one produce the same nothing.

Adding -F'\t' because the columns line up. On a space-aligned file that gives NF=1.

Double-quoting the program. The shell rewrites it into a different working program.

A semicolon after a pattern. The following action becomes unconditional.

Narrowing a pattern to find a bug. Widen it until it matches, then narrow one change at a time.

Reading $? after a pipe. | head replaces awk's status — Chapter 12.1 section 7.

12. Exercises

Exercise 1 — The first command

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "NR=%d NF=%d [%s]\n", NR, NF, $0 }' aligned.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NR=1 NF=3 [u_alu     178    1310.000]
NR=2 NF=3 [u_ctrl    170    1240.000]

Exercise 2 — Find the carriage return

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ printf "  NR=%d length=%d\n", NR, length($0) }' dos.rpt
cat -e dos.rpt | sed 's/^/  /'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  NR=1 length=29
  NR=2 length=28
  Startpoint: u_alu/sum_reg[0]^M$
    slack (VIOLATED)   -0.018^M$

^M before each $. The length includes it.

Exercise 3 — Debug an anchored pattern that matches nothing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /VIOLATED\$/   : %s\n" "$(awk '/VIOLATED$/ { n++ } END { print n+0 }' dos.rpt)"
printf "  /VIOLATED/    : %s\n" "$(awk '/VIOLATED/ { n++ } END { print n+0 }' dos.rpt)"
printf "  /sum_reg[0]\$/ without strip : %s\n" "$(awk '/sum_reg\[0\]$/ { n++ } END { print n+0 }' dos.rpt)"
printf "  /sum_reg[0]\$/ after strip   : %s\n" "$(awk '{ sub(/\r$/, "") } /sum_reg\[0\]$/ { n++ } END { print n+0 }' dos.rpt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /VIOLATED$/   : 0
  /VIOLATED/    : 1
  /sum_reg[0]$/ without strip : 0
  /sum_reg[0]$/ after strip   : 1

Widening the pattern changed 0 to 1, which localised the fault to the anchor. The last two lines are the same anchored pattern with and without sub(/\r$/, "") — 0 then 1 — which is the fix confirmed rather than assumed.

The anchor has to match what is really at the end of the record, which is worth checking rather than assuming:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '{ sub(/\r$/, ""); printf "  NR=%d ends with [%s]\n", NR, substr($0, length($0)-7) }' dos.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  NR=1 ends with [m_reg[0]]
  NR=2 ends with [  -0.018]

So /VIOLATED$/ could never work on the second record even with the CR gone — that record ends with the slack value.

Exercise 4 — Debug a count that equals the record count

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  records in the file            : %s\n" "$(awk 'END { print NR }' aligned.txt)"
printf "  /u_alu/ { n++ }                : %s\n" "$(awk '/u_alu/ { n++ } END { print n+0 }' aligned.txt)"
printf "  /u_alu/; { n++ }               : %s\n" "$(awk '/u_alu/; { n++ } END { print n+0 }' aligned.txt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  records in the file            : 2
  /u_alu/ { n++ }                : 1
  /u_alu/; { n++ }               : u_alu     178    1310.000
2

The third line shows both symptoms at once: the matching record was printed (a pattern with no action prints $0), and the count that follows is 2, equal to NR. Either one alone identifies it.

Exercise 5 — Engineering application

A reusable first-look script, so the first step is one command rather than one typed command.

Azvya Education Pvt. Ltd.VLSI Mentor
look.sh — synthetic teaching script.
#!/bin/sh
# look.sh — the first thing to run when awk prints nothing or the wrong thing.
#
# Usage:  sh look.sh <file> [n]
#
# Answers, in one pass: can the file be read, how many records and fields,
# are there carriage returns, and what do the first n records actually hold.

f=$1
n=${2:-3}
[ -n "$f" ] || { echo "usage: look.sh <file> [n]" >&2; exit 2; }

if [ ! -r "$f" ]; then echo "UNREADABLE: $f" >&2; exit 2; fi

awk -v n="$n" '
    { if (length($0) > 0 && substr($0, length($0)) == "\r") cr++
      nf[NF]++
      if (NR <= n) printf "  NR=%d NF=%d len=%d [%s]\n", NR, NF, length($0), $0 }
    END {
        if (NR == 0) { print "  EMPTY: 0 record(s) -- the file is readable and has no content"; exit 0 }
        printf "  %d record(s)\n", NR
        printf "  field counts seen:"
        for (k in nf) printf " NF=%s(%d)", k, nf[k]
        print ""
        if (cr) printf "  %d record(s) end with a CARRIAGE RETURN -- every anchored pattern will fail\n", cr
        else    print "  no carriage returns"
    }
' "$f"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sh look.sh dos.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  NR=1 NF=2 len=29 [Startpoint: u_alu/sum_reg[0]
]
  NR=2 NF=3 len=28 [  slack (VIOLATED)   -0.018
]
  2 record(s)
  field counts seen: NF=2(1) NF=3(1)
  2 record(s) end with a CARRIAGE RETURN -- every anchored pattern will fail
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sh look.sh empty.txt
sh look.sh no-such.txt 2>&1
echo "status: $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  EMPTY: 0 record(s) -- the file is readable and has no content
UNREADABLE: no-such.txt
status: 2

Three distinct answers for three distinct situations, and the CRLF warning is explicit rather than something to notice. Note where the closing ] lands in the first two rows — captured to a file rather than written to a terminal, the carriage return becomes a visible line break, which is the one context in which a bracketed dump does reveal it. The for (k in nf) listing has no defined order and is acceptable here only because it is a diagnostic read by a person — a script that consumed it would need a sort.

Exercise 6 — Challenge: show that the field-count distribution is the single most informative line

look.sh prints a distribution of NF rather than one value. Show what that catches which a single NF does not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'a b c\nd e\nf g h i\n' > ragged.txt
awk '{ printf "  NR=%d NF=%d\n", NR, NF }' ragged.txt
sh look.sh ragged.txt | tail -3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  NR=1 NF=3
  NR=2 NF=2
  NR=3 NF=4
  3 record(s)
  field counts seen: NF=2(1) NF=3(1) NF=4(1)
  no carriage returns

Three records, three different field counts. A script that read $3 would get a value, an empty string and a value — and the empty one is the record that silently produces nothing.

Chapter 11.3 measured the same thing on a formatted report whose rows had 9, 10 or 11 fields depending on optional annotations, and Chapter 13.3 Exercise 6 found it again in a script's own output. A single NF from the first record asserts that the file is regular; the distribution proves it, and when it is not regular the distribution says exactly how many records are affected.

So the most useful single line of a first look is not NF=3 — it is NF=2(1) NF=3(1) NF=4(1), because one value is a sample and a distribution is a measurement.

13. Summary

  • Empty output has no diagnostic content. Four of the six causes produce output that looks reasonable.
  • Step one is NR, NF and [$0] — it answers whether records arrived, how many fields there are, and what is in them.
  • A bracketed dump cannot show a carriage return. Use length($0) or cat -e; the CR breaks every anchored pattern (/VIOLATED$/ matched 0 against /VIOLATED/'s 1).
  • Widen a pattern until it matches, then narrow back one change at a time.
  • NF=1 on a file with visible columns means FS is wrong. The default handles tabs; forcing -F'\t' on space-aligned text is the dangerous direction.
  • Echo the program. Double quotes turned { print $1 } into { print }, which prints the whole line.
  • A semicolon after a pattern makes the next action unconditional — the count then equals NR.
  • An empty file gives NR=0; an unreadable one gives exit 2. Never conflate them.
  • A distribution of NF beats a single NF. One value is a sample; NF=2(1) NF=3(1) NF=4(1) is a measurement.

Chapter 14.2 — Field and Delimiter Problems takes the cause this chapter could only point at: columns that line up on screen tell you nothing about what separates them, and a report whose format changed between tool versions changes NF without changing how it looks.

Where this fits

Part of the awk curriculum.