McRogueFace/src/CommandLineParser.cpp
John McCardle 43a6cc376b fix(engine): UTF-8 filesystem encoding, --run-forever in --help, honest step() docs
Three documentation-adjacent defects, found while auditing what the July bugfix
batch obliged the docs to say.

#378 -- init_python_with_config(), the init path main.cpp actually uses, did no
pre-initialization at all. PyPreConfig.utf8_mode was never enabled, so the
filesystem encoding fell back to ASCII while sys.getdefaultencoding() and the
locale both reported UTF-8. open() with no explicit encoding= therefore could not
read a UTF-8 file:

    open("notes.py").read()
    # UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2

In any normal CPython 3, open() defaults to UTF-8. This broke any script reading a
data file, a save file, or its own source. init_python() -- the other init path --
had always set utf8_mode = 1; only the live path was missing it.

This also corrects the long-standing folklore that "--exec scripts must be
ASCII-only". They need not be, and never did: the C++ side reads the file and hands
the bytes to Python, which parses them as UTF-8 per PEP 3120. The folklore was
pointing at open(), via harnesses that read scripts themselves.

--run-forever (#350) parsed but was absent from print_help(), which lists every
other McRogueFace flag. The only place a user could learn it existed was the
runtime error printed after their script had already failed. CLI flags are not in
the API manifest, so nothing flagged the omission.

mcrfpy.step()'s docstring still read "Advance simulation time" -- accurate until
30abb0b made step() a full simulation frame (scene update, Python Scene.update(),
timers, animations, transition completion, frame metrics, currentFrame++). It now
says so, says why render and input are deliberately excluded, and states the
sys.exit() requirement that --run-forever exists to waive. The most-changed
semantic in the batch had the least-changed documentation.

closes #378
2026-07-14 18:21:19 -04:00

203 lines
No EOL
7 KiB
C++

#include "CommandLineParser.h"
#include <iostream>
#include <filesystem>
#include <algorithm>
CommandLineParser::CommandLineParser(int argc, char* argv[])
: argc(argc), argv(argv) {}
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
ParseResult result;
current_arg = 1; // Reset for each parse
// Detect if running as Python interpreter
#ifndef __EMSCRIPTEN__
std::filesystem::path exec_name = std::filesystem::path(argv[0]).filename();
if (exec_name.string().find("python") == 0) {
config.headless = true;
config.python_mode = true;
}
#endif
while (current_arg < argc) {
std::string arg = argv[current_arg];
// Handle Python-style single-letter flags
if (arg == "-h" || arg == "--help") {
print_help();
result.should_exit = true;
result.exit_code = 0;
return result;
}
if (arg == "-V" || arg == "--version") {
print_version();
result.should_exit = true;
result.exit_code = 0;
return result;
}
// Python execution modes
if (arg == "-c") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -c option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_command = argv[current_arg];
current_arg++;
continue;
}
if (arg == "-m") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -m option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_module = argv[current_arg];
current_arg++;
// Collect remaining args as module args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
continue;
}
if (arg == "-i") {
config.interactive_mode = true;
config.python_mode = true;
current_arg++;
continue;
}
// McRogueFace specific flags
if (arg == "--headless") {
config.headless = true;
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--run-forever") {
config.run_forever = true;
current_arg++;
continue;
}
if (arg == "--audio-off") {
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--audio-on") {
config.audio_enabled = true;
current_arg++;
continue;
}
if (arg == "--screenshot") {
config.take_screenshot = true;
current_arg++;
if (current_arg < argc && argv[current_arg][0] != '-') {
config.screenshot_path = argv[current_arg];
current_arg++;
} else {
config.screenshot_path = "screenshot.png";
}
continue;
}
if (arg == "--exec") {
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the --exec option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.exec_scripts.push_back(argv[current_arg]);
config.python_mode = true;
current_arg++;
// Look for `--` passthrough marker: everything after it becomes script_args
// (forwarded to sys.argv so fuzzers/libFuzzer can receive their flags).
int scan = current_arg;
while (scan < argc) {
if (std::string(argv[scan]) == "--") {
for (int i = scan + 1; i < argc; i++) {
config.script_args.push_back(argv[i]);
}
current_arg = argc;
break;
}
scan++;
}
continue;
}
if (arg == "--continue-after-exceptions") {
config.exit_on_exception = false;
current_arg++;
continue;
}
// If no flags matched, treat as positional argument (script name)
if (arg[0] != '-') {
config.script_path = arg;
config.python_mode = true;
current_arg++;
// Remaining args are script args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
break;
}
// Unknown flag
std::cerr << "Unknown option: " << arg << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
return result;
}
void CommandLineParser::print_help() {
std::cout << "usage: mcrogueface [option] ... [-c cmd | -m mod | file | -] [arg] ...\n"
<< "Options:\n"
<< " -c cmd : program passed in as string (terminates option list)\n"
<< " -h : print this help message and exit (also --help)\n"
<< " -i : inspect interactively after running script\n"
<< " -m mod : run library module as a script (terminates option list)\n"
<< " -V : print the Python version number and exit (also --version)\n"
<< "\n"
<< "McRogueFace specific options:\n"
<< " --exec file : execute script before main program (can be used multiple times)\n"
<< " --headless : run without creating a window (implies --audio-off)\n"
<< " --run-forever : keep running after --exec scripts finish, instead of\n"
<< " exiting (for a long-lived headless process)\n"
<< " --audio-off : disable audio\n"
<< " --audio-on : enable audio (even in headless mode)\n"
<< " --screenshot [path] : take a screenshot in headless mode\n"
<< " --continue-after-exceptions : don't exit on Python callback exceptions\n"
<< " (default: exit on first exception)\n"
<< "\n"
<< "Arguments:\n"
<< " file : program read from script file\n"
<< " - : program read from stdin\n"
<< " arg ...: arguments passed to program in sys.argv[1:]\n";
}
void CommandLineParser::print_version() {
std::cout << "Python 3.14.0 (McRogueFace embedded)\n";
}