Skip to examples

A visual reference · Regular expressions

Regular expressions,
one change at a time

Choose a regex issue, then compare its actual syntax in plain BRE, grep -E, Perl, sed and Emacs. The input stays fixed; the tool dialect is explicit. Groups and replacement references are the centre of the tour.

Each issue ID contains all five dialect views on one shared input. The matched text and replacement result appear once. A changed assumption or additional check stays inside the same issue; commands and verification code are available on demand. All six native programs per issue are checked against authored expectations.

Last changes: 2026-11-01. Added log extraction, DOI fields, filename transformations, text cleanup, date conversion and URL migrations, including counterexamples and calendar checks. One ID contains every dialect variation.

Download PDFRendered tour · read offline
24regex issues
144native runs
1canonical Org source

Reading the tour

One issue, five views

Start with the issue

Read the shared input and the comparison table. Identify the group, quantifier or reference before reading the command wrapper.

Compare exact spellings

Plain means POSIX BRE as used by grep without -E. It does not mean a universal regex language. Explicit flags and notes explain the differences.

Replace and verify

grep selects complete matches. Perl, sed and Emacs transform the same input; compare their results and then change one assumption.

Every topic has the same five views. The sed column uses default BRE where it can express the example; it explicitly selects sed -E where ERE alternation is needed. A multiline BRE cell lists separate grep -e patterns. No fixed-string example is presented as a regex function.

Capture and replacement syntax

Feature Plain / BRE grep -E Perl sed Emacs
Capture \(...\) (...) (...) \(...\); (...) with -E \(...\)
One or more \{1,\} + + \{1,\}; + with -E +
Optional \{0,1\} ? ? \{0,1\}; ? with -E ?
Bounded repeat \{2,3\} {2,3} {2,3} \{2,3\}; {2,3} with -E \{2,3\}
Replace with group 1 Search only Search only $1 \1 \1
Replace with whole match Search only Search only $& & \&

There is no universal plain-regex syntax. POSIX BRE has no alternation operator; ERE has no noncapturing groups or named groups. Pattern backreferences in grep -E are a tested GNU/BSD extension, not POSIX ERE. Each issue shows supported alternatives without claiming identical syntax.

A reference inside a search pattern constrains what can match. A reference inside replacement text inserts captured text. Number groups by opening parentheses, including nested groups.

Search and replace directly inside Emacs

Open the supplied text in a buffer. With no active region, move to the buffer start using M-<. The Emacs examples show two separate, directly copyable fields.

  • Run M-x replace-regexp, enter Search regexp and press RET, then enter Replacement and press RET. Every match from point onward is replaced.
  • To inspect matches first, use the same Search regexp with M-x re-search-forward.
  • For confirmation at each match, use C-M-% (M-x query-replace-regexp). Press ! to replace all remaining matches.

For the case-sensitive results here, use M-x set-variable to set case-fold-search and case-replace to nil. Leave replace-regexp-lax-whitespace at its default nil: one space means one space. Type backslashes literally, without Lisp string quotes.

For Doe, Jane, Search regexp ^\([A-Za-z]+\), \([A-Za-z]+\)$ defines surname as group 1 and given name as group 2. Replacement \2 \1 produces Jane Doe. To insert literal backslash and 1, type \\1 instead.

The verified result is the resulting text from an automated native reference run, not an Emacs minibuffer transcript. The exact displayed fields are passed unchanged to Emacs. Its Lisp implementation is confined to the collapsible verification program and downloadable script.

Scope and primary references

Inputs are original ASCII teaching text with LF line endings and LC_ALL=C. grep -o is a GNU/BSD extension. sed -E is stated wherever it is used. Same replacement text is not assumed across tools: Perl uses $1, sed and Emacs use backslash references, and whole-match references differ again. Named groups and minimal quantifiers are not invented for engines that lack them. Date-shaped strings are not calendar validation; flat tags are not an HTML parser. The native examples run offline and need no account or personal editor setup.

The examples, explanations, data and independently authored expected results are original teaching material. The manuals establish tool semantics.

RGX-01-001

Literal characters and metacharacters

A dot is a wildcard until a character class makes it literal. All five views can use [.] for a period. The search is unanchored, so the prefix of report.csv.bak also matches; replacement leaves its suffix intact.

Shared input · data/periods.txt
report.csv
reportXcsv
report.csv.bak
README
REPORT.csv
All variations for RGX-01-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
report[.]csv
Search only[.] makes the period literal in BRE.
grep -E / ERE
report[.]csv
Search onlyThe literal-period spelling is unchanged in ERE.
Perl
report[.]csv
report.tsv
[.] is literal in the search; report.tsv is replacement text.
sed / BRE
report[.]csv
report.tsv
The same [.] search works in default sed BRE.
Emacs
report[.]csv
report.tsv
Enter [.] literally; do not add string quotes.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

report.csv
report.csv

After replacement · Perl, sed and Emacs

report.tsv
reportXcsv
report.tsv.bak
README
REPORT.csv
Change one assumption · Let a wildcard match a period

Remove the literal-dot character class. The wildcard also changes reportXcsv, which the literal pattern left alone.

Changed regexp

report.csv

Replacement

report.tsv

Changed result

report.tsv
report.tsv
report.tsv.bak
README
REPORT.csv

Change one assumption: Let a wildcard match a period · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o 'report[.]csv' data/periods.txt

grep -E / ERE

grep -Eo 'report[.]csv' data/periods.txt

Perl

perl -pe '
s{report[.]csv}{report.tsv}g;
' data/periods.txt

sed / BRE

sed 's~report[.]csv~report.tsv~g' data/periods.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-01-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-01-001-bre-command",
      "program_block": "RGX-01-001-bre-run",
      "note": "[.] makes the period literal in BRE."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-01-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-01-001-ere-command",
      "program_block": "RGX-01-001-ere-run",
      "note": "The literal-period spelling is unchanged in ERE."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-01-001-perl-pattern",
      "replacement_block": "RGX-01-001-perl-replacement",
      "command_block": "RGX-01-001-perl-command",
      "program_block": "RGX-01-001-perl-run",
      "note": "[.] is literal in the search; report.tsv is replacement text."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-01-001-sed-pattern",
      "replacement_block": "RGX-01-001-sed-replacement",
      "command_block": "RGX-01-001-sed-command",
      "program_block": "RGX-01-001-sed-run",
      "note": "The same [.] search works in default sed BRE."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-01-001-emacs-pattern",
      "replacement_block": "RGX-01-001-emacs-replacement",
      "command_block": "RGX-01-001-emacs-command",
      "program_block": "RGX-01-001-emacs-run",
      "note": "Enter [.] literally; do not add string quotes."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Let a wildcard match a period",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-01-001-change-pattern",
    "replacement_block": "RGX-01-001-change-replacement",
    "command_block": "RGX-01-001-change-command",
    "program_block": "RGX-01-001-change-run",
    "note": "Remove the literal-dot character class. The wildcard also changes reportXcsv, which the literal pattern left alone.",
    "goal": "Remove the literal-dot character class. The wildcard also changes reportXcsv, which the literal pattern left alone."
  },
  "aliases": [
    "RGX-01-002",
    "RGX-01-003",
    "RGX-01-004",
    "RGX-01-005",
    "RGX-01-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/periods.txt <<'RGX_INPUT'
report.csv
reportXcsv
report.csv.bak
README
REPORT.csv
RGX_INPUT
grep -o 'report[.]csv' data/periods.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/periods.txt <<'RGX_INPUT'
report.csv
reportXcsv
report.csv.bak
README
REPORT.csv
RGX_INPUT
grep -Eo 'report[.]csv' data/periods.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/periods.txt <<'RGX_INPUT'
report.csv
reportXcsv
report.csv.bak
README
REPORT.csv
RGX_INPUT
perl -pe '
s{report[.]csv}{report.tsv}g;
' data/periods.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/periods.txt <<'RGX_INPUT'
report.csv
reportXcsv
report.csv.bak
README
REPORT.csv
RGX_INPUT
sed 's~report[.]csv~report.tsv~g' data/periods.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/periods.txt <<'RGX_INPUT'
report.csv
reportXcsv
report.csv.bak
README
REPORT.csv
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
report[.]csv
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
report.tsv
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/periods.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/periods.txt <<'RGX_INPUT'
report.csv
reportXcsv
report.csv.bak
README
REPORT.csv
RGX_INPUT
perl -pe '
s{report.csv}{report.tsv}g;
' data/periods.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-02-001

Anchor the complete record

Anchors require a complete record. BRE and Emacs escape the repetition braces; ERE and Perl do not. The accepted identifiers are the same, despite those different spellings.

Shared input · data/anchors.txt
AB-123
xxAB-123yy
AB-12
xy-123
ZZ-999
AB-123tail
All variations for RGX-02-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^[A-Z]\{2\}-[0-9]\{3\}$
Search onlyEscape the count braces: \{2\} and \{3\}.
grep -E / ERE
^[A-Z]{2}-[0-9]{3}$
Search onlyUse ordinary count braces: {2} and {3}.
Perl
^[A-Z]{2}-[0-9]{3}$
[accepted]
Anchors and ordinary count braces match the whole record.
sed / BRE
^[A-Z]\{2\}-[0-9]\{3\}$
[accepted]
Default sed uses escaped BRE count braces.
Emacs
^[A-Z]\{2\}-[0-9]\{3\}$
[accepted]
Emacs also needs \{2\} and \{3\}.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

AB-123
ZZ-999

After replacement · Perl, sed and Emacs

[accepted]
xxAB-123yy
AB-12
xy-123
[accepted]
AB-123tail
Change one assumption · Search without whole-record anchors

Remove the anchors. The expression now extracts identifiers embedded in longer records.

Changed regexp

[A-Z]{2}-[0-9]{3}

Changed result

AB-123
AB-123
ZZ-999
AB-123

Change one assumption: Search without whole-record anchors · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^[A-Z]\{2\}-[0-9]\{3\}$' data/anchors.txt

grep -E / ERE

grep -Eo '^[A-Z]{2}-[0-9]{3}$' data/anchors.txt

Perl

perl -pe '
s{^[A-Z]{2}-[0-9]{3}$}{[accepted]}g;
' data/anchors.txt

sed / BRE

sed 's~^[A-Z]\{2\}-[0-9]\{3\}$~[accepted]~g' data/anchors.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-02-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-02-001-bre-command",
      "program_block": "RGX-02-001-bre-run",
      "note": "Escape the count braces: \\{2\\} and \\{3\\}."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-02-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-02-001-ere-command",
      "program_block": "RGX-02-001-ere-run",
      "note": "Use ordinary count braces: {2} and {3}."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-02-001-perl-pattern",
      "replacement_block": "RGX-02-001-perl-replacement",
      "command_block": "RGX-02-001-perl-command",
      "program_block": "RGX-02-001-perl-run",
      "note": "Anchors and ordinary count braces match the whole record."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-02-001-sed-pattern",
      "replacement_block": "RGX-02-001-sed-replacement",
      "command_block": "RGX-02-001-sed-command",
      "program_block": "RGX-02-001-sed-run",
      "note": "Default sed uses escaped BRE count braces."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-02-001-emacs-pattern",
      "replacement_block": "RGX-02-001-emacs-replacement",
      "command_block": "RGX-02-001-emacs-command",
      "program_block": "RGX-02-001-emacs-run",
      "note": "Emacs also needs \\{2\\} and \\{3\\}."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Search without whole-record anchors",
    "tool": "grep",
    "engine": "grep ERE (GNU/BSD -o)",
    "pattern_block": "RGX-02-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-02-001-change-command",
    "program_block": "RGX-02-001-change-run",
    "note": "Remove the anchors. The expression now extracts identifiers embedded in longer records.",
    "goal": "Remove the anchors. The expression now extracts identifiers embedded in longer records."
  },
  "aliases": [
    "RGX-02-002",
    "RGX-02-003",
    "RGX-02-004",
    "RGX-02-005",
    "RGX-02-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/anchors.txt <<'RGX_INPUT'
AB-123
xxAB-123yy
AB-12
xy-123
ZZ-999
AB-123tail
RGX_INPUT
grep -o '^[A-Z]\{2\}-[0-9]\{3\}$' data/anchors.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/anchors.txt <<'RGX_INPUT'
AB-123
xxAB-123yy
AB-12
xy-123
ZZ-999
AB-123tail
RGX_INPUT
grep -Eo '^[A-Z]{2}-[0-9]{3}$' data/anchors.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/anchors.txt <<'RGX_INPUT'
AB-123
xxAB-123yy
AB-12
xy-123
ZZ-999
AB-123tail
RGX_INPUT
perl -pe '
s{^[A-Z]{2}-[0-9]{3}$}{[accepted]}g;
' data/anchors.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/anchors.txt <<'RGX_INPUT'
AB-123
xxAB-123yy
AB-12
xy-123
ZZ-999
AB-123tail
RGX_INPUT
sed 's~^[A-Z]\{2\}-[0-9]\{3\}$~[accepted]~g' data/anchors.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/anchors.txt <<'RGX_INPUT'
AB-123
xxAB-123yy
AB-12
xy-123
ZZ-999
AB-123tail
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^[A-Z]\{2\}-[0-9]\{3\}$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
[accepted]
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/anchors.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/anchors.txt <<'RGX_INPUT'
AB-123
xxAB-123yy
AB-12
xy-123
ZZ-999
AB-123tail
RGX_INPUT
grep -Eo '[A-Z]{2}-[0-9]{3}' data/anchors.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-03-001

Choose a character class

A character class chooses one character; a quantifier repeats it. Use ASCII [0-9] for this agreed input. Do not copy Perl backslash-d into another engine and assume it means digits there.

Shared input · data/classes.txt
t=15
t=7
t=x
t=1.5
t=22
All variations for RGX-03-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^t=[0-9]\{1,\}$
Search onlyBRE repeats digits with \{1,\}.
grep -E / ERE
^t=[0-9]+$
Search onlyERE uses + for one or more digits.
Perl
^t=[0-9]+$
t=#
Use ASCII [0-9]; Perl \d is not universal syntax.
sed / BRE
^t=[0-9]\{1,\}$
t=#
Default sed uses \{1,\}, not ERE +.
Emacs
^t=[0-9]+$
t=#
Emacs supports + directly.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

t=15
t=7
t=22

After replacement · Perl, sed and Emacs

t=#
t=#
t=x
t=1.5
t=#
Change one assumption · Try the POSIX digit class

Use the POSIX digit class instead of an ASCII range. On these ASCII inputs the result is identical; this does not make Perl-style backslash-d portable.

Changed regexp

^t=[[:digit:]]+$

Changed result

t=15
t=7
t=22

Change one assumption: Try the POSIX digit class · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^t=[0-9]\{1,\}$' data/classes.txt

grep -E / ERE

grep -Eo '^t=[0-9]+$' data/classes.txt

Perl

perl -pe '
s{^t=[0-9]+$}{t=#}g;
' data/classes.txt

sed / BRE

sed 's~^t=[0-9]\{1,\}$~t=#~g' data/classes.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-03-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-03-001-bre-command",
      "program_block": "RGX-03-001-bre-run",
      "note": "BRE repeats digits with \\{1,\\}."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-03-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-03-001-ere-command",
      "program_block": "RGX-03-001-ere-run",
      "note": "ERE uses + for one or more digits."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-03-001-perl-pattern",
      "replacement_block": "RGX-03-001-perl-replacement",
      "command_block": "RGX-03-001-perl-command",
      "program_block": "RGX-03-001-perl-run",
      "note": "Use ASCII [0-9]; Perl \\d is not universal syntax."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-03-001-sed-pattern",
      "replacement_block": "RGX-03-001-sed-replacement",
      "command_block": "RGX-03-001-sed-command",
      "program_block": "RGX-03-001-sed-run",
      "note": "Default sed uses \\{1,\\}, not ERE +."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-03-001-emacs-pattern",
      "replacement_block": "RGX-03-001-emacs-replacement",
      "command_block": "RGX-03-001-emacs-command",
      "program_block": "RGX-03-001-emacs-run",
      "note": "Emacs supports + directly."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Try the POSIX digit class",
    "tool": "grep",
    "engine": "grep ERE (GNU/BSD -o)",
    "pattern_block": "RGX-03-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-03-001-change-command",
    "program_block": "RGX-03-001-change-run",
    "note": "Use the POSIX digit class instead of an ASCII range. On these ASCII inputs the result is identical; this does not make Perl-style backslash-d portable.",
    "goal": "Use the POSIX digit class instead of an ASCII range. On these ASCII inputs the result is identical; this does not make Perl-style backslash-d portable."
  },
  "aliases": [
    "RGX-03-002",
    "RGX-03-003",
    "RGX-03-004",
    "RGX-03-005",
    "RGX-03-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/classes.txt <<'RGX_INPUT'
t=15
t=7
t=x
t=1.5
t=22
RGX_INPUT
grep -o '^t=[0-9]\{1,\}$' data/classes.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/classes.txt <<'RGX_INPUT'
t=15
t=7
t=x
t=1.5
t=22
RGX_INPUT
grep -Eo '^t=[0-9]+$' data/classes.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/classes.txt <<'RGX_INPUT'
t=15
t=7
t=x
t=1.5
t=22
RGX_INPUT
perl -pe '
s{^t=[0-9]+$}{t=#}g;
' data/classes.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/classes.txt <<'RGX_INPUT'
t=15
t=7
t=x
t=1.5
t=22
RGX_INPUT
sed 's~^t=[0-9]\{1,\}$~t=#~g' data/classes.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/classes.txt <<'RGX_INPUT'
t=15
t=7
t=x
t=1.5
t=22
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^t=[0-9]+$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
t=#
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/classes.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/classes.txt <<'RGX_INPUT'
t=15
t=7
t=x
t=1.5
t=22
RGX_INPUT
grep -Eo '^t=[[:digit:]]+$' data/classes.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-04-001

Make parts optional or bounded

The optional u accepts color and colour. BRE spells optional repetition as backslash-brace 0,1; ERE, Perl and Emacs use ?. Bounded counts still need escaped braces in BRE and Emacs.

Shared input · data/quantifiers.txt
color=12
colour=123
colouur=12
colour=1
color=1234
colour=99
All variations for RGX-04-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^colou\{0,1\}r=[0-9]\{2,3\}$
Search onlyOptional u is u\{0,1\}; digit counts use \{2,3\}.
grep -E / ERE
^colou?r=[0-9]{2,3}$
Search onlyOptional u is u?; digit counts use {2,3}.
Perl
^colou?r=[0-9]{2,3}$
color=#
Perl uses the same ? and {2,3} spelling as ERE.
sed / BRE
^colou\{0,1\}r=[0-9]\{2,3\}$
color=#
BRE counts stay escaped in default sed.
Emacs
^colou?r=[0-9]\{2,3\}$
color=#
Emacs uses u? but escapes the count braces.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

color=12
colour=123
colour=99

After replacement · Perl, sed and Emacs

color=#
color=#
colouur=12
colour=1
color=1234
color=#
Change one assumption · Narrow the repetition bound

Require exactly two digits instead of two or three. Records with three digits are excluded.

Changed regexp

^colou?r=[0-9]{2}$

Changed result

color=12
colour=99

Change one assumption: Narrow the repetition bound · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^colou\{0,1\}r=[0-9]\{2,3\}$' data/quantifiers.txt

grep -E / ERE

grep -Eo '^colou?r=[0-9]{2,3}$' data/quantifiers.txt

Perl

perl -pe '
s{^colou?r=[0-9]{2,3}$}{color=#}g;
' data/quantifiers.txt

sed / BRE

sed 's~^colou\{0,1\}r=[0-9]\{2,3\}$~color=#~g' data/quantifiers.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-04-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-04-001-bre-command",
      "program_block": "RGX-04-001-bre-run",
      "note": "Optional u is u\\{0,1\\}; digit counts use \\{2,3\\}."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-04-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-04-001-ere-command",
      "program_block": "RGX-04-001-ere-run",
      "note": "Optional u is u?; digit counts use {2,3}."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-04-001-perl-pattern",
      "replacement_block": "RGX-04-001-perl-replacement",
      "command_block": "RGX-04-001-perl-command",
      "program_block": "RGX-04-001-perl-run",
      "note": "Perl uses the same ? and {2,3} spelling as ERE."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-04-001-sed-pattern",
      "replacement_block": "RGX-04-001-sed-replacement",
      "command_block": "RGX-04-001-sed-command",
      "program_block": "RGX-04-001-sed-run",
      "note": "BRE counts stay escaped in default sed."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-04-001-emacs-pattern",
      "replacement_block": "RGX-04-001-emacs-replacement",
      "command_block": "RGX-04-001-emacs-command",
      "program_block": "RGX-04-001-emacs-run",
      "note": "Emacs uses u? but escapes the count braces."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Narrow the repetition bound",
    "tool": "grep",
    "engine": "grep ERE (GNU/BSD -o)",
    "pattern_block": "RGX-04-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-04-001-change-command",
    "program_block": "RGX-04-001-change-run",
    "note": "Require exactly two digits instead of two or three. Records with three digits are excluded.",
    "goal": "Require exactly two digits instead of two or three. Records with three digits are excluded."
  },
  "aliases": [
    "RGX-04-002",
    "RGX-04-003",
    "RGX-04-004",
    "RGX-04-005",
    "RGX-04-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/quantifiers.txt <<'RGX_INPUT'
color=12
colour=123
colouur=12
colour=1
color=1234
colour=99
RGX_INPUT
grep -o '^colou\{0,1\}r=[0-9]\{2,3\}$' data/quantifiers.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/quantifiers.txt <<'RGX_INPUT'
color=12
colour=123
colouur=12
colour=1
color=1234
colour=99
RGX_INPUT
grep -Eo '^colou?r=[0-9]{2,3}$' data/quantifiers.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/quantifiers.txt <<'RGX_INPUT'
color=12
colour=123
colouur=12
colour=1
color=1234
colour=99
RGX_INPUT
perl -pe '
s{^colou?r=[0-9]{2,3}$}{color=#}g;
' data/quantifiers.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/quantifiers.txt <<'RGX_INPUT'
color=12
colour=123
colouur=12
colour=1
color=1234
colour=99
RGX_INPUT
sed 's~^colou\{0,1\}r=[0-9]\{2,3\}$~color=#~g' data/quantifiers.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/quantifiers.txt <<'RGX_INPUT'
color=12
colour=123
colouur=12
colour=1
color=1234
colour=99
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^colou?r=[0-9]\{2,3\}$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
color=#
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/quantifiers.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/quantifiers.txt <<'RGX_INPUT'
color=12
colour=123
colouur=12
colour=1
color=1234
colour=99
RGX_INPUT
grep -Eo '^colou?r=[0-9]{2}$' data/quantifiers.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-05-001

Group alternatives before anchoring

Group the cat/dog choice before anchoring the whole record. POSIX BRE has no alternation operator, so its view uses two separate patterns. sed explicitly enables ERE with -E. Emacs uses backslashed parentheses and alternation.

Shared input · data/alternation.txt
cat
dog
cats
dogs
bird
catdog
catX
All variations for RGX-05-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^cats\{0,1\}$
^dogs\{0,1\}$
Search onlyBRE has no alternation operator; pass two separate -e patterns.
grep -E / ERE
^(cat|dog)s?$
Search onlyOrdinary parentheses group | before the anchors.
Perl
^(cat|dog)s?$
kind=$1
The ordinary capture group supplies $1.
sed -E / ERE
^(cat|dog)s?$
kind=\1
-E enables ERE alternation; replacement uses \1.
Emacs
^\(cat\|dog\)s?$
kind=\1
Use \| for alternation and \( ... \) for the capture.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

cat
dog
cats
dogs

After replacement · Perl, sed and Emacs

kind=cat
kind=dog
kind=cat
kind=dog
bird
catdog
catX
Change one assumption · Remove the alternation group

Remove the alternation group. The beginning anchor applies only to cat, and the ending anchor only to dogs?.

Changed regexp

^cat|dogs?$

Changed result

cat
dog
cat
dogs
cat
dog
cat

Change one assumption: Remove the alternation group · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o -e '^cats\{0,1\}$' -e '^dogs\{0,1\}$' data/alternation.txt

grep -E / ERE

grep -Eo '^(cat|dog)s?$' data/alternation.txt

Perl

perl -pe '
s{^(cat|dog)s?$}{kind=$1}g;
' data/alternation.txt

sed -E / ERE

sed -E 's~^(cat|dog)s?$~kind=\1~g' data/alternation.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-05-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-05-001-bre-command",
      "program_block": "RGX-05-001-bre-run",
      "note": "BRE has no alternation operator; pass two separate -e patterns."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-05-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-05-001-ere-command",
      "program_block": "RGX-05-001-ere-run",
      "note": "Ordinary parentheses group | before the anchors."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-05-001-perl-pattern",
      "replacement_block": "RGX-05-001-perl-replacement",
      "command_block": "RGX-05-001-perl-command",
      "program_block": "RGX-05-001-perl-run",
      "note": "The ordinary capture group supplies $1."
    },
    {
      "key": "sed",
      "label": "sed -E / ERE",
      "tool": "sed",
      "engine": "sed ERE (-E)",
      "pattern_block": "RGX-05-001-sed-pattern",
      "replacement_block": "RGX-05-001-sed-replacement",
      "command_block": "RGX-05-001-sed-command",
      "program_block": "RGX-05-001-sed-run",
      "note": "-E enables ERE alternation; replacement uses \\1."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-05-001-emacs-pattern",
      "replacement_block": "RGX-05-001-emacs-replacement",
      "command_block": "RGX-05-001-emacs-command",
      "program_block": "RGX-05-001-emacs-run",
      "note": "Use \\| for alternation and \\( ... \\) for the capture."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Remove the alternation group",
    "tool": "grep",
    "engine": "grep ERE (GNU/BSD -o)",
    "pattern_block": "RGX-05-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-05-001-change-command",
    "program_block": "RGX-05-001-change-run",
    "note": "Remove the alternation group. The beginning anchor applies only to cat, and the ending anchor only to dogs?.",
    "goal": "Remove the alternation group. The beginning anchor applies only to cat, and the ending anchor only to dogs?."
  },
  "aliases": [
    "RGX-05-002",
    "RGX-05-003",
    "RGX-05-004",
    "RGX-05-005",
    "RGX-05-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/alternation.txt <<'RGX_INPUT'
cat
dog
cats
dogs
bird
catdog
catX
RGX_INPUT
grep -o -e '^cats\{0,1\}$' -e '^dogs\{0,1\}$' data/alternation.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/alternation.txt <<'RGX_INPUT'
cat
dog
cats
dogs
bird
catdog
catX
RGX_INPUT
grep -Eo '^(cat|dog)s?$' data/alternation.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/alternation.txt <<'RGX_INPUT'
cat
dog
cats
dogs
bird
catdog
catX
RGX_INPUT
perl -pe '
s{^(cat|dog)s?$}{kind=$1}g;
' data/alternation.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/alternation.txt <<'RGX_INPUT'
cat
dog
cats
dogs
bird
catdog
catX
RGX_INPUT
sed -E 's~^(cat|dog)s?$~kind=\1~g' data/alternation.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/alternation.txt <<'RGX_INPUT'
cat
dog
cats
dogs
bird
catdog
catX
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\(cat\|dog\)s?$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
kind=\1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/alternation.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/alternation.txt <<'RGX_INPUT'
cat
dog
cats
dogs
bird
catdog
catX
RGX_INPUT
grep -Eo '^cat|dogs?$' data/alternation.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-06-001

Define groups and swap their values

Define surname as group 1 and given name as group 2. BRE, default sed and Emacs use backslashed parentheses; ERE and Perl use ordinary parentheses. Perl replaces with $2 $1; sed and Emacs use backslash-2, space, backslash-1. Doe, Jane becomes Jane Doe.

Shared input · data/names.txt
Doe, Jane
Smith, Sam
No comma
Li, Ada
All variations for RGX-06-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([A-Za-z]\{1,\}\), \([A-Za-z]\{1,\}\)$
Search onlyUse \( ... \) and \{1,\}; grep prints the whole match.
grep -E / ERE
^([A-Za-z]+), ([A-Za-z]+)$
Search onlyUse (...) and +; grep still cannot replace captures.
Perl
^([A-Za-z]+), ([A-Za-z]+)$
$2 $1
Replacement $2 $1 swaps the two captures.
sed / BRE
^\([A-Za-z]\{1,\}\), \([A-Za-z]\{1,\}\)$
\2 \1
Default sed uses BRE groups; replacement is \2 \1.
Emacs
^\([A-Za-z]+\), \([A-Za-z]+\)$
\2 \1
Emacs groups are \( ... \); replacement is \2 \1.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

Doe, Jane
Smith, Sam
Li, Ada

After replacement · Perl, sed and Emacs

Jane Doe
Sam Smith
No comma
Ada Li
Change one assumption · Reuse a capture twice

Reuse a captured value twice in replacement text: the surname appears after the given name and again in parentheses.

Changed regexp

^([A-Za-z]+), ([A-Za-z]+)$

Replacement

$2 $1 ($1)

Changed result

Jane Doe (Doe)
Sam Smith (Smith)
No comma
Ada Li (Li)

Change one assumption: Reuse a capture twice · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([A-Za-z]\{1,\}\), \([A-Za-z]\{1,\}\)$' data/names.txt

grep -E / ERE

grep -Eo '^([A-Za-z]+), ([A-Za-z]+)$' data/names.txt

Perl

perl -pe '
s{^([A-Za-z]+), ([A-Za-z]+)$}{$2 $1}g;
' data/names.txt

sed / BRE

sed 's~^\([A-Za-z]\{1,\}\), \([A-Za-z]\{1,\}\)$~\2 \1~g' data/names.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-06-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-06-001-bre-command",
      "program_block": "RGX-06-001-bre-run",
      "note": "Use \\( ... \\) and \\{1,\\}; grep prints the whole match."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-06-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-06-001-ere-command",
      "program_block": "RGX-06-001-ere-run",
      "note": "Use (...) and +; grep still cannot replace captures."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-06-001-perl-pattern",
      "replacement_block": "RGX-06-001-perl-replacement",
      "command_block": "RGX-06-001-perl-command",
      "program_block": "RGX-06-001-perl-run",
      "note": "Replacement $2 $1 swaps the two captures."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-06-001-sed-pattern",
      "replacement_block": "RGX-06-001-sed-replacement",
      "command_block": "RGX-06-001-sed-command",
      "program_block": "RGX-06-001-sed-run",
      "note": "Default sed uses BRE groups; replacement is \\2 \\1."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-06-001-emacs-pattern",
      "replacement_block": "RGX-06-001-emacs-replacement",
      "command_block": "RGX-06-001-emacs-command",
      "program_block": "RGX-06-001-emacs-run",
      "note": "Emacs groups are \\( ... \\); replacement is \\2 \\1."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Reuse a capture twice",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-06-001-change-pattern",
    "replacement_block": "RGX-06-001-change-replacement",
    "command_block": "RGX-06-001-change-command",
    "program_block": "RGX-06-001-change-run",
    "note": "Reuse a captured value twice in replacement text: the surname appears after the given name and again in parentheses.",
    "goal": "Reuse a captured value twice in replacement text: the surname appears after the given name and again in parentheses."
  },
  "aliases": [
    "RGX-06-002",
    "RGX-06-003",
    "RGX-06-004",
    "RGX-06-005",
    "RGX-06-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/names.txt <<'RGX_INPUT'
Doe, Jane
Smith, Sam
No comma
Li, Ada
RGX_INPUT
grep -o '^\([A-Za-z]\{1,\}\), \([A-Za-z]\{1,\}\)$' data/names.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/names.txt <<'RGX_INPUT'
Doe, Jane
Smith, Sam
No comma
Li, Ada
RGX_INPUT
grep -Eo '^([A-Za-z]+), ([A-Za-z]+)$' data/names.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/names.txt <<'RGX_INPUT'
Doe, Jane
Smith, Sam
No comma
Li, Ada
RGX_INPUT
perl -pe '
s{^([A-Za-z]+), ([A-Za-z]+)$}{$2 $1}g;
' data/names.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/names.txt <<'RGX_INPUT'
Doe, Jane
Smith, Sam
No comma
Li, Ada
RGX_INPUT
sed 's~^\([A-Za-z]\{1,\}\), \([A-Za-z]\{1,\}\)$~\2 \1~g' data/names.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/names.txt <<'RGX_INPUT'
Doe, Jane
Smith, Sam
No comma
Li, Ada
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([A-Za-z]+\), \([A-Za-z]+\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\2 \1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/names.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/names.txt <<'RGX_INPUT'
Doe, Jane
Smith, Sam
No comma
Li, Ada
RGX_INPUT
perl -pe '
s{^([A-Za-z]+), ([A-Za-z]+)$}{$2 $1 ($1)}g;
' data/names.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-07-001

Number nested capture groups

Nested opening parentheses determine numbering. The outer year-month group is 1; year is 2, month is 3 and day is 4. Keep the same intended groups while changing syntax between dialects. These date-shaped strings are not calendar validation.

Shared input · data/nested.txt
2026-11-01
2025-02-03
2026/11/01
date=2026-11-01
All variations for RGX-07-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$
Search onlyThe outer year-month capture is group 1.
grep -E / ERE
^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$
Search onlyOrdinary parentheses keep the same nested numbering.
Perl
^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$
$4/$3/$2
Reorder day/month/year with $4/$3/$2.
sed / BRE
^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$
\4/\3/\2
BRE groups keep numbering; replace with \4/\3/\2.
Emacs
^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$
\4/\3/\2
Use \4/\3/\2; do not count non-group punctuation.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

2026-11-01
2025-02-03

After replacement · Perl, sed and Emacs

01/11/2026
03/02/2025
2026/11/01
date=2026-11-01
Change one assumption · Choose the wrong nested group numbers

Use the wrong nested group numbers. Read the incorrect date output to identify which opening parenthesis each number names.

Changed regexp

^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$

Replacement

$3/$2/$1

Changed result

11/2026/2026-11
02/2025/2025-02
2026/11/01
date=2026-11-01

Change one assumption: Choose the wrong nested group numbers · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$' data/nested.txt

grep -E / ERE

grep -Eo '^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$' data/nested.txt

Perl

perl -pe '
s{^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$}{$4/$3/$2}g;
' data/nested.txt

sed / BRE

sed 's~^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$~\4/\3/\2~g' data/nested.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-07-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-07-001-bre-command",
      "program_block": "RGX-07-001-bre-run",
      "note": "The outer year-month capture is group 1."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-07-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-07-001-ere-command",
      "program_block": "RGX-07-001-ere-run",
      "note": "Ordinary parentheses keep the same nested numbering."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-07-001-perl-pattern",
      "replacement_block": "RGX-07-001-perl-replacement",
      "command_block": "RGX-07-001-perl-command",
      "program_block": "RGX-07-001-perl-run",
      "note": "Reorder day/month/year with $4/$3/$2."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-07-001-sed-pattern",
      "replacement_block": "RGX-07-001-sed-replacement",
      "command_block": "RGX-07-001-sed-command",
      "program_block": "RGX-07-001-sed-run",
      "note": "BRE groups keep numbering; replace with \\4/\\3/\\2."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-07-001-emacs-pattern",
      "replacement_block": "RGX-07-001-emacs-replacement",
      "command_block": "RGX-07-001-emacs-command",
      "program_block": "RGX-07-001-emacs-run",
      "note": "Use \\4/\\3/\\2; do not count non-group punctuation."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Choose the wrong nested group numbers",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-07-001-change-pattern",
    "replacement_block": "RGX-07-001-change-replacement",
    "command_block": "RGX-07-001-change-command",
    "program_block": "RGX-07-001-change-run",
    "note": "Use the wrong nested group numbers. Read the incorrect date output to identify which opening parenthesis each number names.",
    "goal": "Use the wrong nested group numbers. Read the incorrect date output to identify which opening parenthesis each number names."
  },
  "aliases": [
    "RGX-07-002",
    "RGX-07-003",
    "RGX-07-004",
    "RGX-07-005",
    "RGX-07-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/nested.txt <<'RGX_INPUT'
2026-11-01
2025-02-03
2026/11/01
date=2026-11-01
RGX_INPUT
grep -o '^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$' data/nested.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/nested.txt <<'RGX_INPUT'
2026-11-01
2025-02-03
2026/11/01
date=2026-11-01
RGX_INPUT
grep -Eo '^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$' data/nested.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/nested.txt <<'RGX_INPUT'
2026-11-01
2025-02-03
2026/11/01
date=2026-11-01
RGX_INPUT
perl -pe '
s{^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$}{$4/$3/$2}g;
' data/nested.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/nested.txt <<'RGX_INPUT'
2026-11-01
2025-02-03
2026/11/01
date=2026-11-01
RGX_INPUT
sed 's~^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$~\4/\3/\2~g' data/nested.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/nested.txt <<'RGX_INPUT'
2026-11-01
2025-02-03
2026/11/01
date=2026-11-01
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\(\([0-9]\{4\}\)-\([0-9]\{2\}\)\)-\([0-9]\{2\}\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\4/\3/\2
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/nested.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/nested.txt <<'RGX_INPUT'
2026-11-01
2025-02-03
2026/11/01
date=2026-11-01
RGX_INPUT
perl -pe '
s{^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$}{$3/$2/$1}g;
' data/nested.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-08-001

Refer back to text already matched

A backreference inside the pattern requires the same word twice. It is distinct from inserting a group in replacement text. BRE, Perl, sed and Emacs support the example; grep -E uses a tested GNU/BSD extension outside POSIX ERE.

Shared input · data/duplicates.txt
red red
blue blue
red blue
red red red
red  red
All variations for RGX-08-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([a-z]\{1,\}\) \1$
Search only\1 in the pattern is a POSIX BRE backreference.
grep -E / ERE
^([a-z]+) \1$
Search onlyPattern backreferences are a GNU/BSD extension to ERE, not POSIX ERE.
Perl
^([a-z]+) \1$
$1
\1 in the pattern tests equality; $1 inserts the word.
sed / BRE
^\([a-z]\{1,\}\) \1$
\1
\1 tests equality in the pattern and inserts text in replacement.
Emacs
^\([a-z]+\) \1$
\1
The pattern and replacement both use \1, for different jobs.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

red red
blue blue

After replacement · Perl, sed and Emacs

red
blue
red blue
red red red
red  red
Change one assumption · Capture words without requiring equality

Capture two independent words instead of using a pattern backreference. Different words now match too.

Changed regexp

^([a-z]+) ([a-z]+)$

Changed result

red|red
blue|blue
red|blue

Change one assumption: Capture words without requiring equality · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([a-z]\{1,\}\) \1$' data/duplicates.txt

grep -E / ERE

grep -Eo '^([a-z]+) \1$' data/duplicates.txt

Perl

perl -pe '
s{^([a-z]+) \1$}{$1}g;
' data/duplicates.txt

sed / BRE

sed 's~^\([a-z]\{1,\}\) \1$~\1~g' data/duplicates.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-08-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-08-001-bre-command",
      "program_block": "RGX-08-001-bre-run",
      "note": "\\1 in the pattern is a POSIX BRE backreference."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-08-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-08-001-ere-command",
      "program_block": "RGX-08-001-ere-run",
      "note": "Pattern backreferences are a GNU/BSD extension to ERE, not POSIX ERE."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-08-001-perl-pattern",
      "replacement_block": "RGX-08-001-perl-replacement",
      "command_block": "RGX-08-001-perl-command",
      "program_block": "RGX-08-001-perl-run",
      "note": "\\1 in the pattern tests equality; $1 inserts the word."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-08-001-sed-pattern",
      "replacement_block": "RGX-08-001-sed-replacement",
      "command_block": "RGX-08-001-sed-command",
      "program_block": "RGX-08-001-sed-run",
      "note": "\\1 tests equality in the pattern and inserts text in replacement."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-08-001-emacs-pattern",
      "replacement_block": "RGX-08-001-emacs-replacement",
      "command_block": "RGX-08-001-emacs-command",
      "program_block": "RGX-08-001-emacs-run",
      "note": "The pattern and replacement both use \\1, for different jobs."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Capture words without requiring equality",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-08-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-08-001-change-command",
    "program_block": "RGX-08-001-change-run",
    "note": "Capture two independent words instead of using a pattern backreference. Different words now match too.",
    "goal": "Capture two independent words instead of using a pattern backreference. Different words now match too."
  },
  "aliases": [
    "RGX-08-002",
    "RGX-08-003",
    "RGX-08-004",
    "RGX-08-005",
    "RGX-08-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/duplicates.txt <<'RGX_INPUT'
red red
blue blue
red blue
red red red
red  red
RGX_INPUT
grep -o '^\([a-z]\{1,\}\) \1$' data/duplicates.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/duplicates.txt <<'RGX_INPUT'
red red
blue blue
red blue
red red red
red  red
RGX_INPUT
grep -Eo '^([a-z]+) \1$' data/duplicates.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/duplicates.txt <<'RGX_INPUT'
red red
blue blue
red blue
red red red
red  red
RGX_INPUT
perl -pe '
s{^([a-z]+) \1$}{$1}g;
' data/duplicates.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/duplicates.txt <<'RGX_INPUT'
red red
blue blue
red blue
red red red
red  red
RGX_INPUT
sed 's~^\([a-z]\{1,\}\) \1$~\1~g' data/duplicates.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/duplicates.txt <<'RGX_INPUT'
red red
blue blue
red blue
red red red
red  red
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([a-z]+\) \1$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/duplicates.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/duplicates.txt <<'RGX_INPUT'
red red
blue blue
red blue
red red red
red  red
RGX_INPUT
perl -ne '
while (m{^([a-z]+) ([a-z]+)$}g) {
  printf "%s|%s\n", $1, $2;
}
' data/duplicates.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-09-001

Handle an unmatched optional capture

The title group is optional. An absent title contributes an empty string during replacement. BRE uses separate Dr/Prof patterns because it has no alternation; the other views express the alternatives in one pattern. A malformed title is left unchanged by replacement.

Shared input · data/optional.txt
Ada:Dr
Bob
Clara:Prof
Dan:
Eve:Sir
All variations for RGX-09-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([A-Za-z]\{1,\}\)\(:\(Dr\)\)\{0,1\}$
^\([A-Za-z]\{1,\}\)\(:\(Prof\)\)\{0,1\}$
Search onlyUse separate optional Dr/Prof patterns instead of alternation.
grep -E / ERE
^([A-Za-z]+)(:(Dr|Prof))?$
Search onlyERE uses ? on the title group and | for the choice.
Perl
^([A-Za-z]+)(:(Dr|Prof))?$
$1 [$3]
An absent group 3 contributes empty text to $1 [$3].
sed -E / ERE
^([A-Za-z]+)(:(Dr|Prof))?$
\1 [\3]
-E enables the alternatives; \3 is empty when absent.
Emacs
^\([A-Za-z]+\)\(:\(Dr\|Prof\)\)?$
\1 [\3]
The optional title is group 3; insert it using \3.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

Ada:Dr
Bob
Clara:Prof

After replacement · Perl, sed and Emacs

Ada [Dr]
Bob []
Clara [Prof]
Dan:
Eve:Sir
Change one assumption · Make the optional title mandatory

Make the title mandatory. The record without a title no longer matches.

Changed regexp

^([A-Za-z]+)(:(Dr|Prof))$

Changed result

Ada|Dr
Clara|Prof

Change one assumption: Make the optional title mandatory · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o -e '^\([A-Za-z]\{1,\}\)\(:\(Dr\)\)\{0,1\}$' -e '^\([A-Za-z]\{1,\}\)\(:\(Prof\)\)\{0,1\}$' data/optional.txt

grep -E / ERE

grep -Eo '^([A-Za-z]+)(:(Dr|Prof))?$' data/optional.txt

Perl

perl -pe '
s{^([A-Za-z]+)(:(Dr|Prof))?$}{$1 [$3]}g;
' data/optional.txt

sed -E / ERE

sed -E 's~^([A-Za-z]+)(:(Dr|Prof))?$~\1 [\3]~g' data/optional.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-09-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-09-001-bre-command",
      "program_block": "RGX-09-001-bre-run",
      "note": "Use separate optional Dr/Prof patterns instead of alternation."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-09-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-09-001-ere-command",
      "program_block": "RGX-09-001-ere-run",
      "note": "ERE uses ? on the title group and | for the choice."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-09-001-perl-pattern",
      "replacement_block": "RGX-09-001-perl-replacement",
      "command_block": "RGX-09-001-perl-command",
      "program_block": "RGX-09-001-perl-run",
      "note": "An absent group 3 contributes empty text to $1 [$3]."
    },
    {
      "key": "sed",
      "label": "sed -E / ERE",
      "tool": "sed",
      "engine": "sed ERE (-E)",
      "pattern_block": "RGX-09-001-sed-pattern",
      "replacement_block": "RGX-09-001-sed-replacement",
      "command_block": "RGX-09-001-sed-command",
      "program_block": "RGX-09-001-sed-run",
      "note": "-E enables the alternatives; \\3 is empty when absent."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-09-001-emacs-pattern",
      "replacement_block": "RGX-09-001-emacs-replacement",
      "command_block": "RGX-09-001-emacs-command",
      "program_block": "RGX-09-001-emacs-run",
      "note": "The optional title is group 3; insert it using \\3."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Make the optional title mandatory",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-09-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-09-001-change-command",
    "program_block": "RGX-09-001-change-run",
    "note": "Make the title mandatory. The record without a title no longer matches.",
    "goal": "Make the title mandatory. The record without a title no longer matches."
  },
  "aliases": [
    "RGX-09-002",
    "RGX-09-003",
    "RGX-09-004",
    "RGX-09-005",
    "RGX-09-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/optional.txt <<'RGX_INPUT'
Ada:Dr
Bob
Clara:Prof
Dan:
Eve:Sir
RGX_INPUT
grep -o -e '^\([A-Za-z]\{1,\}\)\(:\(Dr\)\)\{0,1\}$' -e '^\([A-Za-z]\{1,\}\)\(:\(Prof\)\)\{0,1\}$' data/optional.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/optional.txt <<'RGX_INPUT'
Ada:Dr
Bob
Clara:Prof
Dan:
Eve:Sir
RGX_INPUT
grep -Eo '^([A-Za-z]+)(:(Dr|Prof))?$' data/optional.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/optional.txt <<'RGX_INPUT'
Ada:Dr
Bob
Clara:Prof
Dan:
Eve:Sir
RGX_INPUT
perl -pe '
s{^([A-Za-z]+)(:(Dr|Prof))?$}{$1 [$3]}g;
' data/optional.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/optional.txt <<'RGX_INPUT'
Ada:Dr
Bob
Clara:Prof
Dan:
Eve:Sir
RGX_INPUT
sed -E 's~^([A-Za-z]+)(:(Dr|Prof))?$~\1 [\3]~g' data/optional.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/optional.txt <<'RGX_INPUT'
Ada:Dr
Bob
Clara:Prof
Dan:
Eve:Sir
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([A-Za-z]+\)\(:\(Dr\|Prof\)\)?$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\1 [\3]
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/optional.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/optional.txt <<'RGX_INPUT'
Ada:Dr
Bob
Clara:Prof
Dan:
Eve:Sir
RGX_INPUT
perl -ne '
while (m{^([A-Za-z]+)(:(Dr|Prof))$}g) {
  printf "%s|%s\n", (defined $1 ? $1 : "<unset>"), (defined $3 ? $3 : "<unset>");
}
' data/optional.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-10-001

Name captures in Perl and map Emacs numbers

Perl gives the prefix and number names. The equivalent BRE, ERE, sed and Emacs patterns use numbered groups. Named replacement references survive an extra outer capture; numeric positions can change.

Shared input · data/named.txt
AB-17
XY-003
bad-17
AB-x
All variations for RGX-10-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([A-Z]\{1,\}\)-\([0-9]\{1,\}\)$
Search onlyNumbered groups: prefix is 1 and number is 2.
grep -E / ERE
^([A-Z]+)-([0-9]+)$
Search onlyOrdinary numbered groups; no named-group syntax.
Perl
^(?<prefix>[A-Z]+)-(?<number>[0-9]+)$
$+{number}/$+{prefix}
Use prefix/number names and $+{number}/$+{prefix}.
sed / BRE
^\([A-Z]\{1,\}\)-\([0-9]\{1,\}\)$
\2/\1
Use numbered BRE groups and \2/\1.
Emacs
^\([A-Z]+\)-\([0-9]+\)$
\2/\1
Use numbered Emacs groups and \2/\1.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

AB-17
XY-003

After replacement · Perl, sed and Emacs

17/AB
003/XY
bad-17
AB-x
Change one assumption · Add an outer named capture

Add an outer named group. Named replacement references still identify prefix and number even though numeric positions change.

Changed regexp

^(?<whole>(?<prefix>[A-Z]+)-(?<number>[0-9]+))$

Replacement

$+{number}/$+{prefix}

Changed result

17/AB
003/XY
bad-17
AB-x

Change one assumption: Add an outer named capture · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([A-Z]\{1,\}\)-\([0-9]\{1,\}\)$' data/named.txt

grep -E / ERE

grep -Eo '^([A-Z]+)-([0-9]+)$' data/named.txt

Perl

perl -pe '
s{^(?<prefix>[A-Z]+)-(?<number>[0-9]+)$}{$+{number}/$+{prefix}}g;
' data/named.txt

sed / BRE

sed 's~^\([A-Z]\{1,\}\)-\([0-9]\{1,\}\)$~\2/\1~g' data/named.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-10-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-10-001-bre-command",
      "program_block": "RGX-10-001-bre-run",
      "note": "Numbered groups: prefix is 1 and number is 2."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-10-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-10-001-ere-command",
      "program_block": "RGX-10-001-ere-run",
      "note": "Ordinary numbered groups; no named-group syntax."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-10-001-perl-pattern",
      "replacement_block": "RGX-10-001-perl-replacement",
      "command_block": "RGX-10-001-perl-command",
      "program_block": "RGX-10-001-perl-run",
      "note": "Use prefix/number names and $+{number}/$+{prefix}."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-10-001-sed-pattern",
      "replacement_block": "RGX-10-001-sed-replacement",
      "command_block": "RGX-10-001-sed-command",
      "program_block": "RGX-10-001-sed-run",
      "note": "Use numbered BRE groups and \\2/\\1."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-10-001-emacs-pattern",
      "replacement_block": "RGX-10-001-emacs-replacement",
      "command_block": "RGX-10-001-emacs-command",
      "program_block": "RGX-10-001-emacs-run",
      "note": "Use numbered Emacs groups and \\2/\\1."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Add an outer named capture",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-10-001-change-pattern",
    "replacement_block": "RGX-10-001-change-replacement",
    "command_block": "RGX-10-001-change-command",
    "program_block": "RGX-10-001-change-run",
    "note": "Add an outer named group. Named replacement references still identify prefix and number even though numeric positions change.",
    "goal": "Add an outer named group. Named replacement references still identify prefix and number even though numeric positions change."
  },
  "aliases": [
    "RGX-10-002",
    "RGX-10-003",
    "RGX-10-004",
    "RGX-10-005",
    "RGX-10-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/named.txt <<'RGX_INPUT'
AB-17
XY-003
bad-17
AB-x
RGX_INPUT
grep -o '^\([A-Z]\{1,\}\)-\([0-9]\{1,\}\)$' data/named.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/named.txt <<'RGX_INPUT'
AB-17
XY-003
bad-17
AB-x
RGX_INPUT
grep -Eo '^([A-Z]+)-([0-9]+)$' data/named.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/named.txt <<'RGX_INPUT'
AB-17
XY-003
bad-17
AB-x
RGX_INPUT
perl -pe '
s{^(?<prefix>[A-Z]+)-(?<number>[0-9]+)$}{$+{number}/$+{prefix}}g;
' data/named.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/named.txt <<'RGX_INPUT'
AB-17
XY-003
bad-17
AB-x
RGX_INPUT
sed 's~^\([A-Z]\{1,\}\)-\([0-9]\{1,\}\)$~\2/\1~g' data/named.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/named.txt <<'RGX_INPUT'
AB-17
XY-003
bad-17
AB-x
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([A-Z]+\)-\([0-9]+\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\2/\1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/named.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/named.txt <<'RGX_INPUT'
AB-17
XY-003
bad-17
AB-x
RGX_INPUT
perl -pe '
s{^(?<whole>(?<prefix>[A-Z]+)-(?<number>[0-9]+))$}{$+{number}/$+{prefix}}g;
' data/named.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-11-001

Group structure without capturing it

A group can organize a choice without capturing it in Perl and Emacs. ERE has no noncapturing group: the color takes group 1 and the number moves to group 2. sed -E therefore needs backslash-2 in replacement. The BRE view splits the choice into separate patterns.

Shared input · data/noncapturing.txt
red:12
blue:7
green:9
red:x
blue:007
All variations for RGX-11-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^red:\([0-9]\{1,\}\)$
^blue:\([0-9]\{1,\}\)$
Search onlyNo noncapturing group: separate patterns each capture digits as group 1.
grep -E / ERE
^(red|blue):([0-9]+)$
Search onlyThe color captures group 1, so digits become group 2.
Perl
^(?:red|blue):([0-9]+)$
n=$1
(?:...) does not capture; digits stay group 1.
sed -E / ERE
^(red|blue):([0-9]+)$
n=\2
-E captures color as 1 and digits as 2; use n=\2.
Emacs
^\(?:red\|blue\):\([0-9]+\)$
n=\1
\(?:...\) does not capture; use n=\1.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

red:12
blue:7
blue:007

After replacement · Perl, sed and Emacs

n=12
n=7
green:9
red:x
n=007
Change one assumption · Let a structural group shift the numbers

Make the structural color group capturing but keep the old replacement number. The output contains the color instead of the number.

Changed regexp

^(red|blue):([0-9]+)$

Replacement

n=$1

Changed result

n=red
n=blue
green:9
red:x
n=blue

Change one assumption: Let a structural group shift the numbers · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o -e '^red:\([0-9]\{1,\}\)$' -e '^blue:\([0-9]\{1,\}\)$' data/noncapturing.txt

grep -E / ERE

grep -Eo '^(red|blue):([0-9]+)$' data/noncapturing.txt

Perl

perl -pe '
s{^(?:red|blue):([0-9]+)$}{n=$1}g;
' data/noncapturing.txt

sed -E / ERE

sed -E 's~^(red|blue):([0-9]+)$~n=\2~g' data/noncapturing.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-11-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-11-001-bre-command",
      "program_block": "RGX-11-001-bre-run",
      "note": "No noncapturing group: separate patterns each capture digits as group 1."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-11-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-11-001-ere-command",
      "program_block": "RGX-11-001-ere-run",
      "note": "The color captures group 1, so digits become group 2."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-11-001-perl-pattern",
      "replacement_block": "RGX-11-001-perl-replacement",
      "command_block": "RGX-11-001-perl-command",
      "program_block": "RGX-11-001-perl-run",
      "note": "(?:...) does not capture; digits stay group 1."
    },
    {
      "key": "sed",
      "label": "sed -E / ERE",
      "tool": "sed",
      "engine": "sed ERE (-E)",
      "pattern_block": "RGX-11-001-sed-pattern",
      "replacement_block": "RGX-11-001-sed-replacement",
      "command_block": "RGX-11-001-sed-command",
      "program_block": "RGX-11-001-sed-run",
      "note": "-E captures color as 1 and digits as 2; use n=\\2."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-11-001-emacs-pattern",
      "replacement_block": "RGX-11-001-emacs-replacement",
      "command_block": "RGX-11-001-emacs-command",
      "program_block": "RGX-11-001-emacs-run",
      "note": "\\(?:...\\) does not capture; use n=\\1."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Let a structural group shift the numbers",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-11-001-change-pattern",
    "replacement_block": "RGX-11-001-change-replacement",
    "command_block": "RGX-11-001-change-command",
    "program_block": "RGX-11-001-change-run",
    "note": "Make the structural color group capturing but keep the old replacement number. The output contains the color instead of the number.",
    "goal": "Make the structural color group capturing but keep the old replacement number. The output contains the color instead of the number."
  },
  "aliases": [
    "RGX-11-002",
    "RGX-11-003",
    "RGX-11-004",
    "RGX-11-005",
    "RGX-11-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/noncapturing.txt <<'RGX_INPUT'
red:12
blue:7
green:9
red:x
blue:007
RGX_INPUT
grep -o -e '^red:\([0-9]\{1,\}\)$' -e '^blue:\([0-9]\{1,\}\)$' data/noncapturing.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/noncapturing.txt <<'RGX_INPUT'
red:12
blue:7
green:9
red:x
blue:007
RGX_INPUT
grep -Eo '^(red|blue):([0-9]+)$' data/noncapturing.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/noncapturing.txt <<'RGX_INPUT'
red:12
blue:7
green:9
red:x
blue:007
RGX_INPUT
perl -pe '
s{^(?:red|blue):([0-9]+)$}{n=$1}g;
' data/noncapturing.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/noncapturing.txt <<'RGX_INPUT'
red:12
blue:7
green:9
red:x
blue:007
RGX_INPUT
sed -E 's~^(red|blue):([0-9]+)$~n=\2~g' data/noncapturing.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/noncapturing.txt <<'RGX_INPUT'
red:12
blue:7
green:9
red:x
blue:007
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\(?:red\|blue\):\([0-9]+\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
n=\1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/noncapturing.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/noncapturing.txt <<'RGX_INPUT'
red:12
blue:7
green:9
red:x
blue:007
RGX_INPUT
perl -pe '
s{^(red|blue):([0-9]+)$}{n=$1}g;
' data/noncapturing.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-12-001

Distinguish the whole match from group 1

The whole match is order=7 while capture 1 is just 7. Perl inserts the whole match with $&, sed with &, and Emacs with backslash-ampersand. Literal backslashes in replacement text need their own escaping.

Shared input · data/whole.txt
order=7
order=42
other=3
All variations for RGX-12-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
order=\([0-9]\{1,\}\)
Search onlygrep selects the complete order=number match.
grep -E / ERE
order=([0-9]+)
Search onlyThe selected capture still cannot be emitted separately by -o.
Perl
order=([0-9]+)
[$&]
Insert the whole match using $&.
sed / BRE
order=\([0-9]\{1,\}\)
[&]
In sed replacement, unescaped & means the whole match.
Emacs
order=\([0-9]+\)
[\&]
In Emacs replacement, \& means the whole match.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

order=7
order=42

After replacement · Perl, sed and Emacs

[order=7]
[order=42]
other=3
Change one assumption · Emacs: insert literal \1

Insert the literal characters backslash and 1. In the Emacs Replacement field, type \\1 rather than \1.

Changed regexp

order=\([0-9]+\)

Replacement

\\1

Changed result

\1
\1
other=3

Change one assumption: Emacs: insert literal \1 · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o 'order=\([0-9]\{1,\}\)' data/whole.txt

grep -E / ERE

grep -Eo 'order=([0-9]+)' data/whole.txt

Perl

perl -pe '
s{order=([0-9]+)}{[$&]}g;
' data/whole.txt

sed / BRE

sed 's~order=\([0-9]\{1,\}\)~[&]~g' data/whole.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-12-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-12-001-bre-command",
      "program_block": "RGX-12-001-bre-run",
      "note": "grep selects the complete order=number match."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-12-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-12-001-ere-command",
      "program_block": "RGX-12-001-ere-run",
      "note": "The selected capture still cannot be emitted separately by -o."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-12-001-perl-pattern",
      "replacement_block": "RGX-12-001-perl-replacement",
      "command_block": "RGX-12-001-perl-command",
      "program_block": "RGX-12-001-perl-run",
      "note": "Insert the whole match using $&."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-12-001-sed-pattern",
      "replacement_block": "RGX-12-001-sed-replacement",
      "command_block": "RGX-12-001-sed-command",
      "program_block": "RGX-12-001-sed-run",
      "note": "In sed replacement, unescaped & means the whole match."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-12-001-emacs-pattern",
      "replacement_block": "RGX-12-001-emacs-replacement",
      "command_block": "RGX-12-001-emacs-command",
      "program_block": "RGX-12-001-emacs-run",
      "note": "In Emacs replacement, \\& means the whole match."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Emacs: insert literal \\1",
    "tool": "emacs",
    "engine": "Emacs regex",
    "pattern_block": "RGX-12-001-change-pattern",
    "replacement_block": "RGX-12-001-change-replacement",
    "command_block": "RGX-12-001-change-command",
    "program_block": "RGX-12-001-change-run",
    "note": "Insert the literal characters backslash and 1. In the Emacs Replacement field, type \\\\1 rather than \\1.",
    "goal": "Insert the literal characters backslash and 1. In the Emacs Replacement field, type \\\\1 rather than \\1."
  },
  "aliases": [
    "RGX-12-002",
    "RGX-12-003",
    "RGX-12-004",
    "RGX-12-005",
    "RGX-12-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/whole.txt <<'RGX_INPUT'
order=7
order=42
other=3
RGX_INPUT
grep -o 'order=\([0-9]\{1,\}\)' data/whole.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/whole.txt <<'RGX_INPUT'
order=7
order=42
other=3
RGX_INPUT
grep -Eo 'order=([0-9]+)' data/whole.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/whole.txt <<'RGX_INPUT'
order=7
order=42
other=3
RGX_INPUT
perl -pe '
s{order=([0-9]+)}{[$&]}g;
' data/whole.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/whole.txt <<'RGX_INPUT'
order=7
order=42
other=3
RGX_INPUT
sed 's~order=\([0-9]\{1,\}\)~[&]~g' data/whole.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/whole.txt <<'RGX_INPUT'
order=7
order=42
other=3
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
order=\([0-9]+\)
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
[\&]
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/whole.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/whole.txt <<'RGX_INPUT'
order=7
order=42
other=3
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
order=\([0-9]+\)
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\\1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/whole.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-13-001

Limit greedy and minimal matches

Greedy .* reaches the final closing tag on a line. Perl and Emacs can use minimal .*?; traditional BRE/ERE instead use a negated character class for this flat input. The changed Emacs example merges two adjacent tags into one replacement. These patterns are not an HTML parser.

Shared input · data/greedy.txt
<b>one</b><b>two</b>
<b>three</b>
plain
All variations for RGX-13-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
<b>[^<]*</b>
Search onlyUse a negated class for this flat input; BRE has no minimal quantifier.
grep -E / ERE
<b>[^<]*</b>
Search onlyDo not treat *? as a minimal ERE quantifier.
Perl
<b>.*?</b>
[item]
Perl supports minimal .*? directly.
sed / BRE
<b>[^<]*</b>
[item]
Use the restricted negated-class equivalent in BRE.
Emacs
<b>.*?</b>
[item]
Emacs supports minimal .*? directly.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

<b>one</b>
<b>two</b>
<b>three</b>

After replacement · Perl, sed and Emacs

[item][item]
[item]
plain
Change one assumption · Emacs: compare greedy matching

Use greedy .* in Emacs instead of minimal .*?. The two adjacent tags on the first line become one match and one replacement.

Changed regexp

<b>.*</b>

Replacement

[item]

Changed result

[item]
[item]
plain

Change one assumption: Emacs: compare greedy matching · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '<b>[^<]*</b>' data/greedy.txt

grep -E / ERE

grep -Eo '<b>[^<]*</b>' data/greedy.txt

Perl

perl -pe '
s{<b>.*?</b>}{[item]}g;
' data/greedy.txt

sed / BRE

sed 's~<b>[^<]*</b>~[item]~g' data/greedy.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-13-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-13-001-bre-command",
      "program_block": "RGX-13-001-bre-run",
      "note": "Use a negated class for this flat input; BRE has no minimal quantifier."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-13-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-13-001-ere-command",
      "program_block": "RGX-13-001-ere-run",
      "note": "Do not treat *? as a minimal ERE quantifier."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-13-001-perl-pattern",
      "replacement_block": "RGX-13-001-perl-replacement",
      "command_block": "RGX-13-001-perl-command",
      "program_block": "RGX-13-001-perl-run",
      "note": "Perl supports minimal .*? directly."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-13-001-sed-pattern",
      "replacement_block": "RGX-13-001-sed-replacement",
      "command_block": "RGX-13-001-sed-command",
      "program_block": "RGX-13-001-sed-run",
      "note": "Use the restricted negated-class equivalent in BRE."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-13-001-emacs-pattern",
      "replacement_block": "RGX-13-001-emacs-replacement",
      "command_block": "RGX-13-001-emacs-command",
      "program_block": "RGX-13-001-emacs-run",
      "note": "Emacs supports minimal .*? directly."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Emacs: compare greedy matching",
    "tool": "emacs",
    "engine": "Emacs regex",
    "pattern_block": "RGX-13-001-change-pattern",
    "replacement_block": "RGX-13-001-change-replacement",
    "command_block": "RGX-13-001-change-command",
    "program_block": "RGX-13-001-change-run",
    "note": "Use greedy .* in Emacs instead of minimal .*?. The two adjacent tags on the first line become one match and one replacement.",
    "goal": "Use greedy .* in Emacs instead of minimal .*?. The two adjacent tags on the first line become one match and one replacement."
  },
  "aliases": [
    "RGX-13-002",
    "RGX-13-003",
    "RGX-13-004",
    "RGX-13-005",
    "RGX-13-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/greedy.txt <<'RGX_INPUT'
<b>one</b><b>two</b>
<b>three</b>
plain
RGX_INPUT
grep -o '<b>[^<]*</b>' data/greedy.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/greedy.txt <<'RGX_INPUT'
<b>one</b><b>two</b>
<b>three</b>
plain
RGX_INPUT
grep -Eo '<b>[^<]*</b>' data/greedy.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/greedy.txt <<'RGX_INPUT'
<b>one</b><b>two</b>
<b>three</b>
plain
RGX_INPUT
perl -pe '
s{<b>.*?</b>}{[item]}g;
' data/greedy.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/greedy.txt <<'RGX_INPUT'
<b>one</b><b>two</b>
<b>three</b>
plain
RGX_INPUT
sed 's~<b>[^<]*</b>~[item]~g' data/greedy.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/greedy.txt <<'RGX_INPUT'
<b>one</b><b>two</b>
<b>three</b>
plain
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
<b>.*?</b>
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
[item]
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/greedy.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/greedy.txt <<'RGX_INPUT'
<b>one</b><b>two</b>
<b>three</b>
plain
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
<b>.*</b>
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
[item]
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/greedy.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-14-001

Replace every occurrence with capture groups

There can be several matches on one line. Perl and sed need g for every occurrence; Emacs replace-regexp replaces from point onward, so start at the beginning. grep -o prints each complete match rather than editing the input.

Shared input · data/global.txt
a1 a2 a3
b4 a5
none
All variations for RGX-14-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
\(a\)\([0-9]\{1,\}\)
Search onlyBRE groups use escaped parentheses and digit counts.
grep -E / ERE
(a)([0-9]+)
Search onlyERE uses ordinary parentheses and +.
Perl
(a)([0-9]+)
$2$1
The g flag replaces every match on each line.
sed / BRE
\(a\)\([0-9]\{1,\}\)
\2\1
The g flag also replaces every match on each line.
Emacs
\(a\)\([0-9]+\)
\2\1
No g flag: replace-regexp works from point onward.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

a1
a2
a3
a5

After replacement · Perl, sed and Emacs

1a 2a 3a
b4 5a
none
Change one assumption · Replace only the first match per line

Remove the Perl g flag. Only the first match on each input line is replaced.

Changed regexp

(a)([0-9]+)

Replacement

$2$1

Changed result

1a a2 a3
b4 5a
none

Change one assumption: Replace only the first match per line · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '\(a\)\([0-9]\{1,\}\)' data/global.txt

grep -E / ERE

grep -Eo '(a)([0-9]+)' data/global.txt

Perl

perl -pe '
s{(a)([0-9]+)}{$2$1}g;
' data/global.txt

sed / BRE

sed 's~\(a\)\([0-9]\{1,\}\)~\2\1~g' data/global.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-14-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-14-001-bre-command",
      "program_block": "RGX-14-001-bre-run",
      "note": "BRE groups use escaped parentheses and digit counts."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-14-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-14-001-ere-command",
      "program_block": "RGX-14-001-ere-run",
      "note": "ERE uses ordinary parentheses and +."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-14-001-perl-pattern",
      "replacement_block": "RGX-14-001-perl-replacement",
      "command_block": "RGX-14-001-perl-command",
      "program_block": "RGX-14-001-perl-run",
      "note": "The g flag replaces every match on each line."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-14-001-sed-pattern",
      "replacement_block": "RGX-14-001-sed-replacement",
      "command_block": "RGX-14-001-sed-command",
      "program_block": "RGX-14-001-sed-run",
      "note": "The g flag also replaces every match on each line."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-14-001-emacs-pattern",
      "replacement_block": "RGX-14-001-emacs-replacement",
      "command_block": "RGX-14-001-emacs-command",
      "program_block": "RGX-14-001-emacs-run",
      "note": "No g flag: replace-regexp works from point onward."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Replace only the first match per line",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-14-001-change-pattern",
    "replacement_block": "RGX-14-001-change-replacement",
    "command_block": "RGX-14-001-change-command",
    "program_block": "RGX-14-001-change-run",
    "note": "Remove the Perl g flag. Only the first match on each input line is replaced.",
    "goal": "Remove the Perl g flag. Only the first match on each input line is replaced."
  },
  "aliases": [
    "RGX-14-002",
    "RGX-14-003",
    "RGX-14-004",
    "RGX-14-005",
    "RGX-14-006"
  ]
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/global.txt <<'RGX_INPUT'
a1 a2 a3
b4 a5
none
RGX_INPUT
grep -o '\(a\)\([0-9]\{1,\}\)' data/global.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/global.txt <<'RGX_INPUT'
a1 a2 a3
b4 a5
none
RGX_INPUT
grep -Eo '(a)([0-9]+)' data/global.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/global.txt <<'RGX_INPUT'
a1 a2 a3
b4 a5
none
RGX_INPUT
perl -pe '
s{(a)([0-9]+)}{$2$1}g;
' data/global.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/global.txt <<'RGX_INPUT'
a1 a2 a3
b4 a5
none
RGX_INPUT
sed 's~\(a\)\([0-9]\{1,\}\)~\2\1~g' data/global.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/global.txt <<'RGX_INPUT'
a1 a2 a3
b4 a5
none
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
\(a\)\([0-9]+\)
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\2\1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/global.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/global.txt <<'RGX_INPUT'
a1 a2 a3
b4 a5
none
RGX_INPUT
perl -pe '
s{(a)([0-9]+)}{$2$1};
' data/global.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-15-001

Screen an ASCII email address

Use a deliberately bounded ASCII email policy: nonempty local segments made from letters, digits, _, +, % and -, separated by single dots; DNS-style domain labels; a final alphabetic label of at least two characters. It rejects consecutive local dots and domain-edge hyphens. Quoted or internationalized mailboxes and other valid local symbols are outside this policy. A match does not prove mailbox existence or delivery.

Shared input · data/email.txt
ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
All variations for RGX-15-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([A-Za-z0-9_+%-]\{1,\}\([.][A-Za-z0-9_+%-]\{1,\}\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}[.]\)\{1,\}[A-Za-z]\{2,\}\)$
Search onlyBRE escapes the groups, + and ? repetitions, and count braces.
grep -E / ERE
^([A-Za-z0-9_+%-]+([.][A-Za-z0-9_+%-]+)*)@(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?[.])+[A-Za-z]{2,})$
Search onlyGroup 1 is the local part; group 3 is the full domain.
Perl
^([A-Za-z0-9_+%-]+([.][A-Za-z0-9_+%-]+)*)@(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?[.])+[A-Za-z]{2,})$
local=$1 domain=$3
Extract $1 and $3; leave unmatched records unchanged.
sed / BRE
^\([A-Za-z0-9_+%-]\{1,\}\([.][A-Za-z0-9_+%-]\{1,\}\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}[.]\)\{1,\}[A-Za-z]\{2,\}\)$
local=\1 domain=\3
Default sed uses BRE groups and \1 / \3 in replacement.
Emacs
^\([A-Za-z0-9_+%-]+\([.][A-Za-z0-9_+%-]+\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)?[.]\)+[A-Za-z]\{2,\}\)$
local=\1 domain=\3
Enter \1 and \3 in the Emacs replacement field.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Format matches · BRE and grep -E

ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org

Extracted fields · Perl, sed and Emacs

local=ada domain=example.org
local=ada.lovelace+notes domain=research.example.org
local=team domain=lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
Change one assumption · Try an over-permissive email shortcut

Replacing the local/domain rules by non-space runs admits consecutive dots, a leading domain hyphen and a domain without a final dot. These are outside the declared email policy.

Changed regexp

^([^[:space:]@]+)@([^[:space:]@]+)$

Replacement

local=$1 domain=$2

Changed result

local=ada domain=example.org
local=ada.lovelace+notes domain=research.example.org
local=team domain=lab-2.example.org
local=bad..dots domain=example.org
local=name domain=-example.org
local=name domain=example
missing-at.example.org
"quoted local"@example.org

Change one assumption: Try an over-permissive email shortcut · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([A-Za-z0-9_+%-]\{1,\}\([.][A-Za-z0-9_+%-]\{1,\}\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}[.]\)\{1,\}[A-Za-z]\{2,\}\)$' data/email.txt

grep -E / ERE

grep -Eo '^([A-Za-z0-9_+%-]+([.][A-Za-z0-9_+%-]+)*)@(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?[.])+[A-Za-z]{2,})$' data/email.txt

Perl

perl -pe '
s{^([A-Za-z0-9_+%-]+([.][A-Za-z0-9_+%-]+)*)@(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?[.])+[A-Za-z]{2,})$}{local=$1 domain=$3}g;
' data/email.txt

sed / BRE

sed 's~^\([A-Za-z0-9_+%-]\{1,\}\([.][A-Za-z0-9_+%-]\{1,\}\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}[.]\)\{1,\}[A-Za-z]\{2,\}\)$~local=\1 domain=\3~g' data/email.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-15-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-15-001-bre-command",
      "program_block": "RGX-15-001-bre-run",
      "note": "BRE escapes the groups, + and ? repetitions, and count braces."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-15-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-15-001-ere-command",
      "program_block": "RGX-15-001-ere-run",
      "note": "Group 1 is the local part; group 3 is the full domain."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-15-001-perl-pattern",
      "replacement_block": "RGX-15-001-perl-replacement",
      "command_block": "RGX-15-001-perl-command",
      "program_block": "RGX-15-001-perl-run",
      "note": "Extract $1 and $3; leave unmatched records unchanged."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-15-001-sed-pattern",
      "replacement_block": "RGX-15-001-sed-replacement",
      "command_block": "RGX-15-001-sed-command",
      "program_block": "RGX-15-001-sed-run",
      "note": "Default sed uses BRE groups and \\1 / \\3 in replacement."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-15-001-emacs-pattern",
      "replacement_block": "RGX-15-001-emacs-replacement",
      "command_block": "RGX-15-001-emacs-command",
      "program_block": "RGX-15-001-emacs-run",
      "note": "Enter \\1 and \\3 in the Emacs replacement field."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Try an over-permissive email shortcut",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-15-001-change-pattern",
    "replacement_block": "RGX-15-001-change-replacement",
    "command_block": "RGX-15-001-change-command",
    "program_block": "RGX-15-001-change-run",
    "note": "Replacing the local/domain rules by non-space runs admits consecutive dots, a leading domain hyphen and a domain without a final dot. These are outside the declared email policy.",
    "goal": "Replacing the local/domain rules by non-space runs admits consecutive dots, a leading domain hyphen and a domain without a final dot. These are outside the declared email policy.",
    "kind": "change"
  },
  "aliases": [],
  "validation": true,
  "replacement_label": "Extracted fields · Perl, sed and Emacs"
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/email.txt <<'RGX_INPUT'
ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
RGX_INPUT
grep -o '^\([A-Za-z0-9_+%-]\{1,\}\([.][A-Za-z0-9_+%-]\{1,\}\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}[.]\)\{1,\}[A-Za-z]\{2,\}\)$' data/email.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/email.txt <<'RGX_INPUT'
ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
RGX_INPUT
grep -Eo '^([A-Za-z0-9_+%-]+([.][A-Za-z0-9_+%-]+)*)@(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?[.])+[A-Za-z]{2,})$' data/email.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/email.txt <<'RGX_INPUT'
ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
RGX_INPUT
perl -pe '
s{^([A-Za-z0-9_+%-]+([.][A-Za-z0-9_+%-]+)*)@(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?[.])+[A-Za-z]{2,})$}{local=$1 domain=$3}g;
' data/email.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/email.txt <<'RGX_INPUT'
ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
RGX_INPUT
sed 's~^\([A-Za-z0-9_+%-]\{1,\}\([.][A-Za-z0-9_+%-]\{1,\}\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}[.]\)\{1,\}[A-Za-z]\{2,\}\)$~local=\1 domain=\3~g' data/email.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/email.txt <<'RGX_INPUT'
ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([A-Za-z0-9_+%-]+\([.][A-Za-z0-9_+%-]+\)*\)@\(\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)?[.]\)+[A-Za-z]\{2,\}\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
local=\1 domain=\3
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/email.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/email.txt <<'RGX_INPUT'
ada@example.org
ada.lovelace+notes@research.example.org
team@lab-2.example.org
bad..dots@example.org
name@-example.org
name@example
missing-at.example.org
"quoted local"@example.org
RGX_INPUT
perl -pe '
s{^([^[:space:]@]+)@([^[:space:]@]+)$}{local=$1 domain=$2}g;
' data/email.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-16-001

Screen HTTP(S) URLs and capture the host

Screen a lowercase http/https profile with a dotted ASCII host, an optional 1–5 digit port and a non-space path/query/fragment. Userinfo, IPv6 literals, local hostnames and internationalized hostnames are outside this profile. The five regex views check shape only: 99999 still fits the port spelling. The additional check enforces the port range; full URI parsing, percent-encoding and reachability are separate questions.

Shared input · data/urls.txt
https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
All variations for RGX-16-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\(https\{0,1\}\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\)\{1,\}\)\(:\([0-9]\{1,5\}\)\)\{0,1\}\([/?#][^[:space:]]*\)\{0,1\}$
Search onlyThe same structure needs escaped BRE groups and bounded repeats.
grep -E / ERE
^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$
Search onlyScheme is group 1, full host group 2 and port digits group 7.
Perl
^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$
scheme=$1 host=$2 port=$7
The optional $7 inserts empty text when no port was supplied.
sed / BRE
^\(https\{0,1\}\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\)\{1,\}\)\(:\([0-9]\{1,5\}\)\)\{0,1\}\([/?#][^[:space:]]*\)\{0,1\}$
scheme=\1 host=\2 port=\7
Default BRE keeps the same group numbers: \1, \2 and \7.
Emacs
^\(https?\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)?\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)?\)+\)\(:\([0-9]\{1,5\}\)\)?\([/?#][^[:space:]]*\)?$
scheme=\1 host=\2 port=\7
Emacs supports ? and +, but escapes braces and groups.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Format matches · BRE and grep -E

https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path

Extracted fields · Perl, sed and Emacs

scheme=https host=example.org port=
scheme=http host=data.example.org port=8080
scheme=https host=example.org port=
scheme=https host=example.org port=
scheme=https host=example.org port=99999
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
Additional check · Check the numeric port range

After the same format match, read group 7 as a decimal number and require 0–65535. shape+port-ok reports just these checks; it does not certify a complete or reachable URL.

Format regexp

^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$

Additional check result

shape+port-ok	https://example.org
shape+port-ok	http://data.example.org:8080/run?q=regex#part
shape+port-ok	https://example.org?q=one
shape+port-ok	https://example.org/a%20b
port-out-of-range	https://example.org:99999/path
outside-profile	ftp://example.org/file
outside-profile	https://bad..example.org/path
outside-profile	https://example.org/a b
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$}) {
  my $port = $7;
  my $status = defined($port) && $port > 65535 ? "port-out-of-range" : "shape+port-ok";
  printf "%s\t%s\n", $status, $input;
} else {
  printf "outside-profile\t%s\n", $input;
}
' data/urls.txt

Additional check: Check the numeric port range · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\(https\{0,1\}\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\)\{1,\}\)\(:\([0-9]\{1,5\}\)\)\{0,1\}\([/?#][^[:space:]]*\)\{0,1\}$' data/urls.txt

grep -E / ERE

grep -Eo '^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$' data/urls.txt

Perl

perl -pe '
s{^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$}{scheme=$1 host=$2 port=$7}g;
' data/urls.txt

sed / BRE

sed 's~^\(https\{0,1\}\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\)\{1,\}\)\(:\([0-9]\{1,5\}\)\)\{0,1\}\([/?#][^[:space:]]*\)\{0,1\}$~scheme=\1 host=\2 port=\7~g' data/urls.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-16-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-16-001-bre-command",
      "program_block": "RGX-16-001-bre-run",
      "note": "The same structure needs escaped BRE groups and bounded repeats."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-16-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-16-001-ere-command",
      "program_block": "RGX-16-001-ere-run",
      "note": "Scheme is group 1, full host group 2 and port digits group 7."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-16-001-perl-pattern",
      "replacement_block": "RGX-16-001-perl-replacement",
      "command_block": "RGX-16-001-perl-command",
      "program_block": "RGX-16-001-perl-run",
      "note": "The optional $7 inserts empty text when no port was supplied."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-16-001-sed-pattern",
      "replacement_block": "RGX-16-001-sed-replacement",
      "command_block": "RGX-16-001-sed-command",
      "program_block": "RGX-16-001-sed-run",
      "note": "Default BRE keeps the same group numbers: \\1, \\2 and \\7."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-16-001-emacs-pattern",
      "replacement_block": "RGX-16-001-emacs-replacement",
      "command_block": "RGX-16-001-emacs-command",
      "program_block": "RGX-16-001-emacs-run",
      "note": "Emacs supports ? and +, but escapes braces and groups."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Check the numeric port range",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-16-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-16-001-change-command",
    "program_block": "RGX-16-001-change-run",
    "note": "After the same format match, read group 7 as a decimal number and require 0–65535. shape+port-ok reports just these checks; it does not certify a complete or reachable URL.",
    "goal": "After the same format match, read group 7 as a decimal number and require 0–65535. shape+port-ok reports just these checks; it does not certify a complete or reachable URL.",
    "kind": "semantic-check",
    "pattern_label": "Format regexp",
    "result_label": "Additional check result"
  },
  "aliases": [],
  "validation": true,
  "replacement_label": "Extracted fields · Perl, sed and Emacs"
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/urls.txt <<'RGX_INPUT'
https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
RGX_INPUT
grep -o '^\(https\{0,1\}\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\)\{1,\}\)\(:\([0-9]\{1,5\}\)\)\{0,1\}\([/?#][^[:space:]]*\)\{0,1\}$' data/urls.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/urls.txt <<'RGX_INPUT'
https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
RGX_INPUT
grep -Eo '^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$' data/urls.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/urls.txt <<'RGX_INPUT'
https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
RGX_INPUT
perl -pe '
s{^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$}{scheme=$1 host=$2 port=$7}g;
' data/urls.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/urls.txt <<'RGX_INPUT'
https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
RGX_INPUT
sed 's~^\(https\{0,1\}\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)\{0,1\}\)\{1,\}\)\(:\([0-9]\{1,5\}\)\)\{0,1\}\([/?#][^[:space:]]*\)\{0,1\}$~scheme=\1 host=\2 port=\7~g' data/urls.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/urls.txt <<'RGX_INPUT'
https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\(https?\)://\([A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)?\([.][A-Za-z0-9]\([A-Za-z0-9-]*[A-Za-z0-9]\)?\)+\)\(:\([0-9]\{1,5\}\)\)?\([/?#][^[:space:]]*\)?$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
scheme=\1 host=\2 port=\7
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/urls.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/urls.txt <<'RGX_INPUT'
https://example.org
http://data.example.org:8080/run?q=regex#part
https://example.org?q=one
https://example.org/a%20b
https://example.org:99999/path
ftp://example.org/file
https://bad..example.org/path
https://example.org/a b
RGX_INPUT
perl -ne '
chomp;
my $input = $_;
if (m{^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$}) {
  my $port = $7;
  my $status = defined($port) && $port > 65535 ? "port-out-of-range" : "shape+port-ok";
  printf "%s\t%s\n", $status, $input;
} else {
  printf "outside-profile\t%s\n", $input;
}
' data/urls.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-17-001

Screen ORCID iDs and verify their check digit

Accept a bare, hyphenated ORCID-shaped identifier or its exact https://orcid.org/ form. Keep all 16 characters, including leading zeros; the final character can be an uppercase X. The replacement builds the canonical URI. This format screen also accepts an incorrect check digit. The additional ISO 7064 MOD 11-2 calculation distinguishes it; a correct checksum is still not proof that the registry issued the identifier.

Shared input · data/orcid.txt
0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
All variations for RGX-17-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\(https://orcid[.]org/\)\{0,1\}\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$
Search onlyGroups and {4}/{3} counts need BRE backslashes.
grep -E / ERE
^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$
Search onlyThe optional URI prefix is group 1; identifier chunks are 2–5.
Perl
^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$
https://orcid.org/$2-$3-$4-$5
Build the URI with $2-$3-$4-$5; preserve leading zeros.
sed / BRE
^\(https://orcid[.]org/\)\{0,1\}\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$
https://orcid.org/\2-\3-\4-\5
Build the same URI with \2-\3-\4-\5.
Emacs
^\(https://orcid[.]org/\)?\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$
https://orcid.org/\2-\3-\4-\5
Use an uppercase X; the tour binds case-fold-search to nil.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Format matches · BRE and grep -E

0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098

Canonical form · Perl, sed and Emacs

https://orcid.org/0000-0002-1825-0097
https://orcid.org/0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
https://orcid.org/0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
Additional check · Verify the ORCID checksum

Apply MOD 11-2 to the first 15 digits: repeatedly double the running total after adding a digit. The resulting check character is compared with the last character, with 10 represented by X. The supplied 0098 ending must fail; this offline check does not query the registry.

Format regexp

^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$

Additional check result

checksum-ok	0000-0002-1825-0097
checksum-ok	0000-0002-1694-233X
checksum-ok	0000-0002-1825-0097
bad-checksum(expected=7)	0000-0002-1825-0098
outside-format	0000-0002-1694-233x
outside-format	0000 0002 1825 0097
outside-format	0000-0002-1825-097
outside-format	https://example.org/0000-0002-1825-0097
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$}) {
  my ($a, $b, $c, $d) = ($2, $3, $4, $5);
  my $id = join "-", $a, $b, $c, $d;
  my $base = $a . $b . $c . substr($d, 0, 3);
  my $given = substr($d, 3, 1);
  my $total = 0;
  for my $digit (split //, $base) { $total = 2 * ($total + $digit); }
  my $number = (12 - $total % 11) % 11;
  my $wanted = $number == 10 ? "X" : "$number";
  my $status = $given eq $wanted ? "checksum-ok" : "bad-checksum(expected=$wanted)";
  printf "%s\t%s\n", $status, $id;
} else {
  printf "outside-format\t%s\n", $input;
}
' data/orcid.txt

Additional check: Verify the ORCID checksum · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\(https://orcid[.]org/\)\{0,1\}\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$' data/orcid.txt

grep -E / ERE

grep -Eo '^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$' data/orcid.txt

Perl

perl -pe '
s{^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$}{https://orcid.org/$2-$3-$4-$5}g;
' data/orcid.txt

sed / BRE

sed 's~^\(https://orcid[.]org/\)\{0,1\}\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$~https://orcid.org/\2-\3-\4-\5~g' data/orcid.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-17-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-17-001-bre-command",
      "program_block": "RGX-17-001-bre-run",
      "note": "Groups and {4}/{3} counts need BRE backslashes."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-17-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-17-001-ere-command",
      "program_block": "RGX-17-001-ere-run",
      "note": "The optional URI prefix is group 1; identifier chunks are 2–5."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-17-001-perl-pattern",
      "replacement_block": "RGX-17-001-perl-replacement",
      "command_block": "RGX-17-001-perl-command",
      "program_block": "RGX-17-001-perl-run",
      "note": "Build the URI with $2-$3-$4-$5; preserve leading zeros."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-17-001-sed-pattern",
      "replacement_block": "RGX-17-001-sed-replacement",
      "command_block": "RGX-17-001-sed-command",
      "program_block": "RGX-17-001-sed-run",
      "note": "Build the same URI with \\2-\\3-\\4-\\5."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-17-001-emacs-pattern",
      "replacement_block": "RGX-17-001-emacs-replacement",
      "command_block": "RGX-17-001-emacs-command",
      "program_block": "RGX-17-001-emacs-run",
      "note": "Use an uppercase X; the tour binds case-fold-search to nil."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Verify the ORCID checksum",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-17-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-17-001-change-command",
    "program_block": "RGX-17-001-change-run",
    "note": "Apply MOD 11-2 to the first 15 digits: repeatedly double the running total after adding a digit. The resulting check character is compared with the last character, with 10 represented by X. The supplied 0098 ending must fail; this offline check does not query the registry.",
    "goal": "Apply MOD 11-2 to the first 15 digits: repeatedly double the running total after adding a digit. The resulting check character is compared with the last character, with 10 represented by X. The supplied 0098 ending must fail; this offline check does not query the registry.",
    "kind": "semantic-check",
    "pattern_label": "Format regexp",
    "result_label": "Additional check result"
  },
  "aliases": [],
  "validation": true,
  "replacement_label": "Canonical form · Perl, sed and Emacs"
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/orcid.txt <<'RGX_INPUT'
0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
RGX_INPUT
grep -o '^\(https://orcid[.]org/\)\{0,1\}\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$' data/orcid.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/orcid.txt <<'RGX_INPUT'
0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
RGX_INPUT
grep -Eo '^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$' data/orcid.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/orcid.txt <<'RGX_INPUT'
0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
RGX_INPUT
perl -pe '
s{^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$}{https://orcid.org/$2-$3-$4-$5}g;
' data/orcid.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/orcid.txt <<'RGX_INPUT'
0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
RGX_INPUT
sed 's~^\(https://orcid[.]org/\)\{0,1\}\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$~https://orcid.org/\2-\3-\4-\5~g' data/orcid.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/orcid.txt <<'RGX_INPUT'
0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\(https://orcid[.]org/\)?\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{4\}\)-\([0-9]\{3\}[0-9X]\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
https://orcid.org/\2-\3-\4-\5
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/orcid.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/orcid.txt <<'RGX_INPUT'
0000-0002-1825-0097
0000-0002-1694-233X
https://orcid.org/0000-0002-1825-0097
0000-0002-1825-0098
0000-0002-1694-233x
0000 0002 1825 0097
0000-0002-1825-097
https://example.org/0000-0002-1825-0097
RGX_INPUT
perl -ne '
chomp;
my $input = $_;
if (m{^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$}) {
  my ($a, $b, $c, $d) = ($2, $3, $4, $5);
  my $id = join "-", $a, $b, $c, $d;
  my $base = $a . $b . $c . substr($d, 0, 3);
  my $given = substr($d, 3, 1);
  my $total = 0;
  for my $digit (split //, $base) { $total = 2 * ($total + $digit); }
  my $number = (12 - $total % 11) % 11;
  my $wanted = $number == 10 ? "X" : "$number";
  my $status = $given eq $wanted ? "checksum-ok" : "bad-checksum(expected=$wanted)";
  printf "%s\t%s\n", $status, $id;
} else {
  printf "outside-format\t%s\n", $input;
}
' data/orcid.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-18-001

Screen dotted IPv4 and check the octets

Use four captured runs of one to three ASCII digits, separated by literal periods. This is a shape screen, so 256 and a leading-zero octet still match. The additional check enforces decimal octets in 0–255 without leading zeros. address-syntax-ok says nothing about whether an address is public, assigned or reachable.

Shared input · data/ipv4.txt
192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1
1.2.3
192.0.2.1:80
All variations for RGX-18-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$
Search onlyBRE escapes the group delimiters and \{1,3\} bounds.
grep -E / ERE
^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$
Search only[.] is a literal separator; {1,3} only limits digit count.
Perl
^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$
octets=$1/$2/$3/$4
Groups $1–$4 hold the four octet strings.
sed / BRE
^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$
octets=\1/\2/\3/\4
Default sed uses \1–\4 in the replacement.
Emacs
^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$
octets=\1/\2/\3/\4
Emacs escapes the groups and count braces; use \1–\4.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Format matches · BRE and grep -E

192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1

Extracted fields · Perl, sed and Emacs

octets=192/0/2/1
octets=198/51/100/42
octets=0/0/0/0
octets=255/255/255/255
octets=256/1/2/3
octets=192/168/001/1
1.2.3
192.0.2.1:80
Additional check · Check decimal octets and leading zeros

Require each captured octet to be at most 255 and reject multi-digit octets beginning with zero. This checks the dotted-decimal address syntax used here, not routing or reserved-address policy.

Format regexp

^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$

Additional check result

address-syntax-ok	192.0.2.1
address-syntax-ok	198.51.100.42
address-syntax-ok	0.0.0.0
address-syntax-ok	255.255.255.255
octet-out-of-range	256.1.2.3
leading-zero	192.168.001.1
outside-format	1.2.3
outside-format	192.0.2.1:80
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$}) {
  my @octets = ($1, $2, $3, $4);
  my $status = "address-syntax-ok";
  if (grep { $_ > 255 } @octets) { $status = "octet-out-of-range"; }
  elsif (grep { length($_) > 1 && substr($_, 0, 1) eq "0" } @octets) { $status = "leading-zero"; }
  printf "%s\t%s\n", $status, $input;
} else {
  printf "outside-format\t%s\n", $input;
}
' data/ipv4.txt

Additional check: Check decimal octets and leading zeros · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$' data/ipv4.txt

grep -E / ERE

grep -Eo '^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$' data/ipv4.txt

Perl

perl -pe '
s{^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$}{octets=$1/$2/$3/$4}g;
' data/ipv4.txt

sed / BRE

sed 's~^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$~octets=\1/\2/\3/\4~g' data/ipv4.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-18-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-18-001-bre-command",
      "program_block": "RGX-18-001-bre-run",
      "note": "BRE escapes the group delimiters and \\{1,3\\} bounds."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-18-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-18-001-ere-command",
      "program_block": "RGX-18-001-ere-run",
      "note": "[.] is a literal separator; {1,3} only limits digit count."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-18-001-perl-pattern",
      "replacement_block": "RGX-18-001-perl-replacement",
      "command_block": "RGX-18-001-perl-command",
      "program_block": "RGX-18-001-perl-run",
      "note": "Groups $1–$4 hold the four octet strings."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-18-001-sed-pattern",
      "replacement_block": "RGX-18-001-sed-replacement",
      "command_block": "RGX-18-001-sed-command",
      "program_block": "RGX-18-001-sed-run",
      "note": "Default sed uses \\1–\\4 in the replacement."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-18-001-emacs-pattern",
      "replacement_block": "RGX-18-001-emacs-replacement",
      "command_block": "RGX-18-001-emacs-command",
      "program_block": "RGX-18-001-emacs-run",
      "note": "Emacs escapes the groups and count braces; use \\1–\\4."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Check decimal octets and leading zeros",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-18-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-18-001-change-command",
    "program_block": "RGX-18-001-change-run",
    "note": "Require each captured octet to be at most 255 and reject multi-digit octets beginning with zero. This checks the dotted-decimal address syntax used here, not routing or reserved-address policy.",
    "goal": "Require each captured octet to be at most 255 and reject multi-digit octets beginning with zero. This checks the dotted-decimal address syntax used here, not routing or reserved-address policy.",
    "kind": "semantic-check",
    "pattern_label": "Format regexp",
    "result_label": "Additional check result"
  },
  "aliases": [],
  "validation": true,
  "replacement_label": "Extracted fields · Perl, sed and Emacs"
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/ipv4.txt <<'RGX_INPUT'
192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1
1.2.3
192.0.2.1:80
RGX_INPUT
grep -o '^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$' data/ipv4.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/ipv4.txt <<'RGX_INPUT'
192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1
1.2.3
192.0.2.1:80
RGX_INPUT
grep -Eo '^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$' data/ipv4.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/ipv4.txt <<'RGX_INPUT'
192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1
1.2.3
192.0.2.1:80
RGX_INPUT
perl -pe '
s{^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$}{octets=$1/$2/$3/$4}g;
' data/ipv4.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/ipv4.txt <<'RGX_INPUT'
192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1
1.2.3
192.0.2.1:80
RGX_INPUT
sed 's~^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$~octets=\1/\2/\3/\4~g' data/ipv4.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/ipv4.txt <<'RGX_INPUT'
192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1
1.2.3
192.0.2.1:80
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)[.]\([0-9]\{1,3\}\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
octets=\1/\2/\3/\4
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/ipv4.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/ipv4.txt <<'RGX_INPUT'
192.0.2.1
198.51.100.42
0.0.0.0
255.255.255.255
256.1.2.3
192.168.001.1
1.2.3
192.0.2.1:80
RGX_INPUT
perl -ne '
chomp;
my $input = $_;
if (m{^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$}) {
  my @octets = ($1, $2, $3, $4);
  my $status = "address-syntax-ok";
  if (grep { $_ > 255 } @octets) { $status = "octet-out-of-range"; }
  elsif (grep { length($_) > 1 && substr($_, 0, 1) eq "0" } @octets) { $status = "leading-zero"; }
  printf "%s\t%s\n", $status, $input;
} else {
  printf "outside-format\t%s\n", $input;
}
' data/ipv4.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-19-001

Extract fields from structured logs

Match one complete record: a UTC-shaped timestamp, a status from 100 to 599, a token request ID and a non-space path. Groups 1, 2 and 3 capture time, status and ID; path text is deliberately not captured. The token - means no request ID. This reads a stated log format, not arbitrary server logs or calendar-valid timestamps.

Shared input · data/logs.txt
2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
All variations for RGX-19-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]\{1,\}\) path=[^[:space:]]\{1,\}$
Search onlyEscape groups, + and repeat counts in BRE. Anchors cover the full log record.
grep -E / ERE
^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([1-5][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$
Search onlyGroups 1/2/3 hold timestamp/status/request ID; the 20 status fails.
Perl
^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([1-5][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$
time=$1 code=$2 id=$3
Keep $1, $2 and $3 while discarding the path field.
sed / BRE
^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]\{1,\}\) path=[^[:space:]]\{1,\}$
time=\1 code=\2 id=\3
Use the same capture numbers with \1, \2 and \3.
Emacs
^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]+\) path=[^[:space:]]+$
time=\1 code=\2 id=\3
Enter literal regexp backslashes; use the shared case-sensitive setup.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health

After replacement · Perl, sed and Emacs

time=2026-11-01T09:15:00Z code=200 id=req-17
time=2026-11-01T09:15:01Z code=404 id=req-18
time=2026-11-01T09:15:02Z code=503 id=req-19
time=2026-11-01T09:15:03Z code=200 id=-
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
Change one assumption · Select only client and server errors

Narrow status group 2 from [1-5][0-9]{2} to [45][0-9]{2}. The Perl check prints only captured 4xx/5xx records; successful 2xx requests disappear from this diagnostic view.

Changed regexp

^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([45][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$

Changed result

time=2026-11-01T09:15:01Z code=404 id=req-18
time=2026-11-01T09:15:02Z code=503 id=req-19
Changed command · Perl
perl -ne '
if (m{^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([45][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$}) {
  printf "time=%s code=%s id=%s\n", $1, $2, $3;
}
' data/logs.txt

Change one assumption: Select only client and server errors · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]\{1,\}\) path=[^[:space:]]\{1,\}$' data/logs.txt

grep -E / ERE

grep -Eo '^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([1-5][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$' data/logs.txt

Perl

perl -pe '
s{^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([1-5][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$}{time=$1 code=$2 id=$3}g;
' data/logs.txt

sed / BRE

sed 's~^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]\{1,\}\) path=[^[:space:]]\{1,\}$~time=\1 code=\2 id=\3~g' data/logs.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-19-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-19-001-bre-command",
      "program_block": "RGX-19-001-bre-run",
      "note": "Escape groups, + and repeat counts in BRE. Anchors cover the full log record."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-19-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-19-001-ere-command",
      "program_block": "RGX-19-001-ere-run",
      "note": "Groups 1/2/3 hold timestamp/status/request ID; the 20 status fails."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-19-001-perl-pattern",
      "replacement_block": "RGX-19-001-perl-replacement",
      "command_block": "RGX-19-001-perl-command",
      "program_block": "RGX-19-001-perl-run",
      "note": "Keep $1, $2 and $3 while discarding the path field."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-19-001-sed-pattern",
      "replacement_block": "RGX-19-001-sed-replacement",
      "command_block": "RGX-19-001-sed-command",
      "program_block": "RGX-19-001-sed-run",
      "note": "Use the same capture numbers with \\1, \\2 and \\3."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-19-001-emacs-pattern",
      "replacement_block": "RGX-19-001-emacs-replacement",
      "command_block": "RGX-19-001-emacs-command",
      "program_block": "RGX-19-001-emacs-run",
      "note": "Enter literal regexp backslashes; use the shared case-sensitive setup."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Select only client and server errors",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-19-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-19-001-change-command",
    "program_block": "RGX-19-001-change-run",
    "note": "Narrow status group 2 from [1-5][0-9]{2} to [45][0-9]{2}. The Perl check prints only captured 4xx/5xx records; successful 2xx requests disappear from this diagnostic view.",
    "goal": "Narrow status group 2 from [1-5][0-9]{2} to [45][0-9]{2}. The Perl check prints only captured 4xx/5xx records; successful 2xx requests disappear from this diagnostic view.",
    "kind": "change",
    "show_command": true
  },
  "aliases": [],
  "compact": true
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/logs.txt <<'RGX_INPUT'
2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
RGX_INPUT
grep -o '^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]\{1,\}\) path=[^[:space:]]\{1,\}$' data/logs.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/logs.txt <<'RGX_INPUT'
2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
RGX_INPUT
grep -Eo '^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([1-5][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$' data/logs.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/logs.txt <<'RGX_INPUT'
2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
RGX_INPUT
perl -pe '
s{^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([1-5][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$}{time=$1 code=$2 id=$3}g;
' data/logs.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/logs.txt <<'RGX_INPUT'
2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
RGX_INPUT
sed 's~^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]\{1,\}\) path=[^[:space:]]\{1,\}$~time=\1 code=\2 id=\3~g' data/logs.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/logs.txt <<'RGX_INPUT'
2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}Z\) status=\([1-5][0-9]\{2\}\) request=\([A-Za-z0-9-]+\) path=[^[:space:]]+$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
time=\1 code=\2 id=\3
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/logs.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/logs.txt <<'RGX_INPUT'
2026-11-01T09:15:00Z status=200 request=req-17 path=/guide
2026-11-01T09:15:01Z status=404 request=req-18 path=/missing
2026-11-01T09:15:02Z status=503 request=req-19 path=/api?q=1
2026-11-01T09:15:03Z status=200 request=- path=/health
2026-11-01T09:15:04Z status=20 request=req-20 path=/bad
unstructured log message
RGX_INPUT
perl -ne '
if (m{^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([45][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$}) {
  printf "time=%s code=%s id=%s\n", $1, $2, $3;
}
' data/logs.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-20-001

Normalize DOI fields in literature data

Accept a bare DOI candidate or a lowercase doi: prefix with optional horizontal blanks. Group 1 is the display prefix; group 2 is the identifier. The bounded ASCII suffix policy includes parentheses and punctuation; it is not a universal DOI validator. These fictional examples require no lookup. Prefix normalization preserves identifier case. For surname/given-name conversion, compare RGX-06-001.

Shared input · data/dois.txt
doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
All variations for RGX-20-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\(doi:[[:blank:]]*\)\{0,1\}\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]\{1,\}\)$
Search only[.] means a literal dot; the registrant part has 4–9 ASCII digits.
grep -E / ERE
^(doi:[[:blank:]]*)?(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)$
Search onlyThe DOI is group 2; parentheses inside its character class are literal.
Perl
^(doi:[[:blank:]]*)?(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)$
https://doi.org/$2
Discard the display prefix, then prepend https://doi.org/ to $2.
sed / BRE
^\(doi:[[:blank:]]*\)\{0,1\}\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]\{1,\}\)$
https://doi.org/\2
Default BRE captures the same identifier; replacement uses \2.
Emacs
^\(doi:[[:blank:]]*\)?\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]+\)$
https://doi.org/\2
Use the exact Emacs field; preserve case with case-replace set to nil.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)

DOI URI fields · Perl, sed and Emacs

https://doi.org/10.1234/ALPHA.7
https://doi.org/10.56789/notes-2
https://doi.org/10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
Change one assumption · Try unanchored replacement inside prose

Searching inside prose can find more candidates, but a global replacement can retain a sentence-final period and duplicate an existing resolver prefix. These are real counterexamples: extraction and field normalization need different contracts. Crossref discusses punctuation bycatch; a match does not establish registration.

Changed regexp

(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)

Replacement

https://doi.org/$1

Changed result

doi:https://doi.org/10.1234/ALPHA.7
https://doi.org/10.56789/notes-2
doi: https://doi.org/10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See https://doi.org/10.1234/ALPHA.7.
https://doi.org/https://doi.org/10.56789/notes-2

Change one assumption: Try unanchored replacement inside prose · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\(doi:[[:blank:]]*\)\{0,1\}\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]\{1,\}\)$' data/dois.txt

grep -E / ERE

grep -Eo '^(doi:[[:blank:]]*)?(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)$' data/dois.txt

Perl

perl -pe '
s{^(doi:[[:blank:]]*)?(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)$}{https://doi.org/$2}g;
' data/dois.txt

sed / BRE

sed 's~^\(doi:[[:blank:]]*\)\{0,1\}\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]\{1,\}\)$~https://doi.org/\2~g' data/dois.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-20-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-20-001-bre-command",
      "program_block": "RGX-20-001-bre-run",
      "note": "[.] means a literal dot; the registrant part has 4–9 ASCII digits."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-20-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-20-001-ere-command",
      "program_block": "RGX-20-001-ere-run",
      "note": "The DOI is group 2; parentheses inside its character class are literal."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-20-001-perl-pattern",
      "replacement_block": "RGX-20-001-perl-replacement",
      "command_block": "RGX-20-001-perl-command",
      "program_block": "RGX-20-001-perl-run",
      "note": "Discard the display prefix, then prepend https://doi.org/ to $2."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-20-001-sed-pattern",
      "replacement_block": "RGX-20-001-sed-replacement",
      "command_block": "RGX-20-001-sed-command",
      "program_block": "RGX-20-001-sed-run",
      "note": "Default BRE captures the same identifier; replacement uses \\2."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-20-001-emacs-pattern",
      "replacement_block": "RGX-20-001-emacs-replacement",
      "command_block": "RGX-20-001-emacs-command",
      "program_block": "RGX-20-001-emacs-run",
      "note": "Use the exact Emacs field; preserve case with case-replace set to nil."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Try unanchored replacement inside prose",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-20-001-change-pattern",
    "replacement_block": "RGX-20-001-change-replacement",
    "command_block": "RGX-20-001-change-command",
    "program_block": "RGX-20-001-change-run",
    "note": "Searching inside prose can find more candidates, but a global replacement can retain a sentence-final period and duplicate an existing resolver prefix. These are real counterexamples: extraction and field normalization need different contracts. Crossref discusses punctuation bycatch; a match does not establish registration.",
    "goal": "Searching inside prose can find more candidates, but a global replacement can retain a sentence-final period and duplicate an existing resolver prefix. These are real counterexamples: extraction and field normalization need different contracts. Crossref discusses punctuation bycatch; a match does not establish registration.",
    "kind": "change"
  },
  "aliases": [],
  "compact": true,
  "replacement_label": "DOI URI fields · Perl, sed and Emacs"
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dois.txt <<'RGX_INPUT'
doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
RGX_INPUT
grep -o '^\(doi:[[:blank:]]*\)\{0,1\}\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]\{1,\}\)$' data/dois.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dois.txt <<'RGX_INPUT'
doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
RGX_INPUT
grep -Eo '^(doi:[[:blank:]]*)?(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)$' data/dois.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dois.txt <<'RGX_INPUT'
doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
RGX_INPUT
perl -pe '
s{^(doi:[[:blank:]]*)?(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)$}{https://doi.org/$2}g;
' data/dois.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dois.txt <<'RGX_INPUT'
doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
RGX_INPUT
sed 's~^\(doi:[[:blank:]]*\)\{0,1\}\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]\{1,\}\)$~https://doi.org/\2~g' data/dois.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dois.txt <<'RGX_INPUT'
doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\(doi:[[:blank:]]*\)?\(10[.][0-9]\{4,9\}/[-A-Za-z0-9._;()/:]+\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
https://doi.org/\2
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/dois.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dois.txt <<'RGX_INPUT'
doi:10.1234/ALPHA.7
10.56789/notes-2
doi: 10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See 10.1234/ALPHA.7.
https://doi.org/10.56789/notes-2
RGX_INPUT
perl -pe '
s{(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)}{https://doi.org/$1}g;
' data/dois.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-21-001

Reassemble project filenames with captures

The contract is project_YYYYMMDD_vNN.ext: a project token may contain letters, digits and hyphens, the version has exactly two digits and the extension has three lowercase letters. Groups 1–6 are project/year/month/day/version/extension. Put the date first without dropping zero padding. The input is a text list; no files are renamed. Date spelling alone does not validate a calendar day; see RGX-23-001.

Shared input · data/filenames.txt
atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
All variations for RGX-21-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$
Search only[.] separates the extension literally; fixed repeats enforce padding.
grep -E / ERE
^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{2})[.]([a-z]{3})$
Search onlySix captures define the handover; group 5 keeps the original version text.
Perl
^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{2})[.]([a-z]{3})$
${2}-${3}-${4}_${1}_v${5}.${6}
Braced numbered references make adjacency to underscores explicit.
sed / BRE
^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$
\2-\3-\4_\1_v\5.\6
The same six captures can be reordered with \2, \3, \4, \1, \5, \6.
Emacs
^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$
\2-\3-\4_\1_v\5.\6
Use the displayed replacement directly; it is not a Lisp string.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv

After replacement · Perl, sed and Emacs

2026-11-01_atlas_v02.csv
2026-10-31_atlas_v12.tsv
2026-02-29_rna-seq_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
Change one assumption · Allow versions with one to three digits

Relax only group 5 from two digits to one through three. The previously excluded v2 record now transforms. Keep the supplied spelling: accepting variable width does not silently zero-pad the version.

Changed regexp

^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{1,3})[.]([a-z]{3})$

Replacement

${2}-${3}-${4}_${1}_v${5}.${6}

Changed result

2026-11-01_atlas_v02.csv
2026-10-31_atlas_v12.tsv
2026-02-29_rna-seq_v01.csv
2026-11-01_atlas_v2.csv
my_project_20261101_v02.csv
notes.txt

Change one assumption: Allow versions with one to three digits · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$' data/filenames.txt

grep -E / ERE

grep -Eo '^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{2})[.]([a-z]{3})$' data/filenames.txt

Perl

perl -pe '
s{^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{2})[.]([a-z]{3})$}{${2}-${3}-${4}_${1}_v${5}.${6}}g;
' data/filenames.txt

sed / BRE

sed 's~^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$~\2-\3-\4_\1_v\5.\6~g' data/filenames.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-21-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-21-001-bre-command",
      "program_block": "RGX-21-001-bre-run",
      "note": "[.] separates the extension literally; fixed repeats enforce padding."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-21-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-21-001-ere-command",
      "program_block": "RGX-21-001-ere-run",
      "note": "Six captures define the handover; group 5 keeps the original version text."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-21-001-perl-pattern",
      "replacement_block": "RGX-21-001-perl-replacement",
      "command_block": "RGX-21-001-perl-command",
      "program_block": "RGX-21-001-perl-run",
      "note": "Braced numbered references make adjacency to underscores explicit."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-21-001-sed-pattern",
      "replacement_block": "RGX-21-001-sed-replacement",
      "command_block": "RGX-21-001-sed-command",
      "program_block": "RGX-21-001-sed-run",
      "note": "The same six captures can be reordered with \\2, \\3, \\4, \\1, \\5, \\6."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-21-001-emacs-pattern",
      "replacement_block": "RGX-21-001-emacs-replacement",
      "command_block": "RGX-21-001-emacs-command",
      "program_block": "RGX-21-001-emacs-run",
      "note": "Use the displayed replacement directly; it is not a Lisp string."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Allow versions with one to three digits",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-21-001-change-pattern",
    "replacement_block": "RGX-21-001-change-replacement",
    "command_block": "RGX-21-001-change-command",
    "program_block": "RGX-21-001-change-run",
    "note": "Relax only group 5 from two digits to one through three. The previously excluded v2 record now transforms. Keep the supplied spelling: accepting variable width does not silently zero-pad the version.",
    "goal": "Relax only group 5 from two digits to one through three. The previously excluded v2 record now transforms. Keep the supplied spelling: accepting variable width does not silently zero-pad the version.",
    "kind": "change"
  },
  "aliases": [],
  "compact": true
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/filenames.txt <<'RGX_INPUT'
atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
RGX_INPUT
grep -o '^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$' data/filenames.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/filenames.txt <<'RGX_INPUT'
atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
RGX_INPUT
grep -Eo '^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{2})[.]([a-z]{3})$' data/filenames.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/filenames.txt <<'RGX_INPUT'
atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
RGX_INPUT
perl -pe '
s{^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{2})[.]([a-z]{3})$}{${2}-${3}-${4}_${1}_v${5}.${6}}g;
' data/filenames.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/filenames.txt <<'RGX_INPUT'
atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
RGX_INPUT
sed 's~^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$~\2-\3-\4_\1_v\5.\6~g' data/filenames.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/filenames.txt <<'RGX_INPUT'
atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([A-Za-z][A-Za-z0-9-]*\)_\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_v\([0-9]\{2\}\)[.]\([a-z]\{3\}\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\2-\3-\4_\1_v\5.\6
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/filenames.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/filenames.txt <<'RGX_INPUT'
atlas_20261101_v02.csv
atlas_20261031_v12.tsv
rna-seq_20260229_v01.csv
atlas_20261101_v2.csv
my_project_20261101_v02.csv
notes.txt
RGX_INPUT
perl -pe '
s{^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{1,3})[.]([a-z]{3})$}{${2}-${3}-${4}_${1}_v${5}.${6}}g;
' data/filenames.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-22-001

Normalize horizontal whitespace and repeated words

Group 1 captures an ASCII word; [[:blank:]]+ consumes its following horizontal whitespace. Replace the whole match by that word and one space. The Use line contains two real tabs. Line breaks and case stay unchanged. Matched-text results retain trailing whitespace, which can be hard to see; the replacement result makes the cleanup visible. The exercise then adds a distinct repeated-word pass.

Shared input · data/cleanup.txt
Keep   spacing   readable.
Use		one separator.
the the result
go go go
The the result
line break stays
All variations for RGX-22-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
\([A-Za-z]\{1,\}\)[[:blank:]]\{1,\}
Search onlyA word capture followed by one or more blanks; neither class matches LF.
grep -E / ERE
([A-Za-z]+)[[:blank:]]+
Search onlyUse [[:blank:]], not a portable-looking but ambiguous backslash shortcut.
Perl
([A-Za-z]+)[[:blank:]]+
$1 
Replace with $1 and one trailing space; leave punctuation and case intact.
sed / BRE
\([A-Za-z]\{1,\}\)[[:blank:]]\{1,\}
\1 
Keep \1 plus one space. Do not trim the replacement field.
Emacs
\([A-Za-z]+\)[[:blank:]]+
\1 
Copy the trailing space too. Leave lax-whitespace nil for exact fields.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

Keep   
spacing   
Use		
one 
the 
the 
go 
go 
The 
the 
line 
break 

After replacement · Perl, sed and Emacs

Keep spacing readable.
Use one separator.
the the result
go go go
The the result
line break stays
Change one assumption · Add a second pass for repeated whole words

First apply the whitespace cleanup above, then collapse adjacent identical whole words with a pattern backreference. The repeated group consumes the whole go go go run. Matching remains case-sensitive, so The the survives. The two cleanup decisions remain separate; the complete script shows both passes.

Changed regexp

\b([A-Za-z]+)([[:blank:]]+\1)+\b

Replacement

$1

Changed result

Keep spacing readable.
Use one separator.
the result
go
The the result
line break stays
Changed command · Perl
perl -pe '
s{([A-Za-z]+)[[:blank:]]+}{$1 }g;
s{\b([A-Za-z]+)([[:blank:]]+\1)+\b}{$1}g;
' data/cleanup.txt

Change one assumption: Add a second pass for repeated whole words · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '\([A-Za-z]\{1,\}\)[[:blank:]]\{1,\}' data/cleanup.txt

grep -E / ERE

grep -Eo '([A-Za-z]+)[[:blank:]]+' data/cleanup.txt

Perl

perl -pe '
s{([A-Za-z]+)[[:blank:]]+}{$1 }g;
' data/cleanup.txt

sed / BRE

sed 's~\([A-Za-z]\{1,\}\)[[:blank:]]\{1,\}~\1 ~g' data/cleanup.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-22-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-22-001-bre-command",
      "program_block": "RGX-22-001-bre-run",
      "note": "A word capture followed by one or more blanks; neither class matches LF."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-22-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-22-001-ere-command",
      "program_block": "RGX-22-001-ere-run",
      "note": "Use [[:blank:]], not a portable-looking but ambiguous backslash shortcut."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-22-001-perl-pattern",
      "replacement_block": "RGX-22-001-perl-replacement",
      "command_block": "RGX-22-001-perl-command",
      "program_block": "RGX-22-001-perl-run",
      "note": "Replace with $1 and one trailing space; leave punctuation and case intact."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-22-001-sed-pattern",
      "replacement_block": "RGX-22-001-sed-replacement",
      "command_block": "RGX-22-001-sed-command",
      "program_block": "RGX-22-001-sed-run",
      "note": "Keep \\1 plus one space. Do not trim the replacement field."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-22-001-emacs-pattern",
      "replacement_block": "RGX-22-001-emacs-replacement",
      "command_block": "RGX-22-001-emacs-command",
      "program_block": "RGX-22-001-emacs-run",
      "note": "Copy the trailing space too. Leave lax-whitespace nil for exact fields."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Add a second pass for repeated whole words",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-22-001-change-pattern",
    "replacement_block": "RGX-22-001-change-replacement",
    "command_block": "RGX-22-001-change-command",
    "program_block": "RGX-22-001-change-run",
    "note": "First apply the whitespace cleanup above, then collapse adjacent identical whole words with a pattern backreference. The repeated group consumes the whole go go go run. Matching remains case-sensitive, so The the survives. The two cleanup decisions remain separate; the complete script shows both passes.",
    "goal": "First apply the whitespace cleanup above, then collapse adjacent identical whole words with a pattern backreference. The repeated group consumes the whole go go go run. Matching remains case-sensitive, so The the survives. The two cleanup decisions remain separate; the complete script shows both passes.",
    "kind": "change",
    "show_command": true
  },
  "aliases": [],
  "compact": true
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/cleanup.txt <<'RGX_INPUT'
Keep   spacing   readable.
Use		one separator.
the the result
go go go
The the result
line break stays
RGX_INPUT
grep -o '\([A-Za-z]\{1,\}\)[[:blank:]]\{1,\}' data/cleanup.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/cleanup.txt <<'RGX_INPUT'
Keep   spacing   readable.
Use		one separator.
the the result
go go go
The the result
line break stays
RGX_INPUT
grep -Eo '([A-Za-z]+)[[:blank:]]+' data/cleanup.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/cleanup.txt <<'RGX_INPUT'
Keep   spacing   readable.
Use		one separator.
the the result
go go go
The the result
line break stays
RGX_INPUT
perl -pe '
s{([A-Za-z]+)[[:blank:]]+}{$1 }g;
' data/cleanup.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/cleanup.txt <<'RGX_INPUT'
Keep   spacing   readable.
Use		one separator.
the the result
go go go
The the result
line break stays
RGX_INPUT
sed 's~\([A-Za-z]\{1,\}\)[[:blank:]]\{1,\}~\1 ~g' data/cleanup.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/cleanup.txt <<'RGX_INPUT'
Keep   spacing   readable.
Use		one separator.
the the result
go go go
The the result
line break stays
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
\([A-Za-z]+\)[[:blank:]]+
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\1 
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/cleanup.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/cleanup.txt <<'RGX_INPUT'
Keep   spacing   readable.
Use		one separator.
the the result
go go go
The the result
line break stays
RGX_INPUT
perl -pe '
s{([A-Za-z]+)[[:blank:]]+}{$1 }g;
s{\b([A-Za-z]+)([[:blank:]]+\1)+\b}{$1}g;
' data/cleanup.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-23-001

Reorder date fields and check calendar rules

Capture exactly DD.MM.YYYY as day/month/year groups 1/2/3 and replace it by YYYY-MM-DD. The regex checks digit counts and literal separators, so impossible dates also transform. The additional offline check applies Gregorian month lengths and leap-year rules to years 0001–9999: 1900 is not a leap year, while 2000 is. Input already in ISO order stays unchanged.

Shared input · data/dates.txt
01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026
2026-11-01
All variations for RGX-23-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$
Search onlyEscaped count braces and groups; [.] keeps the separators literal.
grep -E / ERE
^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$
Search onlyAnchors reject the existing ISO form. Digit counts do not validate dates.
Perl
^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$
$3-$2-$1
Use $3-$2-$1; capture values preserve the leading zeros.
sed / BRE
^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$
\3-\2-\1
Use \3-\2-\1 in replacement; no date utility is invoked.
Emacs
^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$
\3-\2-\1
The actual minibuffer fields use literal backslashes, without Lisp quotes.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026

After replacement · Perl, sed and Emacs

2026-11-01
2024-02-29
2026-02-29
2026-04-31
1900-02-29
2000-02-29
2026-13-00
2026-11-01
Additional check · Check month lengths and century leap years

After the format match, parse captures as decimal values. Require year >= 1, month 1–12 and day within that month. A leap year is divisible by 400, or divisible by 4 but not 100. Emit an ISO date only for valid dates. The 1900/2000 examples test the century rule explicitly.

Format regexp

^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$

Additional check result

calendar-ok	2026-11-01
calendar-ok	2024-02-29
invalid-date	29.02.2026
invalid-date	31.04.2026
invalid-date	29.02.1900
calendar-ok	2000-02-29
invalid-date	00.13.2026
outside-format	2026-11-01
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$}) {
  my ($day, $month, $year) = (0 + $1, 0 + $2, 0 + $3);
  my $leap = $year % 400 == 0 || ($year % 4 == 0 && $year % 100 != 0);
  my @days = (0, 31, 28 + $leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
  if ($year >= 1 && $month >= 1 && $month <= 12 && $day >= 1 && $day <= $days[$month]) {
    printf "calendar-ok\t%04d-%02d-%02d\n", $year, $month, $day;
  } else {
    printf "invalid-date\t%s\n", $input;
  }
} else {
  printf "outside-format\t%s\n", $input;
}
' data/dates.txt

Additional check: Check month lengths and century leap years · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$' data/dates.txt

grep -E / ERE

grep -Eo '^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$' data/dates.txt

Perl

perl -pe '
s{^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$}{$3-$2-$1}g;
' data/dates.txt

sed / BRE

sed 's~^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$~\3-\2-\1~g' data/dates.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-23-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-23-001-bre-command",
      "program_block": "RGX-23-001-bre-run",
      "note": "Escaped count braces and groups; [.] keeps the separators literal."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-23-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-23-001-ere-command",
      "program_block": "RGX-23-001-ere-run",
      "note": "Anchors reject the existing ISO form. Digit counts do not validate dates."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-23-001-perl-pattern",
      "replacement_block": "RGX-23-001-perl-replacement",
      "command_block": "RGX-23-001-perl-command",
      "program_block": "RGX-23-001-perl-run",
      "note": "Use $3-$2-$1; capture values preserve the leading zeros."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-23-001-sed-pattern",
      "replacement_block": "RGX-23-001-sed-replacement",
      "command_block": "RGX-23-001-sed-command",
      "program_block": "RGX-23-001-sed-run",
      "note": "Use \\3-\\2-\\1 in replacement; no date utility is invoked."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-23-001-emacs-pattern",
      "replacement_block": "RGX-23-001-emacs-replacement",
      "command_block": "RGX-23-001-emacs-command",
      "program_block": "RGX-23-001-emacs-run",
      "note": "The actual minibuffer fields use literal backslashes, without Lisp quotes."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Check month lengths and century leap years",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-23-001-change-pattern",
    "replacement_block": null,
    "command_block": "RGX-23-001-change-command",
    "program_block": "RGX-23-001-change-run",
    "note": "After the format match, parse captures as decimal values. Require year >= 1, month 1–12 and day within that month. A leap year is divisible by 400, or divisible by 4 but not 100. Emit an ISO date only for valid dates. The 1900/2000 examples test the century rule explicitly.",
    "goal": "After the format match, parse captures as decimal values. Require year >= 1, month 1–12 and day within that month. A leap year is divisible by 400, or divisible by 4 but not 100. Emit an ISO date only for valid dates. The 1900/2000 examples test the century rule explicitly.",
    "kind": "semantic-check",
    "pattern_label": "Format regexp",
    "result_label": "Additional check result"
  },
  "aliases": [],
  "compact": true
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dates.txt <<'RGX_INPUT'
01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026
2026-11-01
RGX_INPUT
grep -o '^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$' data/dates.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dates.txt <<'RGX_INPUT'
01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026
2026-11-01
RGX_INPUT
grep -Eo '^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$' data/dates.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dates.txt <<'RGX_INPUT'
01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026
2026-11-01
RGX_INPUT
perl -pe '
s{^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$}{$3-$2-$1}g;
' data/dates.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dates.txt <<'RGX_INPUT'
01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026
2026-11-01
RGX_INPUT
sed 's~^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$~\3-\2-\1~g' data/dates.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dates.txt <<'RGX_INPUT'
01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026
2026-11-01
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^\([0-9]\{2\}\)[.]\([0-9]\{2\}\)[.]\([0-9]\{4\}\)$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
\3-\2-\1
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/dates.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/dates.txt <<'RGX_INPUT'
01.11.2026
29.02.2024
29.02.2026
31.04.2026
29.02.1900
29.02.2000
00.13.2026
2026-11-01
RGX_INPUT
perl -ne '
chomp;
my $input = $_;
if (m{^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$}) {
  my ($day, $month, $year) = (0 + $1, 0 + $2, 0 + $3);
  my $leap = $year % 400 == 0 || ($year % 4 == 0 && $year % 100 != 0);
  my @days = (0, 31, 28 + $leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
  if ($year >= 1 && $month >= 1 && $month <= 12 && $day >= 1 && $day <= $days[$month]) {
    printf "calendar-ok\t%04d-%02d-%02d\n", $year, $month, $day;
  } else {
    printf "invalid-date\t%s\n", $input;
  }
} else {
  printf "outside-format\t%s\n", $input;
}
' data/dates.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

RGX-24-001

Migrate URL paths while preserving query and fragment

Match only the exact old.example.org host and the /docs/slug/YYYY path. Groups 1/2/3 hold slug/year/optional query or fragment. Build the new host and /guides/YYYY/slug, then append group 3 unchanged. The host dots are literal and the path is anchored before its optional tail. Similar hostnames, extra path segments and other sites stay unchanged. These are text rewrites, not HTTP requests or server redirects.

Shared input · data/migration.txt
https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
All variations for RGX-24-001
VariantSearch regexpReplacementDifference to notice
Plain / BRE (grep)
^https://old[.]example[.]org/docs/\([A-Za-z0-9-]\{1,\}\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)\{0,1\}$
Search onlyBRE groups and optional tail need escapes; host dots stay literal.
grep -E / ERE
^https://old[.]example[.]org/docs/([A-Za-z0-9-]+)/([0-9]{4})([?#][^[:space:]]*)?$
Search onlyAnchors constrain host and path; the optional query/fragment is group 3.
Perl
^https://old[.]example[.]org/docs/([A-Za-z0-9-]+)/([0-9]{4})([?#][^[:space:]]*)?$
https://new.example.org/guides/$2/$1$3
Swap $2/$1, preserve $3 and change only the declared host.
sed / BRE
^https://old[.]example[.]org/docs/\([A-Za-z0-9-]\{1,\}\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)\{0,1\}$
https://new.example.org/guides/\2/\1\3
Use \2/\1\3; an unmatched tail inserts empty text.
Emacs
^https://old[.]example[.]org/docs/\([A-Za-z0-9-]+\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)?$
https://new.example.org/guides/\2/\1\3
The same tail capture preserves query/fragment text, including /docs/ inside it.

Emacs: enter the two fields with M-x replace-regexp. Multiline BRE entries are separate grep -e patterns. Commands are available below.

Matched text · BRE and grep -E

https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024

After replacement · Perl, sed and Emacs

https://new.example.org/guides/2026/regex
https://new.example.org/guides/2025/data-tools?view=all#top
https://new.example.org/guides/2026/regex?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
Change one assumption · Try a global path-only rewrite

Remove the host and end constraints. The path-only pattern now rewrites other hosts, longer paths and even the nested /docs/ path inside the query value. Compare with the main result: host selection and tail preservation are part of the migration contract.

Changed regexp

/docs/([A-Za-z0-9-]+)/([0-9]{4})

Replacement

/guides/$2/$1

Changed result

https://old.example.org/guides/2026/regex
https://old.example.org/guides/2025/data-tools?view=all#top
https://old.example.org/guides/2026/regex?next=/guides/2024/awk
https://old.example.org.evil.test/guides/2026/regex
https://old.example.org/guides/2026/regex/extra
https://elsewhere.example.org/guides/2026/regex

Change one assumption: Try a global path-only rewrite · see the comparison-check appendix.

Tool commands and Emacs instructions

Plain / BRE (grep)

grep -o '^https://old[.]example[.]org/docs/\([A-Za-z0-9-]\{1,\}\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)\{0,1\}$' data/migration.txt

grep -E / ERE

grep -Eo '^https://old[.]example[.]org/docs/([A-Za-z0-9-]+)/([0-9]{4})([?#][^[:space:]]*)?$' data/migration.txt

Perl

perl -pe '
s{^https://old[.]example[.]org/docs/([A-Za-z0-9-]+)/([0-9]{4})([?#][^[:space:]]*)?$}{https://new.example.org/guides/$2/$1$3}g;
' data/migration.txt

sed / BRE

sed 's~^https://old[.]example[.]org/docs/\([A-Za-z0-9-]\{1,\}\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)\{0,1\}$~https://new.example.org/guides/\2/\1\3~g' data/migration.txt

Emacs

Use M-x replace-regexp with the Search regexp and Replacement above; start at the beginning with no active region.

Complete verification program · all variants
set -euo pipefail
export LC_ALL=C
topic_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-topic.XXXXXX")
trap 'rm -rf -- "$topic_work"' EXIT
cd "$topic_work"
cat > topic.json <<'REGEX_TOPIC_SPEC'
{
  "variants": [
    {
      "key": "bre",
      "label": "Plain / BRE (grep)",
      "tool": "grep",
      "engine": "grep BRE (POSIX patterns; GNU/BSD -o)",
      "pattern_block": "RGX-24-001-bre-pattern",
      "replacement_block": null,
      "command_block": "RGX-24-001-bre-command",
      "program_block": "RGX-24-001-bre-run",
      "note": "BRE groups and optional tail need escapes; host dots stay literal."
    },
    {
      "key": "ere",
      "label": "grep -E / ERE",
      "tool": "grep",
      "engine": "grep ERE (GNU/BSD -Eo)",
      "pattern_block": "RGX-24-001-ere-pattern",
      "replacement_block": null,
      "command_block": "RGX-24-001-ere-command",
      "program_block": "RGX-24-001-ere-run",
      "note": "Anchors constrain host and path; the optional query/fragment is group 3."
    },
    {
      "key": "perl",
      "label": "Perl",
      "tool": "perl",
      "engine": "Perl regex",
      "pattern_block": "RGX-24-001-perl-pattern",
      "replacement_block": "RGX-24-001-perl-replacement",
      "command_block": "RGX-24-001-perl-command",
      "program_block": "RGX-24-001-perl-run",
      "note": "Swap $2/$1, preserve $3 and change only the declared host."
    },
    {
      "key": "sed",
      "label": "sed / BRE",
      "tool": "sed",
      "engine": "sed BRE",
      "pattern_block": "RGX-24-001-sed-pattern",
      "replacement_block": "RGX-24-001-sed-replacement",
      "command_block": "RGX-24-001-sed-command",
      "program_block": "RGX-24-001-sed-run",
      "note": "Use \\2/\\1\\3; an unmatched tail inserts empty text."
    },
    {
      "key": "emacs",
      "label": "Emacs",
      "tool": "emacs",
      "engine": "Emacs regex",
      "pattern_block": "RGX-24-001-emacs-pattern",
      "replacement_block": "RGX-24-001-emacs-replacement",
      "command_block": "RGX-24-001-emacs-command",
      "program_block": "RGX-24-001-emacs-run",
      "note": "The same tail capture preserves query/fragment text, including /docs/ inside it."
    }
  ],
  "experiment": {
    "key": "change",
    "label": "Try a global path-only rewrite",
    "tool": "perl",
    "engine": "Perl regex",
    "pattern_block": "RGX-24-001-change-pattern",
    "replacement_block": "RGX-24-001-change-replacement",
    "command_block": "RGX-24-001-change-command",
    "program_block": "RGX-24-001-change-run",
    "note": "Remove the host and end constraints. The path-only pattern now rewrites other hosts, longer paths and even the nested /docs/ path inside the query value. Compare with the main result: host selection and tail preservation are part of the migration contract.",
    "goal": "Remove the host and end constraints. The path-only pattern now rewrites other hosts, longer paths and even the nested /docs/ path inside the query value. Compare with the main result: host selection and tail preservation are part of the migration contract.",
    "kind": "change"
  },
  "aliases": [],
  "compact": true
}
REGEX_TOPIC_SPEC
cat > compare.py <<'REGEX_TOPIC_RUNNER'
#!/usr/bin/env python3
"""Capture the six source-owned programs for one regexp issue."""
import json
import os
from pathlib import Path
import signal
import subprocess
import sys


def capture(key):
    program = Path(key + '.sh')
    if not program.is_file() or program.is_symlink():
        raise ValueError('Missing regular comparison program: ' + key)
    with subprocess.Popen(['bash', '--noprofile', '--norc', str(program)],
                          stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE, text=True, start_new_session=True) as process:
        try:
            stdout, stderr = process.communicate(timeout=20)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            process.communicate()
            raise RuntimeError('Comparison program timed out: ' + key) from None
    return dict(key=key, stdout=stdout, stderr=stderr, exit=process.returncode)


def main():
    spec = json.loads(Path('topic.json').read_text())
    keys = [entry['key'] for entry in spec['variants']]
    if keys != ['bre', 'ere', 'perl', 'sed', 'emacs'] or spec['experiment']['key'] != 'change':
        raise ValueError('Expected five dialect views and one changed assumption.')
    result = dict(variants=[capture(key) for key in keys], experiment=capture('change'))
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return int(any(run['exit'] != 0 for run in result['variants'] + [result['experiment']]))


if __name__ == '__main__':
    try:
        raise SystemExit(main())
    except (OSError, ValueError, RuntimeError) as error:
        raise SystemExit('Regex comparison: ' + str(error)) from None
REGEX_TOPIC_RUNNER
cat > bre.sh <<'REGEX_PROGRAM_BRE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/migration.txt <<'RGX_INPUT'
https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
RGX_INPUT
grep -o '^https://old[.]example[.]org/docs/\([A-Za-z0-9-]\{1,\}\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)\{0,1\}$' data/migration.txt
REGEX_PROGRAM_BRE
cat > ere.sh <<'REGEX_PROGRAM_ERE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/migration.txt <<'RGX_INPUT'
https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
RGX_INPUT
grep -Eo '^https://old[.]example[.]org/docs/([A-Za-z0-9-]+)/([0-9]{4})([?#][^[:space:]]*)?$' data/migration.txt
REGEX_PROGRAM_ERE
cat > perl.sh <<'REGEX_PROGRAM_PERL'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/migration.txt <<'RGX_INPUT'
https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
RGX_INPUT
perl -pe '
s{^https://old[.]example[.]org/docs/([A-Za-z0-9-]+)/([0-9]{4})([?#][^[:space:]]*)?$}{https://new.example.org/guides/$2/$1$3}g;
' data/migration.txt
REGEX_PROGRAM_PERL
cat > sed.sh <<'REGEX_PROGRAM_SED'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/migration.txt <<'RGX_INPUT'
https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
RGX_INPUT
sed 's~^https://old[.]example[.]org/docs/\([A-Za-z0-9-]\{1,\}\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)\{0,1\}$~https://new.example.org/guides/\2/\1\3~g' data/migration.txt
REGEX_PROGRAM_SED
cat > emacs.sh <<'REGEX_PROGRAM_EMACS'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/migration.txt <<'RGX_INPUT'
https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
RGX_INPUT
cat > regexp.txt <<'RGX_REGEXP'
^https://old[.]example[.]org/docs/\([A-Za-z0-9-]+\)/\([0-9]\{4\}\)\([?#][^[:space:]]*\)?$
RGX_REGEXP
cat > replacement.txt <<'RGX_REPLACEMENT'
https://new.example.org/guides/\2/\1\3
RGX_REPLACEMENT
cat > example.el <<'RGX_ELISP'
(defun tour-read-editor-field (file)
  "Read one LF-terminated editor field without interpreting its backslashes."
  (with-temp-buffer
    (insert-file-contents-literally file)
    (unless (eq (char-before (point-max)) ?\n)
      (error "Expected an LF-terminated editor field: %s" file))
    (buffer-substring-no-properties (point-min) (1- (point-max)))))
(with-temp-buffer
  (insert-file-contents "data/migration.txt")
  (let ((case-fold-search nil)
        (case-replace nil)
        (replace-regexp-lax-whitespace nil)
        (inhibit-message t))
    (replace-regexp (tour-read-editor-field "regexp.txt")
                    (tour-read-editor-field "replacement.txt")
                    nil (point-min) (point-max)))
  (princ (buffer-string)))
RGX_ELISP
emacs --batch -Q --script example.el
REGEX_PROGRAM_EMACS
cat > change.sh <<'REGEX_PROGRAM_CHANGE'
set -euo pipefail
export LC_ALL=C
regex_work=$(mktemp -d "${TMPDIR:-/tmp}/regex-example.XXXXXX")
trap 'rm -rf -- "$regex_work"' EXIT
cd "$regex_work"
mkdir data
cat > data/migration.txt <<'RGX_INPUT'
https://old.example.org/docs/regex/2026
https://old.example.org/docs/data-tools/2025?view=all#top
https://old.example.org/docs/regex/2026?next=/docs/awk/2024
https://old.example.org.evil.test/docs/regex/2026
https://old.example.org/docs/regex/2026/extra
https://elsewhere.example.org/docs/regex/2026
RGX_INPUT
perl -pe '
s{/docs/([A-Za-z0-9-]+)/([0-9]{4})}{/guides/$2/$1}g;
' data/migration.txt
REGEX_PROGRAM_CHANGE
TMPDIR="$topic_work" python3 compare.py

Run all variants for this issue

Comparison checks · assumptions and validation

Each check belongs to the issue ID shown. The HTML keeps these results inside their issue; the print edition collects them here.

RGX-01-001 · Let a wildcard match a period

Remove the literal-dot character class. The wildcard also changes reportXcsv, which the literal pattern left alone.

Changed regexp

report.csv

Replacement

report.tsv

Changed result

report.tsv
report.tsv
report.tsv.bak
README
REPORT.csv

RGX-02-001 · Search without whole-record anchors

Remove the anchors. The expression now extracts identifiers embedded in longer records.

Changed regexp

[A-Z]{2}-[0-9]{3}

Changed result

AB-123
AB-123
ZZ-999
AB-123

RGX-03-001 · Try the POSIX digit class

Use the POSIX digit class instead of an ASCII range. On these ASCII inputs the result is identical; this does not make Perl-style backslash-d portable.

Changed regexp

^t=[[:digit:]]+$

Changed result

t=15
t=7
t=22

RGX-04-001 · Narrow the repetition bound

Require exactly two digits instead of two or three. Records with three digits are excluded.

Changed regexp

^colou?r=[0-9]{2}$

Changed result

color=12
colour=99

RGX-05-001 · Remove the alternation group

Remove the alternation group. The beginning anchor applies only to cat, and the ending anchor only to dogs?.

Changed regexp

^cat|dogs?$

Changed result

cat
dog
cat
dogs
cat
dog
cat

RGX-06-001 · Reuse a capture twice

Reuse a captured value twice in replacement text: the surname appears after the given name and again in parentheses.

Changed regexp

^([A-Za-z]+), ([A-Za-z]+)$

Replacement

$2 $1 ($1)

Changed result

Jane Doe (Doe)
Sam Smith (Smith)
No comma
Ada Li (Li)

RGX-07-001 · Choose the wrong nested group numbers

Use the wrong nested group numbers. Read the incorrect date output to identify which opening parenthesis each number names.

Changed regexp

^(([0-9]{4})-([0-9]{2}))-([0-9]{2})$

Replacement

$3/$2/$1

Changed result

11/2026/2026-11
02/2025/2025-02
2026/11/01
date=2026-11-01

RGX-08-001 · Capture words without requiring equality

Capture two independent words instead of using a pattern backreference. Different words now match too.

Changed regexp

^([a-z]+) ([a-z]+)$

Changed result

red|red
blue|blue
red|blue

RGX-09-001 · Make the optional title mandatory

Make the title mandatory. The record without a title no longer matches.

Changed regexp

^([A-Za-z]+)(:(Dr|Prof))$

Changed result

Ada|Dr
Clara|Prof

RGX-10-001 · Add an outer named capture

Add an outer named group. Named replacement references still identify prefix and number even though numeric positions change.

Changed regexp

^(?<whole>(?<prefix>[A-Z]+)-(?<number>[0-9]+))$

Replacement

$+{number}/$+{prefix}

Changed result

17/AB
003/XY
bad-17
AB-x

RGX-11-001 · Let a structural group shift the numbers

Make the structural color group capturing but keep the old replacement number. The output contains the color instead of the number.

Changed regexp

^(red|blue):([0-9]+)$

Replacement

n=$1

Changed result

n=red
n=blue
green:9
red:x
n=blue

RGX-12-001 · Emacs: insert literal \1

Insert the literal characters backslash and 1. In the Emacs Replacement field, type \\1 rather than \1.

Changed regexp

order=\([0-9]+\)

Replacement

\\1

Changed result

\1
\1
other=3

RGX-13-001 · Emacs: compare greedy matching

Use greedy .* in Emacs instead of minimal .*?. The two adjacent tags on the first line become one match and one replacement.

Changed regexp

<b>.*</b>

Replacement

[item]

Changed result

[item]
[item]
plain

RGX-14-001 · Replace only the first match per line

Remove the Perl g flag. Only the first match on each input line is replaced.

Changed regexp

(a)([0-9]+)

Replacement

$2$1

Changed result

1a a2 a3
b4 5a
none

RGX-15-001 · Try an over-permissive email shortcut

Replacing the local/domain rules by non-space runs admits consecutive dots, a leading domain hyphen and a domain without a final dot. These are outside the declared email policy.

Changed regexp

^([^[:space:]@]+)@([^[:space:]@]+)$

Replacement

local=$1 domain=$2

Changed result

local=ada domain=example.org
local=ada.lovelace+notes domain=research.example.org
local=team domain=lab-2.example.org
local=bad..dots domain=example.org
local=name domain=-example.org
local=name domain=example
missing-at.example.org
"quoted local"@example.org

RGX-16-001 · Check the numeric port range

After the same format match, read group 7 as a decimal number and require 0–65535. shape+port-ok reports just these checks; it does not certify a complete or reachable URL.

Format regexp

^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$

Additional check result

shape+port-ok	https://example.org
shape+port-ok	http://data.example.org:8080/run?q=regex#part
shape+port-ok	https://example.org?q=one
shape+port-ok	https://example.org/a%20b
port-out-of-range	https://example.org:99999/path
outside-profile	ftp://example.org/file
outside-profile	https://bad..example.org/path
outside-profile	https://example.org/a b
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^(https?)://([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+)(:([0-9]{1,5}))?([/?#][^[:space:]]*)?$}) {
  my $port = $7;
  my $status = defined($port) && $port > 65535 ? "port-out-of-range" : "shape+port-ok";
  printf "%s\t%s\n", $status, $input;
} else {
  printf "outside-profile\t%s\n", $input;
}
' data/urls.txt

RGX-17-001 · Verify the ORCID checksum

Apply MOD 11-2 to the first 15 digits: repeatedly double the running total after adding a digit. The resulting check character is compared with the last character, with 10 represented by X. The supplied 0098 ending must fail; this offline check does not query the registry.

Format regexp

^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$

Additional check result

checksum-ok	0000-0002-1825-0097
checksum-ok	0000-0002-1694-233X
checksum-ok	0000-0002-1825-0097
bad-checksum(expected=7)	0000-0002-1825-0098
outside-format	0000-0002-1694-233x
outside-format	0000 0002 1825 0097
outside-format	0000-0002-1825-097
outside-format	https://example.org/0000-0002-1825-0097
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^(https://orcid[.]org/)?([0-9]{4})-([0-9]{4})-([0-9]{4})-([0-9]{3}[0-9X])$}) {
  my ($a, $b, $c, $d) = ($2, $3, $4, $5);
  my $id = join "-", $a, $b, $c, $d;
  my $base = $a . $b . $c . substr($d, 0, 3);
  my $given = substr($d, 3, 1);
  my $total = 0;
  for my $digit (split //, $base) { $total = 2 * ($total + $digit); }
  my $number = (12 - $total % 11) % 11;
  my $wanted = $number == 10 ? "X" : "$number";
  my $status = $given eq $wanted ? "checksum-ok" : "bad-checksum(expected=$wanted)";
  printf "%s\t%s\n", $status, $id;
} else {
  printf "outside-format\t%s\n", $input;
}
' data/orcid.txt

RGX-18-001 · Check decimal octets and leading zeros

Require each captured octet to be at most 255 and reject multi-digit octets beginning with zero. This checks the dotted-decimal address syntax used here, not routing or reserved-address policy.

Format regexp

^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$

Additional check result

address-syntax-ok	192.0.2.1
address-syntax-ok	198.51.100.42
address-syntax-ok	0.0.0.0
address-syntax-ok	255.255.255.255
octet-out-of-range	256.1.2.3
leading-zero	192.168.001.1
outside-format	1.2.3
outside-format	192.0.2.1:80
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})[.]([0-9]{1,3})$}) {
  my @octets = ($1, $2, $3, $4);
  my $status = "address-syntax-ok";
  if (grep { $_ > 255 } @octets) { $status = "octet-out-of-range"; }
  elsif (grep { length($_) > 1 && substr($_, 0, 1) eq "0" } @octets) { $status = "leading-zero"; }
  printf "%s\t%s\n", $status, $input;
} else {
  printf "outside-format\t%s\n", $input;
}
' data/ipv4.txt

RGX-19-001 · Select only client and server errors

Narrow status group 2 from [1-5][0-9]{2} to [45][0-9]{2}. The Perl check prints only captured 4xx/5xx records; successful 2xx requests disappear from this diagnostic view.

Changed regexp

^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([45][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$

Changed result

time=2026-11-01T09:15:01Z code=404 id=req-18
time=2026-11-01T09:15:02Z code=503 id=req-19
Changed command · Perl
perl -ne '
if (m{^([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z) status=([45][0-9]{2}) request=([A-Za-z0-9-]+) path=[^[:space:]]+$}) {
  printf "time=%s code=%s id=%s\n", $1, $2, $3;
}
' data/logs.txt

RGX-20-001 · Try unanchored replacement inside prose

Searching inside prose can find more candidates, but a global replacement can retain a sentence-final period and duplicate an existing resolver prefix. These are real counterexamples: extraction and field normalization need different contracts. Crossref discusses punctuation bycatch; a match does not establish registration.

Changed regexp

(10[.][0-9]{4,9}/[-A-Za-z0-9._;()/:]+)

Replacement

https://doi.org/$1

Changed result

doi:https://doi.org/10.1234/ALPHA.7
https://doi.org/10.56789/notes-2
doi: https://doi.org/10.2468/data_set(3)
doi:10.123/short
doi:10.1234/
See https://doi.org/10.1234/ALPHA.7.
https://doi.org/https://doi.org/10.56789/notes-2

RGX-21-001 · Allow versions with one to three digits

Relax only group 5 from two digits to one through three. The previously excluded v2 record now transforms. Keep the supplied spelling: accepting variable width does not silently zero-pad the version.

Changed regexp

^([A-Za-z][A-Za-z0-9-]*)_([0-9]{4})([0-9]{2})([0-9]{2})_v([0-9]{1,3})[.]([a-z]{3})$

Replacement

${2}-${3}-${4}_${1}_v${5}.${6}

Changed result

2026-11-01_atlas_v02.csv
2026-10-31_atlas_v12.tsv
2026-02-29_rna-seq_v01.csv
2026-11-01_atlas_v2.csv
my_project_20261101_v02.csv
notes.txt

RGX-22-001 · Add a second pass for repeated whole words

First apply the whitespace cleanup above, then collapse adjacent identical whole words with a pattern backreference. The repeated group consumes the whole go go go run. Matching remains case-sensitive, so The the survives. The two cleanup decisions remain separate; the complete script shows both passes.

Changed regexp

\b([A-Za-z]+)([[:blank:]]+\1)+\b

Replacement

$1

Changed result

Keep spacing readable.
Use one separator.
the result
go
The the result
line break stays
Changed command · Perl
perl -pe '
s{([A-Za-z]+)[[:blank:]]+}{$1 }g;
s{\b([A-Za-z]+)([[:blank:]]+\1)+\b}{$1}g;
' data/cleanup.txt

RGX-23-001 · Check month lengths and century leap years

After the format match, parse captures as decimal values. Require year >= 1, month 1–12 and day within that month. A leap year is divisible by 400, or divisible by 4 but not 100. Emit an ISO date only for valid dates. The 1900/2000 examples test the century rule explicitly.

Format regexp

^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$

Additional check result

calendar-ok	2026-11-01
calendar-ok	2024-02-29
invalid-date	29.02.2026
invalid-date	31.04.2026
invalid-date	29.02.1900
calendar-ok	2000-02-29
invalid-date	00.13.2026
outside-format	2026-11-01
Additional check command · Perl
perl -ne '
chomp;
my $input = $_;
if (m{^([0-9]{2})[.]([0-9]{2})[.]([0-9]{4})$}) {
  my ($day, $month, $year) = (0 + $1, 0 + $2, 0 + $3);
  my $leap = $year % 400 == 0 || ($year % 4 == 0 && $year % 100 != 0);
  my @days = (0, 31, 28 + $leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
  if ($year >= 1 && $month >= 1 && $month <= 12 && $day >= 1 && $day <= $days[$month]) {
    printf "calendar-ok\t%04d-%02d-%02d\n", $year, $month, $day;
  } else {
    printf "invalid-date\t%s\n", $input;
  }
} else {
  printf "outside-format\t%s\n", $input;
}
' data/dates.txt

RGX-24-001 · Try a global path-only rewrite

Remove the host and end constraints. The path-only pattern now rewrites other hosts, longer paths and even the nested /docs/ path inside the query value. Compare with the main result: host selection and tail preservation are part of the migration contract.

Changed regexp

/docs/([A-Za-z0-9-]+)/([0-9]{4})

Replacement

/guides/$2/$1

Changed result

https://old.example.org/guides/2026/regex
https://old.example.org/guides/2025/data-tools?view=all#top
https://old.example.org/guides/2026/regex?next=/guides/2024/awk
https://old.example.org.evil.test/guides/2026/regex
https://old.example.org/guides/2026/regex/extra
https://elsewhere.example.org/guides/2026/regex

Example and command index

Content: CC0 1.0. Structure and publishing framework: CC BY 4.0 by Lukas C. Bossert. Repository source · Publishing framework source · License scope and third-party notices.