Skip to content
Linux Basics: Number Selected Text Lines with GNU nl

Linux Basics: Number Selected Text Lines with GNU nl

October 25, 2026

Line numbers help when discussing a short text file, but sometimes only nonempty lines or matching entries should advance the counter. GNU nl lets you choose which lines receive numbers while retaining the rest of the text.

These examples were tested on Linux with GNU Bash 5.2.37 and GNU coreutils 9.7. Each script sets LC_ALL=C. The first two use sed -n l only to make whitespace visible: $ marks a line ending and is not part of nl’s output.

Prepare a scratch directory

You need Bash, GNU nl, sed, a text editor, and the mktemp, rm, and rmdir utilities. Use the same terminal throughout. The guards stop the shell if setup fails. Save the named scripts only after setup succeeds, then run each once in order.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Choose whether blank lines consume a number

Save this as nl-empty-lines.sh:

#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
printf '%s\n' alpha '' beta > lines.txt
printf 'Nonempty lines only:\n'
nl --section-delimiter='' --body-numbering=t \
  --number-format=rz --number-width=2 --number-separator='|' \
  lines.txt | sed -n l
printf 'All lines:\n'
nl --section-delimiter='' --body-numbering=a \
  --number-format=rz --number-width=2 --number-separator='|' \
  lines.txt | sed -n l

Run it:

bash nl-empty-lines.sh

Output:

Nonempty lines only:
01|alpha$
   $
02|beta$
All lines:
01|alpha$
02|$
03|beta$

The t style numbers nonempty body lines. The a style numbers every body line, including an empty one. An unnumbered line still has a blank number area; it is not removed from the output. A line containing spaces is nonempty even if it looks blank in a terminal.

rz right-aligns numbers with leading zeroes. The two-column width and | separator make the result easier to inspect than the default six-column number followed by a tab. The GNU nl manual packaged by Debian lists the numbering styles and formatting options.

2. Number matching entries without filtering other lines

Save this as nl-match-lines.sh:

#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
printf '%s\n' \
  'Notes for review' \
  'TODO: check sample input' \
  'DONE: inspect output' \
  'TODO: update the explanation' > review.txt
nl --section-delimiter='' --body-numbering='p^TODO:' \
  --starting-line-number=10 --line-increment=10 \
  --number-format=rz --number-width=2 --number-separator='|' \
  review.txt | sed -n l

Run it:

bash nl-match-lines.sh

Output:

   Notes for review$
10|TODO: check sample input$
   DONE: inspect output$
20|TODO: update the explanation$

The p style is followed immediately by a basic regular expression. Here ^TODO: restricts numbering to lines beginning with that literal marker. It is not a shell filename pattern, and it does not automatically use extended regular-expression syntax.

The heading and completed item remain visible but do not advance the counter. Starting at ten and increasing by ten gives labels for the selected entries, not their original physical line positions. Use all-line numbering when someone needs to identify an exact line in the source file.

3. Keep delimiter-looking text as ordinary data

Save this as nl-literal-markers.sh:

#!/usr/bin/env bash
set -eu
export LC_ALL=C
printf '%s\n' first '\:\:\:' second > markers.txt
nl --section-delimiter='' --body-numbering=a \
  --number-format=rz --number-width=2 --number-separator='|' \
  markers.txt

Run it:

bash nl-literal-markers.sh

Output:

01|first
02|\:\:\:
03|second

GNU nl supports logical pages with header, body, and footer sections. By default, special lines built from \: act as section delimiters. The fixture deliberately contains one of those marker lines.

Passing an empty --section-delimiter disables section recognition, so the line remains ordinary data and the numbering continues. This GNU extension is useful when numbering arbitrary text rather than a document prepared for nl’s page model. The earlier examples use the same setting for consistency.

Keep in mind

nl writes a new display to standard output; it does not edit its input file. If you want to retain the report, redirect it to a different filename. Redirecting onto the input filename would truncate that input before nl reads it.

The counter reflects the chosen numbering policy. Filtering or rearranging lines before nl also changes what those numbers mean. Decide whether you need source-line references, numbered records, or a presentation sequence before choosing the pipeline order.

Clean up

Remove the named practice files, then return to your original directory:

rm -- nl-empty-lines.sh nl-match-lines.sh nl-literal-markers.sh \
  lines.txt review.txt markers.txt
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on