Code Library
CNC ProgrammingPythonPython 3.8+ · standard library onlyMIT licenseStarter

Clean and renumber a G-code program (Python)

A program that's been hand-edited a dozen times ends up with blank lines, stale N-numbers from three revisions ago, and coordinates that mix X40 with X40.000 in the same file. This script fixes all three in one pass: strips blank lines, renumbers every N-block from a clean starting point, and normalizes X/Y/Z/I/J/K/R words to always carry a decimal point — the same formatting discipline the bolt-circle generator applies to its own output, run backwards over a file someone else wrote.

Before you run it

  • Python 3.8+ (standard library only)
  • A G-code program to clean — try the demo above with your own pasted program

The code

GitHub

Run it here — no install

Loads a real Python interpreter in your browser (Pyodide, one-time ~10 MB download) and calls clean_lines() from the code above. Paste your own program or try the messy sample below.

"""Clean and renumber a G-code program: strip blank lines, renumber N-blocks,
and normalize decimal formatting on X/Y/Z/I/J/K/R words.

Usage:  python gcode_clean.py input.nc --out cleaned.nc
        python gcode_clean.py input.nc --start 10 --step 10
"""

import argparse
import re

WORD_RE = re.compile(r'([A-Z])(-?\d+\.?\d*)')


def clean_lines(lines, start=10, step=10, strip_comments=False):
    """Renumber N-blocks and normalize decimals. Returns the cleaned lines."""
    out = []
    n = start
    for raw in lines:
        line = raw.strip()
        if not line:
            continue
        if strip_comments and (line.startswith("(") or line.startswith(";")):
            continue

        # Drop any existing N-word so renumbering never collides with it
        line = re.sub(r'^N\d+\s*', '', line)

        def normalize(match):
            letter, value = match.groups()
            if letter in "XYZIJKR" and "." not in value:
                value += "."
            return f"{letter}{value}"

        line = WORD_RE.sub(normalize, line)

        if line.startswith("(") or line.startswith(";") or line.startswith("O"):
            out.append(line)  # comments/program numbers keep their own line, no N-word
        else:
            out.append(f"N{n} {line}")
            n += step
    return out


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("file", help="G-code program to clean")
    ap.add_argument("--out", help="output file (default: print to stdout)")
    ap.add_argument("--start", type=int, default=10, help="first N-number")
    ap.add_argument("--step", type=int, default=10, help="N-number increment")
    ap.add_argument("--strip-comments", action="store_true",
                     help="drop comment-only lines instead of keeping them")
    args = ap.parse_args()

    with open(args.file) as fh:
        lines = fh.readlines()

    cleaned = clean_lines(lines, args.start, args.step, args.strip_comments)
    result = "\n".join(cleaned) + "\n"

    if args.out:
        with open(args.out, "w") as fh:
            fh.write(result)
        print(f"{len(cleaned)} lines written to {args.out}")
    else:
        print(result, end="")


if __name__ == "__main__":
    main()

What you get

$ python gcode_clean.py messy.nc
O1001 (BOLT CIRCLE - MESSY EXPORT)
N10 G90 G94 G17 G21
N20 G54
N30 T1 M6
N40 S2400 M3
N50 G43 H01 Z25.
N60 G98 G81 X40. Y0. Z-12. R2. F120
N70 X28.284 Y28.284
(RETRACT)
N80 G80
N90 G0 Z25.
N100 M5
N110 M30

How it works

  • Any existing N-word is stripped from the front of the line before renumbering — so a file with stale N5/N999 numbers from a previous edit gets clean sequential numbers, not a second N-word appended after the old one.L25–26
  • normalize() only touches X/Y/Z/I/J/K/R words, and only when they're missing a decimal point — X40 becomes X40., but S2400 and F120 are left alone, because a bare integer feed rate or RPM means exactly what it says.L28–32
  • Comments and the O-number line keep their place in the file but never get an N-word of their own — matching how real programs are formatted, where the header and inline notes aren't numbered blocks.L36–40
  • --strip-comments is opt-in, not the default — deleting comments destroys documentation a machinist may need; renumbering and reformatting are the safe defaults, dropping notes is a deliberate choice.

Gotchas & honest limits

  • This is a line-oriented text cleaner, not a G-code parser — it will also "normalize" a letter-number pair that happens to appear inside a parenthetical comment (harmless, since comments aren't executed, but worth knowing).
  • Renumbering breaks any internal N-word references (rare in a straight linear post, more common in hand-edited macros with jumps) — check for GOTO-style references before renumbering a macro-heavy file.
  • Decimal normalization doesn't know or care whether the program is inches or mm — it just standardizes formatting either way.
  • Reads the whole file into memory — fine for anything a person would hand-edit; not the tool for a multi-gigabyte five-axis surface job.

Goes deeper

Want this adapted to your shop — or built into a real tool?

Samples are the free 80%. The last 20% is the part I do for a living.

Get in touch
Home
Blog
Tools
Code
Email
LinkedIn
Résumé