Strip comments from a polyglot repo for a public mirror
Contents
This strips every comment from an exported code tree (Java, shell with embedded awk, Python, YAML, XML, Go, JS, properties, Dockerfiles), keeps only what a machine or a licence needs, and proves each changed file equivalent to its source with a per-language oracle, plus control tests that show each oracle can fail.
- Tested on
- Fedora Linux 44, kernel 7.2.10-200.fc44.x86_64; bash 5.3.9, Python 3.14.8, PyYAML 6.0.3, gawk 5.3.2, Go 1.26.0, Node v22.22.0, Temurin 25.0.4 with Maven 3.9.11 (2026-10-16), on 18 branches of one Java benchmark repo.
- Needs
- a git repo you will export from; the scripts attached to this page (
oss-strip-comments.py,test-oss-strip-comments.py,blind-oracle.py,naive.py,residual.py,strip-all.sh); the tools above onPATH; Maven able to resolve the build offline (-o) or online. - Time
- 30 min.
- Risk
- none to the source repo: every step works on a
git archivecopy. The stripper rewrites files in place, so run it only on that copy. Publishing (force-pushing to the mirror) is outside this manual.
Before you start#
Placeholders.
<SRC_REPO>: path to the repo you export from.<BRANCH>: one branch to try first (mine wasmicronaut/imperative-synth-more-io). All commands run from one empty working directory that holds the six attached scripts.Policy the script implements. Kept: the ASF Maven wrapper files untouched (
mvnw,mvnw.bat,mvnw.cmd,maven-wrapper.properties; Apache-2.0 §4(c) requires keeping their attribution notices), a shebang on line 1, Dockerfile parser directives (# syntax=,# escape=,# check=) at the top, Go//go:and// +buildlines. Removed: Java/JS/Go comments and Javadoc, XML comments, YAML and shell comments (whole-line and trailing), Python comments and docstrings, comments inside embedded awk programs (including programs spliced with'"$var"'), whole-line#,!and//lines in.propertiesand.conf, whole-line#comments in.gitignoreand.dockerignore.Files the stripper never opens.
.html,.txt,.json,.cmd,.mdand any suffix not in its map. A vendored licence header in an HTML report (my reports inline Plotly with its MIT header and a/*! For license information please see plotly.min.js.LICENSE.txt */pointer) survives for that reason, not because of an exception rule. If you ship a vendored.jsbundle, its header would be stripped: add it toLICENCE_EXEMPTfirst.Oracles per file type.
Type Oracle Strength .sh,.bashbash --pretty-printof source and result, blank lines dropped, must be equalfull parse awk inside shell gawk -o-(pretty-print) of each program before and after, comments dropped, must be equalfull parse, per program .pyast.dumpof source with docstrings removed equalsast.dumpof resultfull AST .yml,.yamlyaml.safe_load_allof both equaldata equality .xmlElementTree.canonicalize(with_comments=False, strip_text=True)of both equalcanonical form .gotoken stream from go/scanner(comments skipped) of both equaltoken stream .jsnode --checkof the resultsyntax only .java,.properties,.conf,.mod, Dockerfilenone covered by Step 8 for Java; the other four only by the unchanged-line net Every stripper first blanks comments in place, so line numbers stay aligned for the safety checks; a post-pass then drops the lines that held only a comment, so the final file is shorter (Step 6's hunk header shows the shift). A second net aborts if any line the stripper did not touch is missing from the output.
Check the tools.
bash --pretty-printis listed bybash --helpbut not documented inman bash; check that your build has it.$ bash --version | head -1; python3 -c 'import sys, yaml; print("Python", sys.version.split()[0], "PyYAML", yaml.__version__)'; gawk --version | head -1; go version; node --version; bash --help | command grep -- --pretty-print GNU bash, version 5.3.9(1)-release (x86_64-redhat-linux-gnu) Python 3.14.8 PyYAML 6.0.3 GNU Awk 5.3.2, API 4.0, PMA Avon 8-g1, (GNU MPFR 4.2.2, GNU MP 6.3.0) go version go1.26.0 linux/amd64 v22.22.0 --pretty-printExpected: all six lines. A missing
go,nodeorgawkdoes not fail the run: the affected files are stripped unchecked and reported asWARN: no oracle could check …(see Failure modes).Backup. Nothing in the source repo is written. Step 3 keeps a
pristine/copy of the export next to the strippedtree/.
Steps#
Put the scripts in one directory.
test-oss-strip-comments.pyloadsoss-strip-comments.pyfrom its own directory, so the two must sit side by side.Run the trap and control tests.
$ python3 -I test-oss-strip-comments.py -v test_awk (__main__.Controls.test_awk) ... ok test_go (__main__.Controls.test_go) ... ok test_py (__main__.Controls.test_py) ... ok test_sh (__main__.Controls.test_sh) ... ok test_xml (__main__.Controls.test_xml) ... ok test_yaml (__main__.Controls.test_yaml) ... ok test_awk_including_spliced (__main__.Traps.test_awk_including_spliced) ... ok test_dockerfile (__main__.Traps.test_dockerfile) ... ok test_go (__main__.Traps.test_go) ... ok test_java (__main__.Traps.test_java) ... ok test_licence_exempt (__main__.Traps.test_licence_exempt) ... ok test_properties (__main__.Traps.test_properties) ... ok test_python (__main__.Traps.test_python) ... ok test_sh (__main__.Traps.test_sh) ... ok test_xml (__main__.Traps.test_xml) ... ok test_yaml (__main__.Traps.test_yaml) ... ok ---------------------------------------------------------------------- Ran 16 tests in 1.062s OKExpected:
OK, 16 tests. TheTrapstests feed inputs where a comment marker is not a comment (${x#y},$#, heredoc bodies,"http://x", Java text blocks, YAML block scalars,<![CDATA[, Go raw strings,/*in a Dockerfilerm -rf …/lists/*). EachControlstest swaps in a deliberately wrong stripper (renamesecho, swaps an import, edits a quoted YAML value, empties a Go raw string, renames an XML element, changes4 / 2to4 / 3inside awk) and asserts that the oracle stops the run.Prove the controls can fail. A control that passes no matter what proves nothing.
blind-oracle.pyreplaces one oracle with one that always answers "same" and re-runs the controls:#!/usr/bin/env python3 """Control of the controls: blind one oracle (always says "same") and run the control tests. The control for that language must now FAIL. Usage: blind-oracle.py <test-oss-strip-comments.py> <ext>""" import importlib.util, sys, unittest spec = importlib.util.spec_from_file_location("t", sys.argv[1]) t = importlib.util.module_from_spec(spec); spec.loader.exec_module(t) ext = sys.argv[2] t.s.STRIPPERS[ext] = (t.s.STRIPPERS[ext][0], lambda src, new: True) suite = unittest.defaultTestLoader.loadTestsFromTestCase(t.Controls) sys.exit(not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful())$ python3 -I blind-oracle.py test-oss-strip-comments.py .sh test_awk (t.Controls.test_awk) ... ok test_go (t.Controls.test_go) ... ok test_py (t.Controls.test_py) ... ok test_sh (t.Controls.test_sh) ... FAIL test_xml (t.Controls.test_xml) ... ok test_yaml (t.Controls.test_yaml) ... ok ====================================================================== FAIL: test_sh (t.Controls.test_sh) ---------------------------------------------------------------------- Traceback (most recent call last): … AssertionError: SystemExit not raised ---------------------------------------------------------------------- Ran 6 tests in 0.181s FAILED (failures=1)Expected: exit status 1; exactly the control for the blinded language fails, the other five pass. Repeat with
.py,.yml,.go,.xml; each fails only its own control.test_awkstaysokwhen.shis blinded because awk programs have their own oracle.Export one branch into a throwaway tree and keep a pristine copy.
$ mkdir tree $ git -C <SRC_REPO> archive origin/<BRANCH> | tar -x -C tree $ cp -a tree pristine $ find tree -type f | wc -l 144Expected: a file count, no errors. The source repo is only read.
Strip.
$ time python3 -I oss-strip-comments.py tree comments stripped from 55 file(s) real 0m0.363s user 0m0.313s sys 0m0.135sExpected: one
comments stripped from N file(s)line and noWARN:orFATAL:line. AFATAL:line means an oracle or the unchanged-line net found a change in meaning. The run stops at that file; files before it are already rewritten, so export again before retrying (Failure modes, row 1).Read what changed. The awk program inside
lib.shlost its column notes; only comments went:$ diff -u pristine/scripts/bench/lib.sh tree/scripts/bench/lib.sh --- pristine/scripts/bench/lib.sh 2026-10-16 18:41:24.000000000 +0300 +++ tree/scripts/bench/lib.sh 2026-10-16 22:26:50.626119877 +0300 … @@ -132,11 +108,6 @@ log "Analyzing prometheus CSV..." awk -F',' ' BEGIN { - # columns: 1:ts 2:rss 3:heap_used 4:heap_committed 5:heap_max - # 6:nonheap_used 7:nonheap_committed 8:buf_mem 9:buf_count - # 10:gc_count 11:gc_total 12:cpu_usage 13:cpu_seconds 14:uptime - # 15:threads 16:threads_daemon 17:pool_active 18:pool_idle - # 19:pool_pending 20:pool_total 21:http_count 22:http_dur } NR>1 { n++ …On this branch the 55 changed files were 31
.java, 13.sh, 3.properties, 3.yml, 2 Dockerfiles, 1.go, 1.xml, 1.js. All 31 shebangs stayed on line 1. The branch has no Go directives and no Dockerfile parser directives, so those two keep-rules are exercised only by the Step 2 trap tests.Run a naive stripper through the same oracle. This is the control on real files: a plain regex stripper for shell comments, judged by the real
bash --pretty-printoracle.#!/usr/bin/env python3 """Control: replace one language's stripper with a naive regex and let the real oracle judge each file. Usage: naive.py <oss-strip-comments.py> <ext> <regex> <file>...""" import importlib.util, pathlib, re, sys spec = importlib.util.spec_from_file_location("strip", sys.argv[1]) s = importlib.util.module_from_spec(spec); spec.loader.exec_module(s) ext, rx = sys.argv[2], re.compile(sys.argv[3], re.M) s.STRIPPERS[ext] = (lambda t: rx.sub("", t), s.STRIPPERS[ext][1]) caught = 0 for f in map(pathlib.Path, sys.argv[4:]): try: s.strip_text(f, f.read_text()) print(f"passed {f}") except SystemExit as e: caught += 1 print(e) print(f"{caught} of {len(sys.argv) - 4} caught")$ python3 -I naive.py oss-strip-comments.py .sh '[ \t]#.*$|^[ \t]*#(?!!).*$' $(/usr/bin/find pristine -name '*.sh' | sort) | sed '/^passed/d' FATAL: pristine/scripts/bench/lib.sh changed meaning after stripping FATAL: pristine/scripts/prom-scraper.sh changed meaning after stripping FATAL: pristine/scripts/report-detailed.sh changed meaning after stripping FATAL: pristine/scripts/report-summary.sh changed meaning after stripping 4 of 30 caughtExpected: at least one
FATAL. Inreport-detailed.shandreport-summary.shthe regex cut CSS colours out of an HTML template (background: #0d1117; color: #c9d1d9; }becamebackground:). Those two are real breaks, and both still passbash -n, so a syntax check alone would have shipped them. Inlib.shandprom-scraper.shthe regex removed only comments:#lines inside single-quoted awk programs (plus one trailing shell comment inlib.sh). The shell oracle sees the awk program as part of a shell string, so it flags any edit there; those two are cautious catches of comment-only changes, which the real stripper handles with its own gawk oracle.Compile what has no oracle. Java has no equivalence oracle here; a full test-compile of the stripped tree is the check. Build the Go module too.
$ cp -a tree build $ (cd build && mvn -q -o -DskipTests test-compile > ../mvn.log 2>&1; echo "mvn exit=$?") mvn exit=0 $ (cd build/mock-server && go build -o /dev/null . ; echo "go exit=$?") go exit=0Expected: both
exit=0.test-compilecovers test sources too, which plaincompileskips. The copy keepstarget/out oftree/.Repeat for every branch you will publish.
strip-all.shexports, strips, test-compiles and builds each branch:#!/usr/bin/env bash # usage: strip-all.sh <SRC_REPO> <OUT_DIR> <STRIPPER> <BRANCH>... set -u src=$1 out=$2 stripper=$3; shift 3 rc=0 for b in "$@"; do d="$out/${b//\//_}" rm -rf "$d"; mkdir -p "$d" git -C "$src" archive "origin/$b" | tar -x -C "$d" s=$(python3 -I "$stripper" "$d" 2>&1) || { echo "$b STRIP FAILED: $s"; rc=1; continue; } ( cd "$d" && mvn -q -o -DskipTests test-compile > "$d.mvn.log" 2>&1 ) && m=OK || { m=FAIL; rc=1; } ( cd "$d/mock-server" && go build -o /dev/null . > "$d.go.log" 2>&1 ) && g=OK || { g=FAIL; rc=1; } echo "$b |${s# comments} | mvn=$m go=$g" done exit $rc$ time bash strip-all.sh <SRC_REPO> "$PWD/all" oss-strip-comments.py $(git -C <SRC_REPO> for-each-ref --format='%(refname:lstrip=3)' 'refs/remotes/origin/*/*synth*') micronaut/imperative-jdbc-synth | stripped from 55 file(s) | mvn=OK go=OK micronaut/imperative-synth | stripped from 52 file(s) | mvn=OK go=OK micronaut/imperative-synth-more-io | stripped from 55 file(s) | mvn=OK go=OK … springboot/reactive-r2dbc-synth | stripped from 49 file(s) | mvn=OK go=OK springboot/reactive-synth | stripped from 44 file(s) | mvn=OK go=OK springboot/reactive-synth-more-io | stripped from 46 file(s) | mvn=OK go=OK real 1m51.490s user 8m36.538s sys 0m27.609sExpected: one line per branch, every one
mvn=OK go=OK, noSTRIP FAILED, exit status 0. Themock-serverpath and the branch pattern are specific to my repo; change them for yours.
Verify#
Every changed file that has an oracle passed it (Step 5), the Java compiled (Steps 8 and 9), the licence files are byte-identical, and no comment-shaped line is left in the types the scan can judge without a parser:
$ for f in mvnw mvnw.cmd .mvn/wrapper/maven-wrapper.properties; do cmp pristine/$f tree/$f && echo "unchanged: $f"; done
unchanged: mvnw
unchanged: mvnw.cmd
unchanged: .mvn/wrapper/maven-wrapper.properties
$ python3 -I residual.py pristine | tail -1
93 files scanned, 513 comment-shaped lines
$ python3 -I residual.py tree
93 files scanned, 0 comment-shaped lines
$ python3 -I residual.py all
all/micronaut_imperative-synth/src/main/java/dev/tsvinc/space/service/ServiceConstants.java:41: * Math.sin(dLon / 2) * Math.sin(dLon / 2);
all/micronaut_reactive-synth/src/main/java/dev/tsvinc/space/service/ServiceConstants.java:41: * Math.sin(dLon / 2) * Math.sin(dLon / 2);
all/quarkus_imperative-synth/src/main/java/dev/tsvinc/space/service/ServiceConstants.java:41: * Math.sin(dLon / 2) * Math.sin(dLon / 2);
all/quarkus_reactive-synth/src/main/java/dev/tsvinc/space/service/ServiceConstants.java:41: * Math.sin(dLon / 2) * Math.sin(dLon / 2);
all/springboot_imperative-synth/src/main/java/dev/tsvinc/space/service/ServiceConstants.java:41: * Math.sin(dLon / 2) * Math.sin(dLon / 2);
all/springboot_reactive-synth/src/main/java/dev/tsvinc/space/service/ServiceConstants.java:41: * Math.sin(dLon / 2) * Math.sin(dLon / 2);
1451 files scanned, 6 comment-shaped lines
The six hits across all 18 branches are one line of code: the continuation of a multiplication (+ Math.cos(…) * Math.cos(…) on the line above, * Math.sin(dLon / 2) … on this one). residual.py only flags line starts (#, //, /*, *, <!--) in .java, .xml, .yml, .yaml, .properties, .go, .js, .py; shell is left out because case patterns (*)) and .gitignore globs match the same shapes.
Undo / rollback#
- Local tree: delete
tree/and export again, or copypristine/back. Verified: a secondgit archiveof the same branch intotree2/gave no output fromdiff -r pristine tree2. - Keep-everything export: my export wrapper takes
STRIP_COMMENTS=0and skips the stripper. On 2026-10-16 that path also stopped on the comment-validator error (FATAL: 1 comment(s) do not stand alone, Failure modes), so it was not a working rollback that day. - Published mirror: re-push the previous commit of each branch (the export prints
updated <old> -> <new>per branch).[UNVERIFIED: rollback not exercised]
Failure modes#
| Symptom (exact message) | Cause | Fix |
|---|---|---|
FATAL: <path> changed meaning after stripping | The oracle for that file type saw a difference: a real stripper bug, or a deliberately wrong stripper in a control. | Diff the oracle's view of both versions (for shell: bash --pretty-print of each) and fix the stripper; never bypass the oracle. |
FATAL: … changed meaning after stripping on a shell file whose only change is a removed comment block | bash --pretty-print renders a removed mid-file comment block as a blank line, so a strict comparison differs by one empty line. | Compare with blank lines dropped; the current _oracle_sh does this. |
WARN: no oracle could check <path>: spliced awk program at line <N> | An awk program built as '…'"$var"'…' does not parse standalone, so gawk cannot check it. | The current stripper joins the pieces with placeholder identifiers and checks the whole program; the WARN means an older stripper. |
UNUSABLE COMMENT pom.xml:21 cites a plan wave: <!-- Wave 2: … then FATAL: 1 comment(s) do not stand alone | My export runs a comment validator (meant for the keep-comments mode) before the stripper, so a comment the stripper would delete still fails the run. | Run the stripper before the validation pass. I made that reorder first in a private clone, where the same 19-branch dry run finished with exit 0. It is now committed (4fc38fe1, 2026-10-16): with the default strip, the stripper runs first. STRIP_COMMENTS=0 still validates and still rejects such a comment, which is intended, because in that mode the comment ships. |
Field notes#
- 2026-10-16 18:40–19:02 (Steps 4–9 and Verify re-run and saved 22:26–22:29; Steps 2, 3 and 7 pasted from the 18:42–19:02 runs), workstation (Fedora 44, kernel 7.2.10; bash 5.3.9, Python 3.14.8, PyYAML 6.0.3, gawk 5.3.2, Go 1.26.0, Node v22.22.0, Temurin 25.0.4, Maven 3.9.11):
- Scripts from the repo's
mainat 9e3a2f6. Test suite: 16 tests, OK. - Blinded each oracle in turn (
.sh,.py,.yml,.go,.xml, and awk by making the gawk normaliser return a constant). Every time, only the matching control failed. - The JS oracle is weaker than the rest. A syntax break (
1 - 2to1 - ;) is caught, but a meaning change (1 - 2to1 + 2) passes. There is no JS control test, and none would make sense againstnode --check. - One branch (144 files, at 9acf917c) stripped 55 files in under a second. Wrapper files were byte-identical afterwards. It test-compiled offline in 11.5 s.
- The naive shell regex was caught on 4 of 30 scripts. Two were real breaks (CSS colours cut from HTML templates) that
bash -naccepts; the other two were comment-only edits inside awk programs, flagged cautiously because the shell oracle compares the awk text as a shell string. - The full dry-run export, as committed, stopped on the second branch: a
<!-- Wave 2: … -->comment in that branch'spom.xmlfailed the keep-comments validator, which runs before the stripper.STRIP_COMMENTS=0stopped on the same comment. The reorder below went into the export later that evening as4fc38fe1. - In a private clone I moved the repairs/validation block after the strip block. The dry run then passed all 19 branches in 29.5 s.
maincame out with the same tree as the published commit, and the other 18 came out as updates. - Test-compile plus Go build of all 18 stripped branches: 18 of 18 OK, from both the dry-run mirror and
strip-all.sh. - Across the 18 branches the wrapper files were byte-identical: 54 of 54.
- Stripping
main's ownscripts/stripped the stripper. Its stripped test suite still passed against the stripped stripper. - The public mirror, cloned read-only, had 19 branches.
mainstill carried the Plotly 2.35.2 MIT header, the/*! For license information … */pointer andplotly.min.js.LICENSE.txt. That file is byte-identical toplotly.js@2.35.2/dist/plotly.min.js.LICENSE.txton jsDelivr;plotly.js-dist-min@2.35.2ships no such file. residual.pyover all 19 mirror branches (1452 files) reported 10 comment-shaped lines and no comments: 6 are the Java continuation line, 4 are lines inside Python string literals in the report generator (*{box-sizing:border-box;}and three##Markdown headings). It skips line-1 shebangs.
- Scripts from the repo's
- 2026-10-16 (earlier the same day), same workstation, JDK 25.0.2 GraalVM CE for the compile check:
- The first strip-mode dry runs stopped twice.
- First a shell false positive on a bench scenario script: the pretty-printer had turned a removed comment block into one blank line. The oracle now drops blank lines.
- Then a Go FATAL on the mock server's
main.go. The saved before/after pair passes the 16 October oracle, and the output of the 16 October stripper matches the saved "after" file. What the earlier check compared was not kept.
- Another dry run carried 36
WARNlines, two spliced awk programs on each of the 18 branches. That stripper had no splice support yet. - After the fixes, dry runs were clean, and all 18 stripped branches test-compiled and built.
- Pushed all 19 branches as orphan commits. A second push added the Plotly notice file to
mainand left the 18 others unchanged. - The mirror still has wrapper 3.1.1 (
mvnw.bat). The source branches have since moved to wrapper 3.3.4:only-script,mvnw.cmd, and amaven-wrapper.propertieswith no licence header. A 3.3.4 export was not pushed by the time of this run.
- The first strip-mode dry runs stopped twice.
Notes#
- The script's docstring records a break that the unchanged-line net alone missed: a Dockerfile named
Dockerfile.javawent to the Java scanner, and the/*inrm -rf /var/lib/apt/lists/*opened a block comment that blanked ten lines. Blanked lines count as changed, so the net did not fire. The fix is classifying by what the file is (Dockerfile*is checked before the suffix map). Dockerfiles still have no oracle. - Why exempt the wrapper files by name:
mvnwhas no suffix and.cmdis not in the map, so neither would be touched anyway.maven-wrapper.propertiesis stripped as.properties, and in wrapper 3.1.1 it carries the ASF header. The name list covers all of them and does not depend on the map. - Java equivalence beyond compiling (comparing
javactoken streams, say) is a possible stronger oracle. Not tried. - The attached
oss-strip-comments.pyis byte-identical to the copy in my repo; its docstring mentionsexport-oss-mirror.shand a plan rowD5.10, both of which live in my private repo and are not published. STRIP_COMMENTS=0and the repair/validation pass belong to my export wrapper, which also deletes Markdown, addsLICENSE, and makes one orphan commit per branch. They are not part of the attached scripts.- The public mirror these runs fed: https://gitlab.com/myrants-oss/micronaut-native-as-hell
Filed under: git, testing, bash, python, awk
Sources#
- Apache License 2.0, §4(c), on retaining attribution notices in redistributed source: https://www.apache.org/licenses/LICENSE-2.0.txt
- gawk options,
-o/--pretty-print: https://www.gnu.org/software/gawk/manual/html_node/Options.html - Go
go/scanner(Mode;ScanCommentsunset skips comments): https://pkg.go.dev/go/scanner - Python
ast: https://docs.python.org/3/library/ast.html - Python
xml.etree.ElementTree.canonicalize: https://docs.python.org/3/library/xml.etree.elementtree.html - Node.js CLI (
-c, --check): https://nodejs.org/api/cli.html - Maven Wrapper (default
only-scripttype,mvnw/mvnw.cmd): https://maven.apache.org/tools/wrapper/ - Plotly 2.35.2 notice file: https://cdn.jsdelivr.net/npm/plotly.js@2.35.2/dist/plotly.min.js.LICENSE.txt