JavaScript Basics: Extract Text With Regular Expressions
A log line might contain a ticket number, a status, and a short message. A regular expression can describe the text we want to extract without splitting every line at fixed positions. We will start with a small ticket pattern, collect several matches, and then inspect a common surprise when reusing a global expression.
Prepare a scratch directory
You need Linux, Bash, a text editor, the mktemp, rm, and rmdir utilities, and Node.js on your PATH. These examples were tested with Node.js v24.19.0 on Linux. They use built-in features only, with no npm packages, network requests, or custom environment variables.
Run in Bash:
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.0Keep this terminal open. Save each source block as a UTF-8 file with the stated filename in this directory. The .mjs extension selects ES modules. The assertions below stay silent when they pass.
Extract named fields
Save as extract-ticket-fields.mjs:
import assert from "node:assert/strict";
const pattern = /Ticket (?<number>[0-9]+): (?<status>open|closed)/;
const match = pattern.exec("Ticket 42: open");
assert.notEqual(match, null);
assert.equal(match.groups.number, "42");
assert.equal(match.groups.status, "open");
console.log(`Number: ${match.groups.number}`);
console.log(`Status: ${match.groups.status}`);
console.log(`Full match: ${match[0]}`);
const missing = pattern.exec("No tickets today");
assert.equal(missing, null);
console.log(`No match: ${missing}`);Run:
node extract-ticket-fields.mjsExpected output:
Number: 42
Status: open
Full match: Ticket 42: open
No match: null[0-9]+ matches one or more ASCII digits. open|closed selects either word, and (?<name>...) gives a captured piece a name. exec returns a match object or null; check that result before reading its groups. Captured digits are still text. This pattern searches for a matching part of the input, so it is an extractor rather than a complete-line validator. See MDN’s named capturing groups reference.
Collect all matching ticket numbers
Save as collect-ticket-numbers.mjs:
import assert from "node:assert/strict";
const text = "Fixed BUG-12 and BUG-34; retest BUG-12.";
const pattern = /BUG-(?<number>[0-9]+)/g;
const numbers = Array.from(text.matchAll(pattern), match => match.groups.number);
assert.deepEqual(numbers, ["12", "34", "12"]);
assert.equal(pattern.lastIndex, 0);
assert.deepEqual(Array.from("No tickets".matchAll(pattern)), []);
console.log(`Numbers: ${JSON.stringify(numbers)}`);
console.log(`Original search position: ${pattern.lastIndex}`);
const normalized = text.replace(/BUG-([0-9]+)/g, "issue:$1");
assert.equal(normalized, "Fixed issue:12 and issue:34; retest issue:12.");
console.log(normalized);Run:
node collect-ticket-numbers.mjsExpected output:
Numbers: ["12","34","12"]
Original search position: 0
Fixed issue:12 and issue:34; retest issue:12.The g flag asks for global matching. matchAll requires this flag when passed a regular expression and returns an iterator; Array.from collects its results. It uses a copy of the expression and does not advance the original expression’s lastIndex. Start with a fresh expression, as above: an already changed lastIndex influences where its copy begins searching. Each result includes captures, so duplicates remain in our array. See MDN’s matchAll documentation.
For replace, $1 inserts the first captured group in the replacement string. The global flag makes this replacement apply to every match. Without it, only the first match would be replaced. The original string is unchanged; replace returns a new one. See MDN’s replace reference.
Avoid stateful tests for simple membership checks
Save as inspect-pattern-state.mjs:
import assert from "node:assert/strict";
const globalPattern = /cat/g;
const first = globalPattern.test("cat");
const second = globalPattern.test("cat");
assert.equal(first, true);
assert.equal(second, false);
assert.equal(globalPattern.lastIndex, 0);
console.log(`Repeated global test: ${first}, ${second}`);
const simplePattern = /cat/;
assert.equal(simplePattern.test("cat"), true);
assert.equal(simplePattern.test("cat"), true);
assert.equal(simplePattern.test("dog"), false);
console.log(`Repeated plain test: ${simplePattern.test("cat")}, ${simplePattern.test("cat")}`);Run:
node inspect-pattern-state.mjsExpected output:
Repeated global test: true, false
Repeated plain test: true, trueWith g, a successful test advances lastIndex. The next test starts there, even when given the same string. The failed second search resets the position to zero. Leave off g and y when you only need independent yes-or-no checks. The sticky y flag is stateful too. See MDN’s test documentation.
Use simple string methods when you only need a literal substring. Keep regex patterns under your control and small enough to understand; these examples do not provide a general validator for arbitrary input formats.
Clean up
Remove only the three tutorial files:
rm -- extract-ticket-fields.mjs collect-ticket-numbers.mjs inspect-pattern-state.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirChoose exec for one result, matchAll for all results with captures, and a non-global test for an independent existence check.
Acknowledgements
This post was created with assistance from AI.