Manual

Strip comments from a polyglot repo for a public mirror

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 on PATH; 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 archive copy. 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 was micronaut/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 // +build lines. 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 .properties and .conf, whole-line # comments in .gitignore and .dockerignore.

  • Files the stripper never opens. .html, .txt, .json, .cmd, .md and 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 .js bundle, its header would be stripped: add it to LICENCE_EXEMPT first.

  • Oracles per file type.

    TypeOracleStrength
    .sh, .bashbash --pretty-print of source and result, blank lines dropped, must be equalfull parse
    awk inside shellgawk -o- (pretty-print) of each program before and after, comments dropped, must be equalfull parse, per program
    .pyast.dump of source with docstrings removed equals ast.dump of resultfull AST
    .yml, .yamlyaml.safe_load_all of 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 --check of the resultsyntax only
    .java, .properties, .conf, .mod, Dockerfilenonecovered 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-print is listed by bash --help but not documented in man 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-print
    

    Expected: all six lines. A missing go, node or gawk does not fail the run: the affected files are stripped unchecked and reported as WARN: 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 stripped tree/.

Steps#

  1. Put the scripts in one directory. test-oss-strip-comments.py loads oss-strip-comments.py from its own directory, so the two must sit side by side.

  2. 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
    
    OK
    

    Expected: OK, 16 tests. The Traps tests 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 Dockerfile rm -rf …/lists/*). Each Controls test swaps in a deliberately wrong stripper (renames echo, swaps an import, edits a quoted YAML value, empties a Go raw string, renames an XML element, changes 4 / 2 to 4 / 3 inside awk) and asserts that the oracle stops the run.

  3. Prove the controls can fail. A control that passes no matter what proves nothing. blind-oracle.py replaces 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_awk stays ok when .sh is blinded because awk programs have their own oracle.

  4. 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
    144
    

    Expected: a file count, no errors. The source repo is only read.

  5. Strip.

    $ time python3 -I oss-strip-comments.py tree
      comments stripped from 55 file(s)
    
    real	0m0.363s
    user	0m0.313s
    sys	0m0.135s
    

    Expected: one comments stripped from N file(s) line and no WARN: or FATAL: line. A FATAL: 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).

  6. Read what changed. The awk program inside lib.sh lost 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.

  7. 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-print oracle.

    #!/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 caught
    

    Expected: at least one FATAL. In report-detailed.sh and report-summary.sh the regex cut CSS colours out of an HTML template (background: #0d1117; color: #c9d1d9; } became background:). Those two are real breaks, and both still pass bash -n, so a syntax check alone would have shipped them. In lib.sh and prom-scraper.sh the regex removed only comments: # lines inside single-quoted awk programs (plus one trailing shell comment in lib.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.

  8. 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=0
    

    Expected: both exit=0. test-compile covers test sources too, which plain compile skips. The copy keeps target/ out of tree/.

  9. Repeat for every branch you will publish. strip-all.sh exports, 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.609s
    

    Expected: one line per branch, every one mvn=OK go=OK, no STRIP FAILED, exit status 0. The mock-server path 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 copy pristine/ back. Verified: a second git archive of the same branch into tree2/ gave no output from diff -r pristine tree2.
  • Keep-everything export: my export wrapper takes STRIP_COMMENTS=0 and 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)CauseFix
FATAL: <path> changed meaning after strippingThe 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 blockbash --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 aloneMy 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 main at 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 - 2 to 1 - ;) is caught, but a meaning change (1 - 2 to 1 + 2) passes. There is no JS control test, and none would make sense against node --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 -n accepts; 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's pom.xml failed the keep-comments validator, which runs before the stripper. STRIP_COMMENTS=0 stopped on the same comment. The reorder below went into the export later that evening as 4fc38fe1.
    • 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. main came 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 own scripts/ stripped the stripper. Its stripped test suite still passed against the stripped stripper.
    • The public mirror, cloned read-only, had 19 branches. main still carried the Plotly 2.35.2 MIT header, the /*! For license information … */ pointer and plotly.min.js.LICENSE.txt. That file is byte-identical to plotly.js@2.35.2/dist/plotly.min.js.LICENSE.txt on jsDelivr; plotly.js-dist-min@2.35.2 ships no such file.
    • residual.py over 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.
  • 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 WARN lines, 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 main and 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 a maven-wrapper.properties with no licence header. A 3.3.4 export was not pushed by the time of this run.

Notes#

  • The script's docstring records a break that the unchanged-line net alone missed: a Dockerfile named Dockerfile.java went to the Java scanner, and the /* in rm -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: mvnw has no suffix and .cmd is not in the map, so neither would be touched anyway. maven-wrapper.properties is 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 javac token streams, say) is a possible stronger oracle. Not tried.
  • The attached oss-strip-comments.py is byte-identical to the copy in my repo; its docstring mentions export-oss-mirror.sh and a plan row D5.10, both of which live in my private repo and are not published.
  • STRIP_COMMENTS=0 and the repair/validation pass belong to my export wrapper, which also deletes Markdown, adds LICENSE, 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#