From cd654e182b89e83b5515c1f5978c3475a9d5cf67 Mon Sep 17 00:00:00 2001 From: is-primary-dev <215415441+is-primary-dev@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:55:23 -0700 Subject: [PATCH] ini: restore #INCLUDE handling in Tcl parse_ini MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream commit 2f57090adb ("ini: Implement a new ini-file parser and adapt the code to use it") added native #INCLUDE support to the new C++ parser in src/emc/ini/inifile.cc and removed the 53-line handle_includes() shell preprocessor from scripts/linuxcnc.in, because the new parser made the preprocessor redundant for the main launcher flow. However, lib/hallib/check_config.tcl uses the pure-Tcl parse_ini proc in tcl/linuxcnc.tcl, which has no #INCLUDE support. It relied on the shell preprocessor expanding the file before parse_ini ever saw it. Removing the preprocessor broke check_config.tcl for any INI file using #INCLUDE directives — including the shipped sim demo at configs/sim/axis/ini_with_includes/includes_demo.ini, which fails on current upstream master with: $ tclsh8.6 lib/hallib/check_config.tcl \ configs/sim/axis/ini_with_includes/includes_demo.ini check_config: Missing [KINS]KINEMATICS= Missing [KINS]JOINTS= check_config validation failed Extend parse_ini to recognize '#INCLUDE ' lines and parse the referenced file recursively, resolving relative filenames against the including file's directory. This matches the semantics of inifile.cc's IniFileContent::parseLine() #INCLUDE handling. Errors during recursive parse (missing file, unreadable, etc.) are logged to stderr and the outer parse continues — matching the C parser's non-fatal behavior on bad includes so that mandatory-items checks can still see whatever was loaded up to the point of failure. The recursive call uses `uplevel 1` so the included file's sections land in the same scope as the top-level file's sections. For existing callers (check_config.tcl from file scope, twopass.tcl::tp::pass1 from a proc), this preserves the historical single-file upvar 1 semantics at every recursion depth — the recursive call runs as if it had been made directly from the original caller, so upvar 1 inside the recursive parse_ini walks back to the same frame as the top-level call. After this change, check_config.tcl accepts includes_demo.ini cleanly (the file has two intentional error-demo directives at the end which now surface as stderr warnings but no longer abort the check). --- tcl/linuxcnc.tcl.in | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tcl/linuxcnc.tcl.in b/tcl/linuxcnc.tcl.in index 9bc498d0975..43ffe7640ac 100644 --- a/tcl/linuxcnc.tcl.in +++ b/tcl/linuxcnc.tcl.in @@ -136,12 +136,37 @@ proc linuxcnc::standard_fixed_font {} { proc parse_ini {filename} { # create associative arrays for all ini file sections - # like: ::EMC(VERSION), ::KINS(JOINTS), ... etc + # like: ::EMC(VERSION), ::KINS(JOINTS), ... etc. + # + # #INCLUDE directives are followed recursively, matching + # the semantics of the C++ INI parser's IniFileContent::parseLine() + # in src/emc/ini/inifile.cc. Relative includes resolve against the + # including file's directory. + # + # The recursive call uses `uplevel 1` so the included file's + # sections land in the same scope as the top-level file's sections + # — which for existing callers (check_config.tcl from file scope, + # twopass.tcl::tp::pass1 from a proc) matches the historical + # single-file upvar 1 semantics at every recursion depth. set f [open $filename] + set dir [file dirname $filename] while {[gets $f line] >= 0} { set line [string trim $line] - if {[regexp {^\[(.*)\]\s*$} $line _ section]} { + if {[regexp {^#INCLUDE\s+(.+)$} $line _ include_file]} { + # Resolve a relative #INCLUDE against the including file's + # directory, matching inifile.cc's dirname-prepend logic. + # On error (missing file, unreadable, parse failure), emit + # a warning to stderr and continue — matching the C parser's + # non-fatal behavior in inifile.cc: it prints a message and + # lets the caller see whatever was loaded up to that point. + if {[file pathtype $include_file] ne "absolute"} { + set include_file [file join $dir $include_file] + } + if {[catch {uplevel 1 [list parse_ini $include_file]} errmsg]} { + puts stderr "parse_ini: $filename: #INCLUDE $include_file: $errmsg" + } + } elseif {[regexp {^\[(.*)\]\s*$} $line _ section]} { # nothing } elseif {[regexp {^([^#]+?)\s*=\s*(.*?)\s*$} $line _ k v]} { upvar $section s