For a long time I just used cat -n any time I needed line numbers, because it was the first thing I learned and it worked well enough. Then I ran into a source file where I only wanted numbers on lines with actual content, skipping blank lines so the numbering matched a printed document’s paragraph numbering. cat -n numbers every single line, blank or not, with no way to change that. That’s when I actually looked into nl and realized it was purpose-built for exactly this kind of formatting control that cat -n simply doesn’t offer.
What nl Does
nl numbers the lines of a file (or STDIN) and writes the numbered output to standard output. Unlike cat -n, which numbers every line unconditionally, nl has configurable logic for which lines get numbered and how the numbers are formatted.
nl [OPTION]... [FILE]...
I tested the default behavior:
$ printf "line1\nline2\nline3\n" | nl
1 line1
2 line2
3 line3
Right-aligned numbers, a tab separator, then the content — that’s the default output style.
The Key Difference from cat -n: Blank Line Handling
This is the single most important thing to understand about nl, and it’s the reason the command exists as a separate tool at all. By default, nl does not number blank lines:
$ printf "\nline1\n\nline2\n" | nl
1 line1
2 line2
I tested this directly — the blank lines pass through with no number at all (just blank space where the number would go), while the actual content lines get numbered 1 and 2. Compare this to forcing all lines to be numbered with -ba:
$ printf "\nline1\n\nline2\n" | nl -ba
1
2 line1
3
4 line2
With -ba, every line — including blank ones — receives a sequential number, matching what cat -n would produce.
Core Options and Parameters
-b STYLE — Body Numbering Style
This controls which lines get numbered:
-b a— number all lines, including blank ones.-b t— number only non-empty lines (this is the default behavior).-b n— number no lines at all.-b pREGEX— number only lines matching a given regular expression.
$ printf "\nline1\n\nline2\n" | nl -ba
1
2 line1
3
4 line2
The regex mode is particularly useful for numbering only specific structural lines — for example, numbering only lines that look like section headers:
nl -bpSECTION document.txt
This numbers only lines containing the string “SECTION,” leaving everything else unnumbered — handy for generating a numbered table of contents-style output from a structured document.
-s SEPARATOR — Custom Separator Between Number and Text
By default, nl separates the line number from the content with a tab. Change it with -s:
$ printf "a\nb\nc\n" | nl -s': '
1: a
2: b
3: c
I tested this and it’s a small but genuinely nice formatting improvement when you want output that reads more naturally, especially for terminal display where tab width can vary.
-w WIDTH — Number Field Width
Controls how many character positions the line number field occupies:
$ printf "a\nb\nc\n" | nl -w4
1 a
2 b
3 c
I confirmed the numbers here are right-aligned in a 4-character-wide field rather than the wider default width. This matters for consistent visual alignment, especially when you know your file has a manageable, roughly predictable number of lines and want tighter formatting than the default width provides.
-v NUMBER — Starting Line Number
Instead of starting the count at 1, start it wherever you need:
$ printf "a\nb\nc\n" | nl -v10
10 a
11 b
12 c
This is genuinely useful when numbering a file that represents a continuation of a previous section — for instance, appending numbered output to something that already ended at line 9.
-i NUMBER — Increment Value
Controls the step between consecutive numbers, instead of always incrementing by 1:
$ printf "a\nb\nc\n" | nl -i5
1 a
6 b
11 c
I tested this and confirmed it increments by 5 each time starting from 1 — useful for leaving numbering gaps intentionally, similar to old BASIC line-numbering conventions (10, 20, 30…) that made later insertions easier without renumbering everything.
-n FORMAT — Number Alignment Format
Controls how the number itself is formatted within its field:
-n ln— left-justified, no leading zeros.-n rn— right-justified, no leading zeros (the default).-n rz— right-justified, with leading zeros.
$ printf "a\nb\n" | nl -n rz -w5
00001 a
00002 b
I tested the rz format combined with -w5, producing zero-padded 5-digit line numbers — a format that comes up in generating file listings that need to sort correctly as strings (since zero-padded numbers sort identically whether compared as text or as numbers).
-p — Don’t Reset Numbering at Logical Page Breaks
nl has a concept of “logical pages” derived from special delimiter lines (\:\:\:, \:\:, \:) that historically mimicked page-break-aware numbering for printed documents. By default, numbering resets at each new logical page. -p disables that reset, keeping numbering continuous throughout the whole file regardless of these page-break markers. Most modern usage of nl never encounters this feature directly since files rarely include those special markers, but it’s worth knowing it exists if you’re working with older-style formatted documents that do use them.
How nl Works Internally
nl reads the input stream line by line, and for each line it checks against the current body-numbering rule (-b style) to decide whether that line should receive a number. If it should, nl formats the current counter value according to the -n and -w settings, appends the configured separator, appends the original line content, and increments the counter by the configured -i step. This is a straightforward stateful line-by-line pass — nl doesn’t need to buffer the whole file, so it scales linearly with input size regardless of file length.
Practical, Real-World Examples
1. Numbering Only Content Lines in a Script or Config File for Documentation
nl -ba script.sh > script_numbered.txt
Using -ba here ensures blank lines used for visual spacing in the script still get a number, keeping the numbering consistent with what you’d see in a text editor’s gutter.
2. Generating a Numbered Table of Contents
grep -n '^## ' README.md | nl -w3 -s'. '
3. Creating a Zero-Padded Sequence for File Naming Reference
ls *.jpg | nl -n rz -w4 -s': '
4. Numbering Only Lines Matching a Pattern
nl -bpERROR application.log
This numbers only the lines containing “ERROR,” leaving all other lines unnumbered — a quick way to get a running count of error occurrences while still seeing full log context.
5. Preparing Line-Referenced Output for a Bug Report
nl -ba -s': ' buggy_script.py | sed -n '40,60p'
Numbering the whole file first, then slicing a specific range, gives you output you can paste directly into a bug report or code review comment with accurate line references.
nl in Shell Scripting and Automation
A pattern I use when generating printable inventory or audit lists that need sequential, human-readable reference numbers:
#!/bin/bash
find /data/exports -name '*.csv' -newer /tmp/last_run \
| nl -w3 -s'. ' -n rn > new_files_report.txt
This produces a clean, numbered list of newly found files, formatted for inclusion directly in an email or report without further processing.
Comparing nl to Related Commands
| Task | Best Tool |
|---|---|
| Simple line numbering, every line including blanks | cat -n |
| Line numbering with control over blank-line handling | nl |
| Line numbering only for matching lines | nl -bpREGEX |
Numbering as part of a larger sed transformation | sed = file | sed 'N;s/\n/\t/' |
| Extracting line numbers of matches (not full numbering) | grep -n |
The clearest distinction: cat -n is a quick, no-configuration option baked into cat itself, while nl is a dedicated tool with real formatting control — blank-line handling, custom separators, width, zero-padding, starting values, and pattern-based selective numbering. If you need anything beyond “number every line,” nl is the right tool; if you just want a quick visual reference while reading a file in the terminal, cat -n or less -N is often simpler.
Troubleshooting Common nl Issues
Blank lines not getting numbers when you expected them to — this is the default -b t behavior (number only non-blank lines); use -ba if you want every line numbered.
Numbers reset partway through a large document unexpectedly — check whether the file contains logical page-break marker lines (\:\:\:, \:\:, \:); use -p to disable the reset behavior if that’s not what you want.
Output columns misaligned in a terminal — adjust -w to a width appropriate for your file’s total line count, and consider a fixed-width separator with -s instead of the default tab, which can render inconsistently across terminal emulators.
Performance Considerations
nl is lightweight and processes input in a single streaming pass, so performance scales linearly with file size and is rarely a bottleneck in practice, even on fairly large text files.
Security Implications
nl poses no direct security risk on its own; as with other text-processing tools, be mindful that numbered output derived from sensitive files retains the same sensitivity as the source content — numbering doesn’t redact or transform the underlying data in any protective way.
Compatibility Across Distributions
nl is part of GNU coreutils (tested here at version 9.4) and ships by default on Ubuntu, Debian, Fedora, RHEL/CentOS, Arch, and openSUSE. It’s also specified by POSIX, so BSD and macOS include a compatible nl implementation with the core options (-b, -n, -s, -w, -v, -i) working consistently, making it one of the more portable formatting utilities across Unix-like systems.
nl and the Concept of Logical Pages
One of the more historically interesting, less-used features of nl is its support for “logical pages,” inherited from its original design purpose of numbering lines for printed documents that were divided into header, body, and footer sections. A file can contain special marker lines — \:\:\: for the start of a new header section, \:\: for a new body section, and \: for a new footer section — and nl treats these as boundaries that, by default, reset the line-number counter. This was originally meant to mirror how a typesetting or printing system might restart numbering at the top of a new logical page in a formatted document.
In practice, very few modern files actually contain these marker sequences, so most users never encounter this behavior directly. But it’s worth knowing about for two reasons: first, if you ever see nl‘s numbering mysteriously reset partway through a file for no apparent reason, checking for these marker lines (or stray occurrences of \: sequences) is a reasonable first troubleshooting step; second, the -h, -b, and -f header/body/footer-specific numbering-style options (distinct from the general -b body style discussed earlier) exist specifically to control numbering behavior independently within each of these logical sections, for anyone still working with content genuinely structured this way.
Comparing nl’s Default Behavior Across Common Use Cases
It’s useful to see, side by side, how nl‘s default blank-line-skipping behavior actually changes practical output compared to what many people expect from a “number my file” command:
$ printf "def foo():\n pass\n\ndef bar():\n pass\n" > script.py
$ nl script.py
1 def foo():
2 pass
3 def bar():
4 pass
Notice that the blank line separating the two function definitions received no number at all and was simply passed through — the numbering sequence jumps straight from the pass statement of the first function to the def bar(): line without acknowledging the blank line in between. If you’re numbering source code specifically to reference exact line numbers in an editor or IDE (where every line, blank or not, does get a number), this default nl behavior will produce numbers that don’t line up with what your editor shows. In that case, -ba is what you actually want:
$ nl -ba script.py
1 def foo():
2 pass
3
4 def bar():
5 pass
This distinction — and knowing to reach for -ba specifically when numbers need to match editor line numbers — is probably the single most common practical gotcha people run into with nl.
Using nl for Quick Data Auditing
Beyond formatting documents for printing or review, nl is genuinely useful as a lightweight way to spot-check row counts and specific record positions in structured data files during ad hoc data auditing, without needing to open a full data-processing tool:
nl -ba inventory.csv | sed -n '1p;500p;$p'
This shows the first, 500th, and last lines of a CSV file, each tagged with its actual line number — a quick sanity check that a file has the expected number of records and that specific rows look reasonable, all in a single composed command.
Summary
nl exists specifically because cat -n isn’t configurable enough for real formatting needs. The defaults — skip blank lines, right-justify numbers, tab-separate — cover a lot of cases, but the real value shows up when you need selective numbering by pattern (-bp), custom increments (-i), zero-padded output for consistent sorting (-n rz), or a starting offset (-v) for continuing a sequence across multiple files or sections.
References
- GNU Coreutils Manual —
nl: https://www.gnu.org/software/coreutils/manual/html_node/nl-invocation.html - POSIX Specification for
nl: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nl.html man nl(local manual page)