Skip to content

Instantly share code, notes, and snippets.

@Frank-Buss
Created October 11, 2025 19:20
Show Gist options
  • Save Frank-Buss/167694087d29b3d14e832edd4674c3a5 to your computer and use it in GitHub Desktop.
Save Frank-Buss/167694087d29b3d14e832edd4674c3a5 to your computer and use it in GitHub Desktop.
Converts commas in a gcode nc file to dots for the decimal separator
#!/usr/bin/env python3
"""
Proper G-code parser that fixes comma decimal separators.
G-code structure:
- Words: Letter followed by a number (e.g., G0, X123.456, Y-78.9)
- Comments: Text in parentheses ()
- Lines can have multiple words, with or without spaces
"""
import re
import sys
from pathlib import Path
class GCodeParser:
"""Parser for G-code files."""
def __init__(self):
# Pattern for G-code words: Letter followed by optional +/- and number
# Number can have comma or dot as decimal separator
self.word_pattern = re.compile(r'([A-Z])([+-]?[\d]+[,\.]?[\d]*)')
def parse_line(self, line):
"""
Parse a G-code line into components.
Returns: (commands, comment, original_structure)
- commands: list of (letter, number_string) tuples
- comment: comment string including parentheses, or None
- original_structure: to preserve spacing
"""
# Extract comment if present
comment_match = re.search(r'\([^)]*\)', line)
if comment_match:
comment = comment_match.group()
comment_start = comment_match.start()
comment_end = comment_match.end()
# Get the code before and after comment
code_part = line[:comment_start] + line[comment_end:]
else:
comment = None
code_part = line
# Parse the code part into words
commands = []
last_end = 0
for match in self.word_pattern.finditer(code_part):
letter = match.group(1)
number = match.group(2)
# Store the prefix (spaces, etc.) before this word
prefix = code_part[last_end:match.start()]
commands.append((prefix, letter, number))
last_end = match.end()
# Get any trailing characters
trailing = code_part[last_end:]
return commands, comment, trailing
def fix_line(self, line):
"""Fix decimal separators in a G-code line."""
# Handle empty lines
if not line.strip():
return line
commands, comment, trailing = self.parse_line(line)
# Reconstruct the line with fixed decimals
result = ""
# Add commands with fixed numbers
for prefix, letter, number in commands:
result += prefix
result += letter
# Replace comma with dot in number
fixed_number = number.replace(',', '.')
result += fixed_number
result += trailing
# Add comment back if it exists (preserve as-is, don't modify comments)
if comment:
result += comment
return result
def fix_file(self, content):
"""Fix decimal separators in entire G-code file."""
lines = content.split('\n')
fixed_lines = [self.fix_line(line) for line in lines]
return '\n'.join(fixed_lines)
def validate_line(self, line):
"""
Validate that a G-code line looks correct after conversion.
Returns (is_valid, message)
"""
# Skip empty lines and pure comments
stripped = line.strip()
if not stripped or (stripped.startswith('(') and stripped.endswith(')')):
return True, ""
# Check for common errors
if '..' in line:
return False, "Multiple consecutive dots"
# Check for letter followed immediately by dot (should have digit first)
if re.search(r'[A-Z]\.(?!\d)', line):
# Allow cases like "X.5" which is valid G-code
if not re.search(r'[A-Z]\.[0-9]', line):
return False, "Letter followed by dot without valid number"
# Check for numbers that still have commas (should all be converted)
if re.search(r'\d,\d', line):
return False, "Comma still present between digits"
return True, ""
def main():
if len(sys.argv) < 2:
print("Usage: gcode_parser.py <input_file> [output_file]")
print("Properly parses and fixes comma decimal separators in G-code")
sys.exit(1)
input_file = Path(sys.argv[1])
if not input_file.exists():
print(f"Error: File '{input_file}' not found")
sys.exit(1)
# Determine output file
if len(sys.argv) >= 3:
output_file = Path(sys.argv[2])
else:
output_file = input_file.with_suffix(input_file.suffix + '.fixed')
# Read input
print(f"Reading: {input_file}")
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
# Parse and fix
parser = GCodeParser()
print("Parsing and fixing G-code...")
fixed_content = parser.fix_file(content)
# Validate
print("Validating output...")
errors = []
for line_num, line in enumerate(fixed_content.split('\n'), 1):
is_valid, message = parser.validate_line(line)
if not is_valid:
errors.append(f"Line {line_num}: {message} - {line[:60]}")
if errors:
print("\nERROR: Validation failed!")
for error in errors[:5]:
print(f" {error}")
if len(errors) > 5:
print(f" ... and {len(errors) - 5} more errors")
print("\nNOT writing output file due to errors.")
sys.exit(1)
# Count changes
orig_commas = sum(1 for line in content.split('\n')
for match in re.finditer(r'\d,\d', line))
fixed_commas = sum(1 for line in fixed_content.split('\n')
for match in re.finditer(r'\d,\d', line))
changes = orig_commas - fixed_commas
# Write output
print(f"Writing: {output_file}")
with open(output_file, 'w', encoding='utf-8') as f:
f.write(fixed_content)
print(f"\n✓ Fixed {changes} decimal separators")
print("✓ Validation passed - file is safe to use")
# Show examples
print("\nExample conversions:")
orig_lines = content.split('\n')
fixed_lines = fixed_content.split('\n')
shown = 0
for i, (orig, fixed) in enumerate(zip(orig_lines, fixed_lines), 1):
if orig != fixed and shown < 5:
print(f" Line {i}:")
print(f" Before: {orig}")
print(f" After: {fixed}")
shown += 1
print("\nDone!")
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment