Skip to content

Command-line processing and "main program" #1277

@ghost

Description

The top-level processing of command-line arguments and start/run/terminate control flow in FontForge is currently several plates of spaghetti spread throughout the system:

  • main.c:main() - trivial wrapper that passes argc and argv to fontforge_main(), which has two different definitions in two different files selected by conditionals at the Makefile level.
  • startnoui.c:fontforge_main() - does some initialization, prints a banner, passes argc and argv to CheckIsScript which may or may not return, then if it returns, parses the command line, then if Python is not enabled passes argc and argv to ProcessNativeScript(), else if Python is enabled (but even if native is also enabled) passes to PyFF_Stdin(), then if that returns, does cleanup and exits.
  • startui.c:fontforge_main() - does some command-line parsing first, without the initialization done by the other version of this function. Then runs a bunch of Macintosh- and Windows-specific code, depending on the platform. Then runs CheckIsScript(), passing it argc and argv; it may or may not return. Then (if it returns) parses the command line again, differently. Then does Python initialization (which would NOT have been done in the other version). Shows the splash screen, runs the GUI loop, does collab stuff. Does cleanup and exits.
  • scripting.c:CheckIsScript() - trivial wrapper that passes argc and argv to _CheckIsScript()
  • scripting.c:_CheckIsScript() - parses the command line. Also attempts to determine whether it was executed via a shebang, using hardcoded assumptions about the name of the FontForge executable. Passes argc and argv to ProcessNativeScript() or to PyFF_Main(), or invokes PyFF_Stdin() without argc and argv, or just returns, depending on complicated conditions and #ifdefs.
  • scripting.c:ProcessNativeScript() - parses the command line. Obtains native-language script code from a location determined by the command-line parse, runs it, does not do cleanup, and terminates.
  • python.c:PyFF_Main() - does some Python-specific initialization. Parses the command line. Creates a new, filtered version of argc and argv, passes those to Py_Main(), and then terminates.
  • python.c:PyFF_Stdin() - does Python-specific initialization. Then depending on whether standard input is a TTY, invokes either PyRun_InteractiveLoop() or PyRun_SimpleFile(), and terminates.
  • Py_Main() - I cannot find this function in FontForge. I guess it's actually part of Python. It seems to parse the command line again, because it takes (the filtered) argc and argv as input. I imagine it performs a similar function to PyFF_Stdin, except using a script determined by the command-line parsing instead of coming from stdin.

The overall picture is that it's basically impossible to guess where, and whether, initialization will be performed; some initialization is probably skipped on some code paths. It's impossible to guess where, and whether, cleanup will be performed; some cleanup is probably skipped on some code paths. It's impossible to guess exactly what command line argument processing will take place: the set of options recognized or not depends in a complicated way on many things the user cannot reasonably keep track of. A script, in either language, will go through any of several different code paths before hitting the appropriate interpreter depending on details like whether the other scripting language was enabled, whether the GUI was enabled, exactly which command-line options, shebang, or other means were used to tell FontForge to execute the script; and so on. There is no realistic hope of keeping all these command-line parsers and all the code paths up to date relative to one another. This is a recipe for subtle bugs.

My suggestion is that one and only one place in the code should be responsible for processing command-line arguments. The decision of whether or not to invoke the native script interpreter, and the decision of whether or not to invoke the Python interpreter, should each be made at one and only one place. Normal non-error termination of the FontForge process should happen at one and only one place. Global initialization (for instance, of the i18n subsystem) should happen only once, on an unconditional code path, and global cleanup should happen only once, on an unconditional code path. It would make sense, but is not necessary, for all of these single decision points to be located in the same function, for that function to contain very little else, and for the file containing it to contain very few other functions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions