Authentic GW-BASIC screen editor with 25x80 buffer, free cursor movement, enter-on-any-line, F1-F10 function keys, Insert/Overwrite toggle, KEY ON/OFF/LIST statement, and Ctrl+Break handling. HAL pointer swap routes all PRINT/LIST/error output through the TUI automatically. Piped mode unchanged (50/50 tests pass). Adds automated compatibility testing infrastructure: DOSBox-X headless config, PRINT-to-file transform script, and run_compat.sh with --generate and --compare modes for verifying output against real GWBASIC.EXE. Project renamed from gwbasic-c to GW-BASIC 2026.
104 lines
3.1 KiB
Python
104 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Transform a .bas file so PRINT output goes to a file instead of screen.
|
|
|
|
Real GWBASIC.EXE uses BIOS INT 10h for PRINT, so DOS > redirection won't
|
|
capture it. This script rewrites PRINT to PRINT #9, with file #9 opened
|
|
for output, so the results land in a file we can diff against.
|
|
|
|
Usage: transform_for_capture.py input.bas output.bas outfile.txt
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
|
|
|
|
def transform(lines, outfile):
|
|
result = []
|
|
# Add file open as the very first line (line 0)
|
|
result.append(f'0 OPEN "{outfile}" FOR OUTPUT AS #9\n')
|
|
|
|
for line in lines:
|
|
line = line.rstrip('\r\n')
|
|
if not line.strip():
|
|
continue
|
|
|
|
# Parse line number
|
|
m = re.match(r'^(\s*\d+)(.*)', line)
|
|
if not m:
|
|
result.append(line + '\n')
|
|
continue
|
|
|
|
linenum = m.group(1)
|
|
rest = m.group(2)
|
|
|
|
# Replace PRINT with PRINT #9, (but not PRINT# which is file I/O)
|
|
# Handle multi-statement lines with :
|
|
rest = transform_statements(rest)
|
|
|
|
result.append(linenum + rest + '\n')
|
|
|
|
# Add cleanup: close file and exit
|
|
result.append('63999 CLOSE #9:SYSTEM\n')
|
|
return result
|
|
|
|
|
|
def transform_statements(text):
|
|
"""Transform PRINT statements in a line to PRINT #9,"""
|
|
parts = split_statements(text)
|
|
out = []
|
|
for part in parts:
|
|
stripped = part.lstrip()
|
|
upper = stripped.upper()
|
|
if upper.startswith('PRINT') and not upper.startswith('PRINT#') and not upper.startswith('PRINT #'):
|
|
# Check it's not PRINT USING with a file
|
|
after = stripped[5:]
|
|
# Replace PRINT with PRINT #9,
|
|
if after and after[0] not in (' ', '\t', ';', ',', '"'):
|
|
# PRINT immediately followed by letter - might be variable like PRINTER
|
|
out.append(part)
|
|
else:
|
|
indent = part[:len(part) - len(stripped)]
|
|
out.append(indent + 'PRINT #9,' + after)
|
|
elif upper.startswith('END'):
|
|
# Replace END with CLOSE #9:SYSTEM
|
|
out.append(' CLOSE #9:SYSTEM')
|
|
elif upper.startswith('STOP'):
|
|
out.append(' CLOSE #9:SYSTEM')
|
|
elif upper.startswith('SYSTEM'):
|
|
out.append(' CLOSE #9:SYSTEM')
|
|
else:
|
|
out.append(part)
|
|
return ':'.join(out)
|
|
|
|
|
|
def split_statements(text):
|
|
"""Split a BASIC line on : but not inside strings."""
|
|
parts = []
|
|
current = []
|
|
in_string = False
|
|
for ch in text:
|
|
if ch == '"':
|
|
in_string = not in_string
|
|
current.append(ch)
|
|
elif ch == ':' and not in_string:
|
|
parts.append(''.join(current))
|
|
current = []
|
|
else:
|
|
current.append(ch)
|
|
parts.append(''.join(current))
|
|
return parts
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) != 4:
|
|
print(f'Usage: {sys.argv[0]} input.bas output.bas outfile.txt', file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
with open(sys.argv[1]) as f:
|
|
lines = f.readlines()
|
|
|
|
result = transform(lines, sys.argv[3])
|
|
|
|
with open(sys.argv[2], 'w') as f:
|
|
f.writelines(result)
|