nl in Shell Reports: Logical Pages, Section Rules, and Line-Number Semantics
Use nl to number selected lines in document sections, while preserving the distinction between presentation labels and stable record identifiers.
The nl utility numbers lines in text while allowing different rules for document headers, bodies, and footers. It is more configurable than prefixing every line with a counter: it can number nonempty lines, all lines, or selected lines, set a start value and increment, and reset numbering at logical page boundaries. Those features help create readable reports, but the output is a presentation transform. Line numbers are not durable record IDs and should not be used as database keys or as a substitute for source locations.
nl views its input as logical pages with header, body, and footer sections. Special delimiter lines mark transitions between those sections; by default, delimiters use repeated backslashes and colons. Line-numbering options can differ independently across the three sections. The command can therefore emit no number for headers, number nonempty body lines, and handle footers separately. This page model is useful for structured documents, but it is surprising when the input is an arbitrary log that happens to contain delimiter-looking lines.
Select what counts as a line
The body numbering type supports all lines, nonempty lines, no lines, or lines matching a basic regular expression. POSIX defines these types; GNU and BSD may offer extra formats and convenience flags. Because -b takes a required argument, POSIX permits the argument to be attached (-ba) as well as separated (-b a). The separated form can be easier to read, but the attached form is not GNU-specific. Always verify nonstandard options on the target system.
For a simple report that should number every input line, a common invocation is:
nl -b a -v 1 -i 1 -w 5 -s ' ' report.txt
This requests all-line numbering, an initial value and increment of one, a five-character number field, and two spaces between the number and source text. It is intended for display, not machine parsing. Leading spaces in the formatted number field can be significant, and changing the width or separator changes the output layout.
If empty lines should remain unnumbered, choose the corresponding nonempty-line rule. Under the POSIX text-line model, a line containing spaces or tabs is nonempty; nl -b t does not mean “contains a non-whitespace character.” For a pattern-selected section, check the regular-expression dialect and locale. A visually empty line may contain a carriage return, nonbreaking space, or other characters that the selected pattern does not match.
Page delimiters can change numbering state
The page delimiters are not decorative if they appear as exact delimiter-only lines. They switch sections and can reset line numbering at the next logical page. A document that contains such a line as literal content may therefore receive unexpected numbering. Configure a different delimiter when the content format uses those strings, or preprocess through a parser that distinguishes structure from data.
Header, body, and footer settings are independent. This lets an author number body paragraphs while leaving title blocks and footnotes unnumbered, but it also means a careless setting can skip lines that a downstream reviewer expects to cite. Build test fixtures with each section empty and nonempty, multiple pages, delimiter lines that resemble data, and blank lines in every region. Inspect the output around transitions instead of checking only the first page.
Why generated line numbers are unstable identifiers
Insert one line near the top and every later line number shifts. A filtered or normalized report can also renumber rows, so a line label has meaning only relative to a specific exact input version and transformation. If a system needs a stable reference, assign an explicit record ID before formatting or pair the report with a source digest and byte/line offset. For source-code diagnostics, use the compiler or parser’s own location output rather than adding nl as an independent numbering pass.
Avoid parsing nl output by splitting on whitespace. The original line may itself contain leading spaces or tabs, and the number field’s width is configurable. If a later tool needs both fields, use a structured format with an escaping rule or keep the source line and index as separate values in AWK, Python, or another parser. nl is intended to make text easier for a person to reference, not to create a lossless database encoding.
Reliability and portability checks
nl reads text records, so binary data and arbitrary path lists are outside its intended model. A missing final newline can change how a final record is displayed. Very long lines may be expensive to copy or may exceed downstream viewer limits. Keep stderr visible, check the status, and write generated output to a temporary destination if it will replace an authoritative report.
Use a fixed locale if matching or number formatting must be deterministic. Check line count before and after transformation and test output with the actual renderer or ticketing system where it will be pasted. If numbers are used in a review conversation, include the file version or digest so a later edit does not make the references ambiguous. Document the numbering rule, start value, width, separator, and page-reset behavior in any automated report specification.
The utility is most useful when a person needs an easy way to discuss a fixed text snapshot. It should not be confused with grep -n, which annotates matching lines, or with an editor’s stable source identifiers. Use nl as a presentation stage after the content has been validated, and retain the unmodified original so that formatting does not become the only record of what was analyzed.
Related:
- AWK Records and Fields: Parse Text Without Pretending It Is CSV
- sort in Shell Pipelines: Locale, Keys, Stability, and Reproducible Output
Sources: