Skip to content
OneKitly

Numbering the Lines of a Text for a Review with Several People

Published 8/12/2026 · 15 min read · Text & language tools

Daniel Okonkwo

Daniel OkonkwoFront-end developer and tech writer at OneKitly

Web performance · File formats

Checked against 3 sources

View profile
In short

Paste the text into add-line-numbers and every line gets a number in front of it, starting at 1. The "Start at" box accepts another value, but not zero: the code reads Number(start) || 1, and 0 is falsy, so 0 comes back as 1. Negative starts do work — set −3 on three lines and you get −3, −2, −1. Alignment is automatic and uses spaces, never leading zeros: the width comes from the last number, so 102 lines produce " 1. " through "102. ", and a 99-line text produces " 1. " with one space. The default separator is a dot and a space; ten others are offered, including "1) ", "1: ", "[1] ", "#1 " and a tab. The return trip is asymmetrical, but it is no longer destructive. remove-line-numbers matches one pattern — leading whitespace, digits, a separator drawn from a set of eight, then a single space or the end of the line — so it undoes eight of the eleven and leaves the dash, the pipe and the bare space alone, on purpose: "10 - 20 people" and "1 | Alice | 30" are content far more often than numbering. Because it now takes exactly one space rather than a greedy run, numbering indented JavaScript and un-numbering it again hands the code back byte for byte, indentation included. And because a separator must be followed by a space, "3.14 is pi" and "12.50 Coffee" are left where they are. What no rule can rescue is a line that looks exactly like the tool's own output: "2026. A year." still comes back as "A year." Number a copy, keep the original.

Numbering starts at 1 and cannot be set to 0, the alignment is spaces rather than zeros, and the remover undoes eight of the eleven separators without touching the indentation. What it still cannot do is tell your numbers from its own.

Why a shared text needs numbers at all

Code arrives with numbers already attached. The editor prints them down the left margin, a diff prints them either side of the change, and a review comment on a hosting platform anchors itself to one of them. Prose does not. A contract clause, a set of subtitles, a translation memory export, a list of product names waiting to be checked by two colleagues — none of these has a stable way of saying "the fourth one from the top" other than counting.

Counting by hand is where the review goes wrong. Two people count from different places, one of them includes the blank line and the other does not, and an hour later there are two edits to the same clause and one to a clause nobody meant to touch. Attaching a number to every line before circulating it removes that whole class of confusion for the cost of one paste.

The important word is "before". Numbers are a coordinate system, and a coordinate system is only useful while it stands still. If one person renumbers a text that has already been annotated, every comment on it becomes a guess. Number once, circulate that exact version, and collect the edits against it.

What the tool actually produces

The default is a dot and a space: "first line" becomes "1. first line". Ten other separators are offered — "1) ", "1: ", "1 - ", "1 | ", "[1] ", "1] ", "#1 ", "1 → ", a plain space, and a tab. Two of them wrap the number rather than follow it, which is why "[1] " and "#1 " look different from the rest.

Alignment is worked out from the last number and applied with ordinary spaces, not zeros. A text of 102 lines comes back with " 1. line1" at the top and "102. line102" at the bottom, the digits flush right. The width is decided once, before the first line is written, so a 99-line text gets a width of two and a 100-line text gets a width of three — one extra leading space on every line, from a single added line. If you are diffing two numbered versions, that is why every line shows as changed.

Three small behaviours are worth knowing before you paste something long. A trailing newline produces a numbered empty line at the end, because splitting "a\nb\n" on line breaks yields three pieces and the third is empty — the output ends with "3. ". An empty input produces "1. " rather than nothing. And an input with Windows line endings comes back with Unix ones: the split accepts a carriage return before the line feed but the join puts back only the line feed.

Start at 1, and only at 1 — or at a negative number

The "Start at" box exists because a numbered extract usually belongs somewhere: page four of a transcript should not start again at 1. Set it to 500 and the output runs 500, 501, 502, with a width of three. That much works exactly as advertised.

Zero is the exception, and it is not a bug you can work around by typing it differently. The value is read as Number(start) || 1. Zero is one of JavaScript's falsy values, so the expression falls through to 1 — typing 0 and typing nothing give the same result. Anyone who wanted array-style numbering, where the first line is line 0, will not get it here. A blank box behaves the same way, and so does a value that is not a number at all: "abc" yields 1.

Negative starts, oddly, do work, because −3 is truthy. Three lines starting at −3 come back as "-3. ", "-2. ", "-1. " — useful for nothing in particular, but it tells you the guard is about falsiness and not about sign. Fractional values are floored towards minus infinity: 2.7 becomes 2 and −2.7 becomes −3.

The return trip, which is not symmetrical

remove-line-numbers has no options at all. It applies one pattern to each line: leading spaces or tabs, then the number and its marker — digits followed by a tab, or digits wrapped in square brackets or preceded by a hash, or digits followed by a dot, a closing parenthesis, a colon, a closing square bracket or an arrow — and then one single space, or the end of the line. Everything matched is deleted, once per line. Two details in that sentence do all the work: the marker must be followed by a space, and the space that gets eaten is exactly one.

Run every one of the eleven separators through it and eight survive the trip: dot, parenthesis, colon, tab, "[1] ", "1] ", "#1 " and "1 → " all come back to the original text exactly. Three do not, and the omission is deliberate. "1 - alpha", "1 | alpha" and "1 alpha" come back unchanged, numbers still attached, because "5 - 3 = 2", "10 - 20 people", "1 | Alice | 30" and "007 James" have exactly the same shape and are content far more often than they are numbering. A remover that undid those three would eat a spreadsheet row every time somebody pasted one in.

The pattern used to end with a greedy run of whitespace, which took the text's own indentation with it; it now takes exactly one space. Number five lines of JavaScript with a function, an if and a return, un-number them, and the file comes back byte for byte, indents included. YAML and Python behave the same way, which matters more there than anywhere: a flattened result in those two languages is not merely ugly, it no longer means the same thing.

A remover cannot know whose number it is

This is the honest limit of the whole idea, and the space rule narrows it rather than removing it. "1. Wash the rice" is a numbered line if the tool put the 1 there and a piece of content if the cook did. Nothing in the string distinguishes the two, and no pattern can: those two lines are the same bytes. Requiring a space after the separator rules out the accidents — "3.14 is pi" and "1.2.3 version" come back untouched now, because a decimal point is never followed by a space — but it cannot rule out the coincidences. "12: 30 departure" still comes back as "30 departure". "2026. A year." still comes back as "A year." Both are character for character what add-line-numbers writes, so on a line nobody numbered, that is still data loss.

Two things keep the ordinary case safe. The deletion happens once per line and stops at the first separator, so a numbered price list gives "1. 12.50 Coffee", and un-numbering it matches "1. " and stops, handing "12.50 Coffee" back intact; a list that already had its own numbers, "1. 1. alpha", loses exactly one level. And that price line now survives even without the numbering, because the dot in "12.50" is not followed by a space. So the round trip is safe on any text, indented or flat, with any of the eight separators. What is left to bite is the remover on its own, applied to text nobody numbered, on the narrow set of lines that happen to look like its own output.

Knowing why something is safe is more useful than the list. A CSV of identifiers survives — "1024, Dupont, 42" comes back untouched — because the character after the digits is a comma, and a comma appears in none of the marker shapes. "100 units of flour" and "007 James" survive because a bare space is not one either. Decimals and semantic versions are safe too, but for the other reason: "3.14" and "1.2.3" do carry a dot, and what saves them is that no space follows it. What is at risk is the narrow set where a number, a recognised marker and a space arrive in that order: years used as headings, "12: 30" written with a space after the colon, and — new since the marker set was widened — footnote markers like "[1] " and lines opening with "#1 ".

Running the review so nothing is lost

Keep the unnumbered original somewhere you will not overwrite it, and treat the numbered version as a printout: something to read, quote and annotate, not something to edit and send back. Every edit collected as "line 47, replace X with Y" applies cleanly to the original, and the numbers never need removing at all.

If you genuinely intend to un-number later, avoid exactly three separators: the dash, the pipe and the bare space. Everything else on the menu — dot, parenthesis, colon, tab, "[1] ", "1] ", "#1 ", "1 → " — comes back off. Prefer the tab if the text is going anywhere near a spreadsheet: it puts the numbers in their own column.

Numbering code you intend to paste back is now survivable, which it was not: with a dot, a parenthesis, a colon or a tab, the file comes back byte for byte, indentation included. It is still rarely the right move — a review of code has better instruments, a diff, a pull request, or simply the line numbers the editor is already showing both of you — but if you do it, choose one of those four and run the round trip on a copy first.

One line in, one line out, run through both tools
InputResultWhy
"Start at" set to 0, two lines1. a and 2. bNumber(start) || 1 — zero is falsy, so it becomes 1
"Start at" set to −3, three lines-3. a, -2. b, -1. cA negative number is truthy, so the guard lets it through
A 102-line text, default separator" 1. " at the top, "102. " at the bottomRight-aligned with spaces; the width comes from the last number
Text ending with a line breakA numbered empty line at the endSplitting on line breaks yields one more piece than there are lines of text
"1 - alpha" through remove-line-numbersUnchangedThe dash is left out on purpose: "10 - 20 people" is content far more often than numbering
Five lines of indented JavaScript, numbered then un-numberedThe code comes back byte for byte, indents includedThe pattern takes exactly one space after the separator, not a greedy run
"3.14 is pi", never numbered, through the removerUnchangedA separator must be followed by a space, and a decimal point never is
"1024, Dupont, 42", never numbered, through the removerUnchangedA comma appears in none of the eight marker shapes, so nothing matches
"1. 12.50 Coffee", numbered by the tool, through the remover"12.50 Coffee" — the round trip is losslessThe replacement runs once per line and stops at the first separator
Add line numbersPrefix every line of your text with a sequential line number.Try the tool

Frequently asked questions

Can I make the numbering start at 0?
No, and there is no trick that gets around it. The tool reads the box as Number(start) || 1. Zero is falsy in JavaScript, so the expression discards it and hands back 1 — typing 0, leaving the box empty and typing a word all produce a list that starts at 1. This was checked with 0, "0", "" and "abc", and all four gave the same output. If you need zero-based numbering, number from 1 and subtract one in your head, or number from −1 upwards if the offset matters more than the sign: negative starts do work, because a negative number is truthy.
Will the numbers still line up once the text passes 100 lines?
Yes. The width is computed from the last number before anything is written, and every number is padded on the left with spaces to that width. A 102-line text starts with two leading spaces on line 1 and none on line 102, so the dots form a straight column. There are no leading zeros — you get " 7. ", never "007. ". One consequence catches people out: a text of 99 lines uses a width of two and the same text with one line added uses a width of three, so every single line changes by one space. If you compare two numbered versions with a diff, that is why the whole file looks modified. Compare the originals instead.
Does remove-line-numbers always undo add-line-numbers?
No, but the reasons have changed. It recognises eight of the eleven separators — dot, closing parenthesis, colon, tab, "[1] ", "1] ", "#1 " and "1 → " — and leaves the dash, the pipe and the bare space alone on purpose, because "10 - 20 people", "1 | Alice | 30" and "007 James" have the same shape and are usually content. With a recognised separator the round trip is now byte-for-byte, indentation included: five lines of JavaScript with a function body indented four and eight spaces went in and came back identical, and so did YAML, Python and a flat price list.
I ran the remover on a list of prices and it ate part of them. What happened?
It cannot do that any more. The remover used to delete leading digits followed by a dot, a closing parenthesis, a colon or a tab, with nothing required after — so "3.14 is pi" became "14 is pi" and "12.50 Coffee" became "50 Coffee". The pattern now requires a space, or the end of the line, after the separator, and a decimal point is never followed by a space: "3.14 is pi", "1.2.3 version" and "12.50 Coffee" all come back untouched. What it can still take is a line shaped exactly like its own output — "2026. A year." gives "A year.", "12: 30 departure" gives "30 departure" — and, since the marker set was widened, footnote markers: "[1] See note" gives "See note". A comma-separated list is safe either way, because a comma appears in none of the marker shapes.
Should I number source code for a code review?
Usually not, though it is no longer risky. Code already carries line numbers everywhere it is read — in the editor's margin, in a diff, in the review interface of any hosting platform — so numbering it mostly adds noise. It does survive the round trip now: with a dot, a parenthesis, a colon or a tab, numbering an indented file and un-numbering it returns it byte for byte, indentation included, which was not true before. Three separators are still never removed — the dash, the pipe and the bare space — so avoid those if the numbers are meant to come off. Numbering earns its place on text that has no numbers of its own and is being read outside a tool that supplies them: a contract circulated as a document, a subtitle file discussed in a chat thread, a list of strings sent to a translator. If you are reviewing code and the platform is out of reach, quoting it in a fenced block and referring to it by function name is still the calmer route.

Articles you may find interesting

All guides
How-toFiltering Lines by a Pattern Without a Command LineThis is grep for people who do not use grep, with one important difference: the match is a plain substring, so a real regular expression returns an empty box and no error. Every claim here was checked by running the tool.ExplainerSorting Text Is Not One Operation: Four Orderings That All Call Themselves AlphabeticalCode-unit order, natural order and locale collation run on the same list in Node, with the outputs printed. Why Zebra beats apple, why item10 beats item9, and why ä sits next to a in German but after z in Swedish.ExplainerFinding Duplicates in a List Without a SpreadsheetTwo lines that look identical are often not identical. Case, a trailing space, a no-break space and two different encodings of the same accented letter were each run through the duplicate finder, and it reported no duplicates for three of the four.GuideFind and Replace: The Regex Features That Bite, Demonstrated One by OneGreedy against lazy on the same string, the dot that skips newlines, $& and $$ in the replacement, a reused /g regex that silently skips a row, and why /i knows nothing about Turkish i. Every failure run in Node, with a count-then-replace routine that catches them.How-toRegex Basics: A Beginner's GuideA regular expression is a pattern for matching text. Here are the building blocks — character classes, quantifiers and anchors — with a worked example.ExplainerDetecting the Language of a Text, and Why Short Texts FailMeasured, not asserted: 90 short real phrases across six languages, none declined and 68 right — 76%, falling to 64% under sixteen letters. Four of the wrong answers came back at 100% confidence.

Related tools

Everything here describes what these four tools do today, checked by running their own code against the exact inputs printed in each article — not what a standard obliges them to do. Where a tool gets a case wrong, that is said plainly rather than worked around, and nothing was changed to make an article read better. Two consequences follow. Run any transform over a copy first and compare both ends: a text tool that deletes something is silent about it. And treat a secret the moment it leaves the page as a secret you have shared — pasting one into a chat, a ticket or a repository burns it however well it was generated.

Sources

Spotted a mistake in this article?