Loading src/shell.cpp +80 −0 Changes for src/shell.cpp: 80 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -480,6 +480,86 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ // ISO boot-medium scan) into an infinite loop. Must be checked // BEFORE the "$(cmd)" branch just below, which explicitly excludes // "$((" and would otherwise never get a chance to run first. // "${...}" parameter expansion: scanned here as one complete, // brace-depth-matched unit (same simple '{'/'}' counting // Environment::expand() itself already uses internally for e.g. // "${var:-word}") BEFORE the "$(cmd)"/"$((expr))" branches below // ever get a look at it. Without this, a "$(...)" nested inside an // operator's default word -- e.g. kernel.spec's own // "${KERNEL_JOBS:-$(nproc)}" -- gets intercepted by the standalone // "$(cmd)" branch mid-scan: it flushPending()s the still-open, // syntactically incomplete "${KERNEL_JOBS:-" prefix on its own, // Environment::expand() (which has no closing '}' to find) falls // back to its own documented "unterminated -- treat as literal" // behavior, and the REAL closing '}' -- once the scan reaches it -- // has nothing left open to close and comes out as a stray literal // character. Net effect: "-jKERNEL_JOBS" silently became // "-j:-32}"/"-j32:-32}" instead of "-j32", and make -j's malformed // value went unnoticed rather than erroring, because *some* text // (the genuinely-computed default/value) still came out first. // Any "$(...)"/"`...`" found while scanning the span is resolved // right here, via the same runCaptured() plumbing the standalone // branches below use, so the text finally handed to // Environment::expand() is fully substituted already -- its own // "${var:-word}" operator handling has no command-substitution // support of its own and was never meant to need it. if(c=='$' && i+1<n && segment[i+1]=='{'){ size_t j=i+2; int depth=1; std::string raw; while(j<n && depth>0){ char cj=segment[j]; if(cj=='{'){ ++depth; raw+=cj; ++j; }else if(cj=='}'){ --depth; if(depth>0) raw+=cj; ++j; }else if(cj=='$' && j+1<n && segment[j+1]=='(' && !(j+2<n && segment[j+2]=='(')){ size_t k=j+2; int pdepth=1; while(k<n && pdepth>0){ if(segment[k]=='(') ++pdepth; else if(segment[k]==')') --pdepth; if(pdepth>0) ++k; } raw+=Shell::instance().runCaptured(segment.substr(j+2,k-(j+2))); j=(k<n) ? k+1 : k; }else if(cj=='`'){ size_t k=j+1; std::string inner; while(k<n && segment[k]!='`'){ if(segment[k]=='\\' && k+1<n && (segment[k+1]=='`' || segment[k+1]=='$' || segment[k+1]=='\\')){ inner+=segment[k+1]; k+=2; continue; } inner+=segment[k]; ++k; } raw+=Shell::instance().runCaptured(inner); j=(k<n) ? k+1 : k; }else{ raw+=cj; ++j; } } if(depth==0){ flushPending(); current+=fresh::Environment::instance().expand("${"+raw+"}"); have=true; i=j; continue; } // unterminated -- fall through, let '$' accumulate as an // ordinary character below, same "not full parity" handling // every other unterminated-substitution case here already gets. } // flushPending() first, same as the "$(cmd)" branch, so any plain // text accumulated just before this substitution resolves (in the // right order) ahead of it. Loading Loading
src/shell.cpp +80 −0 Changes for src/shell.cpp: 80 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -480,6 +480,86 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ // ISO boot-medium scan) into an infinite loop. Must be checked // BEFORE the "$(cmd)" branch just below, which explicitly excludes // "$((" and would otherwise never get a chance to run first. // "${...}" parameter expansion: scanned here as one complete, // brace-depth-matched unit (same simple '{'/'}' counting // Environment::expand() itself already uses internally for e.g. // "${var:-word}") BEFORE the "$(cmd)"/"$((expr))" branches below // ever get a look at it. Without this, a "$(...)" nested inside an // operator's default word -- e.g. kernel.spec's own // "${KERNEL_JOBS:-$(nproc)}" -- gets intercepted by the standalone // "$(cmd)" branch mid-scan: it flushPending()s the still-open, // syntactically incomplete "${KERNEL_JOBS:-" prefix on its own, // Environment::expand() (which has no closing '}' to find) falls // back to its own documented "unterminated -- treat as literal" // behavior, and the REAL closing '}' -- once the scan reaches it -- // has nothing left open to close and comes out as a stray literal // character. Net effect: "-jKERNEL_JOBS" silently became // "-j:-32}"/"-j32:-32}" instead of "-j32", and make -j's malformed // value went unnoticed rather than erroring, because *some* text // (the genuinely-computed default/value) still came out first. // Any "$(...)"/"`...`" found while scanning the span is resolved // right here, via the same runCaptured() plumbing the standalone // branches below use, so the text finally handed to // Environment::expand() is fully substituted already -- its own // "${var:-word}" operator handling has no command-substitution // support of its own and was never meant to need it. if(c=='$' && i+1<n && segment[i+1]=='{'){ size_t j=i+2; int depth=1; std::string raw; while(j<n && depth>0){ char cj=segment[j]; if(cj=='{'){ ++depth; raw+=cj; ++j; }else if(cj=='}'){ --depth; if(depth>0) raw+=cj; ++j; }else if(cj=='$' && j+1<n && segment[j+1]=='(' && !(j+2<n && segment[j+2]=='(')){ size_t k=j+2; int pdepth=1; while(k<n && pdepth>0){ if(segment[k]=='(') ++pdepth; else if(segment[k]==')') --pdepth; if(pdepth>0) ++k; } raw+=Shell::instance().runCaptured(segment.substr(j+2,k-(j+2))); j=(k<n) ? k+1 : k; }else if(cj=='`'){ size_t k=j+1; std::string inner; while(k<n && segment[k]!='`'){ if(segment[k]=='\\' && k+1<n && (segment[k+1]=='`' || segment[k+1]=='$' || segment[k+1]=='\\')){ inner+=segment[k+1]; k+=2; continue; } inner+=segment[k]; ++k; } raw+=Shell::instance().runCaptured(inner); j=(k<n) ? k+1 : k; }else{ raw+=cj; ++j; } } if(depth==0){ flushPending(); current+=fresh::Environment::instance().expand("${"+raw+"}"); have=true; i=j; continue; } // unterminated -- fall through, let '$' accumulate as an // ordinary character below, same "not full parity" handling // every other unterminated-substitution case here already gets. } // flushPending() first, same as the "$(cmd)" branch, so any plain // text accumulated just before this substitution resolves (in the // right order) ahead of it. Loading