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
