Loading src/shell.cpp +73 −9 Changes for src/shell.cpp: 73 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -2056,6 +2056,29 @@ bool fresh::needsMoreInput(const std::string &text){ text[pos]=='\n' || text[pos]=='\r' || text[pos]==';'; }; // Whitespace alone (what isBoundary() above checks) only tells us a // candidate keyword isn't glued onto some longer identifier -- it does // NOT tell us the word is actually in command-name position. "echo // done" has plain word-separating whitespace right before "done" too, // but that "done" is cat's own argument, not a real while-loop // terminator; matching it as one anyway desyncs `depth` and makes this // function wrongly report a still-open "{"/if/for/while/case as // already-complete. Real command-name position is: the very start of // `text`, or right after skipping back over any plain whitespace to a // ';', a real newline, '|', or "&&"/"||" -- the same set of separators // parse()'s own per-statement scan and collectFunctionDefinitions() // gate the identical keyword recognition on (see their own comments). auto isStmtStart=[&](size_t pos)->bool{ size_t p=pos; while(p>0 && (text[p-1]==' ' || text[p-1]=='\t')) --p; if(p==0) return true; char c=text[p-1]; if(c==';' || c=='\n' || c=='|') return true; if(c=='&' && p>=2 && text[p-2]=='&') return true; return false; }; while(i<n){ if(inHeredoc){ size_t lineStart=i; Loading Loading @@ -2147,11 +2170,32 @@ bool fresh::needsMoreInput(const std::string &text){ // tokenizeShellWords() naturally gluing the whole "${...}" into // one word the original word-based approach relied on. bool leftBoundary=(i==0) || isBoundary(i-1) || text[i-1]=='#'; if(leftBoundary){ bool stmtStart=isStmtStart(i); // A function definition's own "NAME()"/"NAME ()" header (glued or // spaced, tokenizeShellWords() accepts both) is the single most // common position for a "{" to open its body -- e.g. this exact // function's own real-world trigger, the Linux kernel's own // scripts/syscallhdr.sh: "usage() {". isStmtStart() alone rejects // it (nothing in its own recognized separator set includes a bare // ')'), so check for it separately, and ONLY for "{" specifically // -- unlike a real statement separator, a bare ')' right before // "done"/"fi"/"esac"/"}" (a command-substitution result used as a // plain argument, e.g. "echo $(date) done") is NOT a safe signal // that "done" here is a real keyword, so this must never relax // the closers' own, stricter check below. bool afterFuncHeaderParen=false; if(leftBoundary && !stmtStart){ size_t p=i; while(p>0 && (text[p-1]==' ' || text[p-1]=='\t')) --p; afterFuncHeaderParen=(p>0 && text[p-1]==')'); } if(leftBoundary && (stmtStart || afterFuncHeaderParen)){ bool handled=false; for(const char *kw: openers){ size_t klen=strlen(kw); if(text.compare(i,klen,kw)==0 && isBoundary(i+klen)){ bool positionOk=stmtStart || (afterFuncHeaderParen && kw==std::string("{")); if(positionOk && text.compare(i,klen,kw)==0 && isBoundary(i+klen)){ ++depth; i+=klen; handled=true; Loading @@ -2159,6 +2203,7 @@ bool fresh::needsMoreInput(const std::string &text){ } } if(handled) continue; if(stmtStart){ for(const char *kw: closers){ size_t klen=strlen(kw); if(text.compare(i,klen,kw)==0 && isBoundary(i+klen)){ Loading @@ -2170,6 +2215,7 @@ bool fresh::needsMoreInput(const std::string &text){ } if(handled) continue; } } ++i; } Loading Loading @@ -2904,7 +2950,8 @@ int fresh::Shell::runSegmentWords(const std::string &input,const std::vector<She // this depth count and either close the group too early or // (as observed) miss the real closing '}' entirely. bool atStmtStart=(i==from+1 || words[i-1].text==";" || words[i-1].text=="&&" || words[i-1].text=="||"); words[i-1].text=="&&" || words[i-1].text=="||" || words[i-1].text=="|"); if(atStmtStart && words[i].text=="{") ++depth; else if(atStmtStart && words[i].text=="}"){ if(--depth==0){ closeIdx=i; break; } Loading Loading @@ -3043,7 +3090,8 @@ void fresh::Shell::collectFunctionDefinitions(const std::string &input,std::vect const std::string &w=words[i].text; bool atStmtStart=(i==0) || words[i-1].text==";" || words[i-1].text=="&&" || words[i-1].text=="||"; words[i-1].text=="&&" || words[i-1].text=="||" || words[i-1].text=="|"; if(depth==0 && atStmtStart){ std::string name; Loading Loading @@ -3148,9 +3196,16 @@ void fresh::Shell::collectFunctionDefinitions(const std::string &input,std::vect } } if(w=="if" || w=="for" || w=="while" || w=="case" || w=="{") // Same "must be in command-name position" guard as the outer // per-statement scan in parse() applies for the identical reason // (see its own comment) -- otherwise an ordinary argument word // that happens to equal one of these (a "done"/"fi"/"esac"/"}" // arriving via echo, a variable's value, etc.) desyncs this depth // count, throwing off whether a LATER real function header // anywhere else in this same input is seen at depth 0 or not. if(atStmtStart && (w=="if" || w=="for" || w=="while" || w=="case" || w=="{")) ++depth; else if(depth>0 && (w=="fi" || w=="done" || w=="esac" || w=="}")) else if(atStmtStart && depth>0 && (w=="fi" || w=="done" || w=="esac" || w=="}")) --depth; ++i; Loading Loading @@ -3331,11 +3386,20 @@ int fresh::Shell::parse(const std::string &input,bool applyErrexit){ // covers this SPECIFIC loop's own equivalent of "i==0" there (the // start of whichever ';'/'&&'/'||'-delimited segment is currently // being scanned, not necessarily the whole input's own word 0), // and the "!" case covers "! if ...; then ...; fi"'s negated // compound command, whose "if" sits right after "!", not right // after a real separator. // the "!" case covers "! if ...; then ...; fi"'s negated compound // command, whose "if" sits right after "!", not right after a real // separator, and "|" covers a compound command starting (or, for // a lone "}", nonsensically but harmlessly, ending) any stage of a // pipeline, not just its first one -- e.g. "echo hi | { echo abc | // tr a-z A-Z; echo done; } > file" (the Linux kernel's own // scripts/syscallhdr.sh uses exactly this shape): without this, // that "{" sat right after "|", was never recognized as an opener // at all, and every ';'/'&&'/'||' inside the group -- starting // with the very first one -- was wrongly treated as a top-level // segment boundary instead. bool atStmtStart=(i==segstart || words[i-1].text==";" || words[i-1].text=="&&" || words[i-1].text=="||" || words[i-1].text=="|" || (i==segstart+1 && words[segstart].text=="!")); if(atStmtStart && (w=="if" || w=="for" || w=="while" || w=="case" || w=="{")) ++depth; Loading Loading
src/shell.cpp +73 −9 Changes for src/shell.cpp: 73 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -2056,6 +2056,29 @@ bool fresh::needsMoreInput(const std::string &text){ text[pos]=='\n' || text[pos]=='\r' || text[pos]==';'; }; // Whitespace alone (what isBoundary() above checks) only tells us a // candidate keyword isn't glued onto some longer identifier -- it does // NOT tell us the word is actually in command-name position. "echo // done" has plain word-separating whitespace right before "done" too, // but that "done" is cat's own argument, not a real while-loop // terminator; matching it as one anyway desyncs `depth` and makes this // function wrongly report a still-open "{"/if/for/while/case as // already-complete. Real command-name position is: the very start of // `text`, or right after skipping back over any plain whitespace to a // ';', a real newline, '|', or "&&"/"||" -- the same set of separators // parse()'s own per-statement scan and collectFunctionDefinitions() // gate the identical keyword recognition on (see their own comments). auto isStmtStart=[&](size_t pos)->bool{ size_t p=pos; while(p>0 && (text[p-1]==' ' || text[p-1]=='\t')) --p; if(p==0) return true; char c=text[p-1]; if(c==';' || c=='\n' || c=='|') return true; if(c=='&' && p>=2 && text[p-2]=='&') return true; return false; }; while(i<n){ if(inHeredoc){ size_t lineStart=i; Loading Loading @@ -2147,11 +2170,32 @@ bool fresh::needsMoreInput(const std::string &text){ // tokenizeShellWords() naturally gluing the whole "${...}" into // one word the original word-based approach relied on. bool leftBoundary=(i==0) || isBoundary(i-1) || text[i-1]=='#'; if(leftBoundary){ bool stmtStart=isStmtStart(i); // A function definition's own "NAME()"/"NAME ()" header (glued or // spaced, tokenizeShellWords() accepts both) is the single most // common position for a "{" to open its body -- e.g. this exact // function's own real-world trigger, the Linux kernel's own // scripts/syscallhdr.sh: "usage() {". isStmtStart() alone rejects // it (nothing in its own recognized separator set includes a bare // ')'), so check for it separately, and ONLY for "{" specifically // -- unlike a real statement separator, a bare ')' right before // "done"/"fi"/"esac"/"}" (a command-substitution result used as a // plain argument, e.g. "echo $(date) done") is NOT a safe signal // that "done" here is a real keyword, so this must never relax // the closers' own, stricter check below. bool afterFuncHeaderParen=false; if(leftBoundary && !stmtStart){ size_t p=i; while(p>0 && (text[p-1]==' ' || text[p-1]=='\t')) --p; afterFuncHeaderParen=(p>0 && text[p-1]==')'); } if(leftBoundary && (stmtStart || afterFuncHeaderParen)){ bool handled=false; for(const char *kw: openers){ size_t klen=strlen(kw); if(text.compare(i,klen,kw)==0 && isBoundary(i+klen)){ bool positionOk=stmtStart || (afterFuncHeaderParen && kw==std::string("{")); if(positionOk && text.compare(i,klen,kw)==0 && isBoundary(i+klen)){ ++depth; i+=klen; handled=true; Loading @@ -2159,6 +2203,7 @@ bool fresh::needsMoreInput(const std::string &text){ } } if(handled) continue; if(stmtStart){ for(const char *kw: closers){ size_t klen=strlen(kw); if(text.compare(i,klen,kw)==0 && isBoundary(i+klen)){ Loading @@ -2170,6 +2215,7 @@ bool fresh::needsMoreInput(const std::string &text){ } if(handled) continue; } } ++i; } Loading Loading @@ -2904,7 +2950,8 @@ int fresh::Shell::runSegmentWords(const std::string &input,const std::vector<She // this depth count and either close the group too early or // (as observed) miss the real closing '}' entirely. bool atStmtStart=(i==from+1 || words[i-1].text==";" || words[i-1].text=="&&" || words[i-1].text=="||"); words[i-1].text=="&&" || words[i-1].text=="||" || words[i-1].text=="|"); if(atStmtStart && words[i].text=="{") ++depth; else if(atStmtStart && words[i].text=="}"){ if(--depth==0){ closeIdx=i; break; } Loading Loading @@ -3043,7 +3090,8 @@ void fresh::Shell::collectFunctionDefinitions(const std::string &input,std::vect const std::string &w=words[i].text; bool atStmtStart=(i==0) || words[i-1].text==";" || words[i-1].text=="&&" || words[i-1].text=="||"; words[i-1].text=="&&" || words[i-1].text=="||" || words[i-1].text=="|"; if(depth==0 && atStmtStart){ std::string name; Loading Loading @@ -3148,9 +3196,16 @@ void fresh::Shell::collectFunctionDefinitions(const std::string &input,std::vect } } if(w=="if" || w=="for" || w=="while" || w=="case" || w=="{") // Same "must be in command-name position" guard as the outer // per-statement scan in parse() applies for the identical reason // (see its own comment) -- otherwise an ordinary argument word // that happens to equal one of these (a "done"/"fi"/"esac"/"}" // arriving via echo, a variable's value, etc.) desyncs this depth // count, throwing off whether a LATER real function header // anywhere else in this same input is seen at depth 0 or not. if(atStmtStart && (w=="if" || w=="for" || w=="while" || w=="case" || w=="{")) ++depth; else if(depth>0 && (w=="fi" || w=="done" || w=="esac" || w=="}")) else if(atStmtStart && depth>0 && (w=="fi" || w=="done" || w=="esac" || w=="}")) --depth; ++i; Loading Loading @@ -3331,11 +3386,20 @@ int fresh::Shell::parse(const std::string &input,bool applyErrexit){ // covers this SPECIFIC loop's own equivalent of "i==0" there (the // start of whichever ';'/'&&'/'||'-delimited segment is currently // being scanned, not necessarily the whole input's own word 0), // and the "!" case covers "! if ...; then ...; fi"'s negated // compound command, whose "if" sits right after "!", not right // after a real separator. // the "!" case covers "! if ...; then ...; fi"'s negated compound // command, whose "if" sits right after "!", not right after a real // separator, and "|" covers a compound command starting (or, for // a lone "}", nonsensically but harmlessly, ending) any stage of a // pipeline, not just its first one -- e.g. "echo hi | { echo abc | // tr a-z A-Z; echo done; } > file" (the Linux kernel's own // scripts/syscallhdr.sh uses exactly this shape): without this, // that "{" sat right after "|", was never recognized as an opener // at all, and every ';'/'&&'/'||' inside the group -- starting // with the very first one -- was wrongly treated as a top-level // segment boundary instead. bool atStmtStart=(i==segstart || words[i-1].text==";" || words[i-1].text=="&&" || words[i-1].text=="||" || words[i-1].text=="|" || (i==segstart+1 && words[segstart].text=="!")); if(atStmtStart && (w=="if" || w=="for" || w=="while" || w=="case" || w=="{")) ++depth; Loading