Loading commands/shell/for.cpp +13 −3 Changes for commands/shell/for.cpp: 13 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -46,14 +46,19 @@ namespace fresh { namespace cmd { std::string remainder=(argc>1 && argv[1]) ? argv[1] : ""; std::vector<ShellWord> words=tokenizeShellWords(remainder); if(words.size()<2 || words[1].text!="in"){ // "for VAR; do" / "for VAR do" (no "in" list): the loop runs // over the positional parameters, "$@" (the kernel's // scripts/setlocalversion uses "for file; do") bool overArgs=words.size()>=2 && words[1].text!="in" && (words[1].text==";" || words[1].text=="do"); if(words.size()<2 || (words[1].text!="in" && !overArgs)){ std::cerr << "for: expected 'VAR in ...'" << std::endl; return 2; } std::string varname=words[0].text; size_t doIdx=findShellKeyword(words,2,words.size(),"do"); size_t doIdx=findShellKeyword(words,overArgs ? 1 : 2,words.size(),"do"); if(doIdx==words.size()){ std::cerr << "for: expected 'do'" << std::endl; return 2; Loading @@ -65,11 +70,16 @@ namespace fresh { namespace cmd { return 2; } std::vector<std::string> items; if(overArgs){ items=Environment::instance().positionalArgs(); }else{ size_t itemsEnd=doIdx; if(itemsEnd>2 && words[itemsEnd-1].text==";") --itemsEnd; std::string itemtext=shellWordsRawText(remainder,words,2,itemsEnd); std::vector<std::string> items=tokenizeShellArgs(itemtext); items=tokenizeShellArgs(itemtext); } std::string body=shellWordsRawText(remainder,words,doIdx+1,doneIdx); std::string trailing=shellWordsRawText(remainder,words,doneIdx+1,words.size()); Loading src/shell.cpp +80 −121 Changes for src/shell.cpp: 80 added lines, 121 removed lines. Original line number Diff line number Diff line Loading @@ -80,6 +80,70 @@ static bool isValidShellName(const std::string &s){ return true; } // The ')' closing a "$(" whose inner text starts at `from` (npos-safe: // s.size() when unterminated). Quoted text, backslash escapes, backticks // and nested "$(" are skipped, and a case pattern's unbalanced ')' // ("case x in a) ...;; esac") does not close anything -- counting bare // parens instead ended the kernel's // $(ld -v | sed -e 's/(compatible with [^)]*)//') // at the ')' inside the sed program. static size_t cmdSubstClose(const std::string &s,size_t from){ size_t n=s.size(),i=from; int depth=1; std::vector<int> caseAt; // paren depth at each open "case" auto wordAt=[&](size_t k,const char *w){ size_t l=strlen(w); if(s.compare(k,l,w)!=0) return false; bool startOk=(k==0) || strchr(" \t\n;&|(",s[k-1]); bool endOk=(k+l>=n) || strchr(" \t\n;&|)",s[k+l]); return startOk && endOk; }; while(i<n){ char c=s[i]; if(c=='\\'){ i+=2; continue; } if(c=='\''){ size_t e=s.find('\'',i+1); i=(e==std::string::npos) ? n : e+1; continue; } if(c=='"'){ size_t k=i+1; while(k<n && s[k]!='"'){ if(s[k]=='\\') { k+=2; continue; } if(s[k]=='$' && k+1<n && s[k+1]=='('){ k=cmdSubstClose(s,k+2)+1; continue; } if(s[k]=='`'){ size_t e=s.find('`',k+1); k=(e==std::string::npos) ? n : e+1; continue; } k++; } i=k+1; continue; } if(c=='`'){ size_t k=i+1; while(k<n && s[k]!='`'){ if(s[k]=='\\') k++; k++; } i=k+1; continue; } if(c=='#' && (i==from || strchr(" \t\n;",s[i-1]))){ size_t e=s.find('\n',i); i=(e==std::string::npos) ? n : e; continue; } if(c=='$' && i+1<n && s[i+1]=='('){ i=cmdSubstClose(s,i+2)+1; continue; } if(wordAt(i,"case")){ caseAt.push_back(depth); i+=4; continue; } if(wordAt(i,"esac") && !caseAt.empty()){ caseAt.pop_back(); i+=4; continue; } if(c=='('){ depth++; i++; continue; } if(c==')'){ if(!caseAt.empty() && caseAt.back()==depth){ i++; continue; } // a case pattern if(--depth==0) return i; i++; continue; } i++; } return n; } static void pushGlobExpanded(std::vector<std::string> &tokens,const std::string &word){ if(word.find_first_of("*?[")==std::string::npos){ tokens.push_back(word); Loading Loading @@ -526,13 +590,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ ++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; } size_t k=cmdSubstClose(segment,j+2); raw+=Shell::instance().runCaptured(segment.substr(j+2,k-(j+2))); j=(k<n) ? k+1 : k; }else if(cj=='`'){ Loading Loading @@ -610,13 +668,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ // as the rest of this tokenizer; a "$(...)" written inside "..." // is not expanded, just copied like any other quoted text. if(c=='$' && i+1<n && segment[i+1]=='(' && !(i+2<n && segment[i+2]=='(')){ size_t j=i+2; int depth=1; while(j<n && depth>0){ if(segment[j]=='(') ++depth; else if(segment[j]==')') --depth; if(depth>0) ++j; } size_t j=cmdSubstClose(segment,i+2); std::string inner=segment.substr(i+2,j-(i+2)); flushPending(); current+=Shell::instance().runCaptured(inner); Loading Loading @@ -767,13 +819,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ ++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; } size_t k=cmdSubstClose(segment,j+2); rawExpr+=Shell::instance().runCaptured(segment.substr(j+2,k-(j+2))); j=(k<n) ? k+1 : k; }else if(cj=='\\' && j+1<n && segment[j+1]=='`'){ Loading Loading @@ -827,13 +873,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ // limit the top-level "$(...)" handling already has. if(segment[i]=='$' && i+1<n && segment[i+1]=='(' && !(i+2<n && segment[i+2]=='(')){ size_t j=i+2; int pdepth=1; while(j<n && pdepth>0){ if(segment[j]=='(') ++pdepth; else if(segment[j]==')') --pdepth; if(pdepth>0) ++j; } size_t j=cmdSubstClose(segment,i+2); std::string inner=segment.substr(i+2,j-(i+2)); flushRaw(); dqResult+=Shell::instance().runCaptured(inner); Loading Loading @@ -1084,46 +1124,11 @@ std::vector<fresh::ShellWord> fresh::tokenizeShellWords(const std::string &input // real ';'-respecting parse of its contents once this // whole span reaches it as one piece). Same depth/quote/ // backtick-aware scan as extractSubshellBody() itself. int depth=1; ++i; while(i<n && depth>0){ char c=input[i]; if(c=='\'' || c=='"'){ char q=c; ++i; // Double-quoted: a backslash-escaped quote char // (`\"`) is NOT this span's own closing quote -- // same recognized escape set as needsMoreInput()/ // tokenizeShellArgs() elsewhere in this file. Single // quotes stay fully literal (no escapes at all, // matching POSIX): the `else` scan below handles // that case unchanged. if(q=='"'){ while(i<n && input[i]!=q){ if(input[i]=='\\' && i+1<n && (input[i+1]=='"' || input[i+1]=='\\' || input[i+1]=='$' || input[i+1]=='`')) i+=2; else ++i; } }else{ while(i<n && input[i]!=q) ++i; } }else if(c=='`'){ ++i; while(i<n && input[i]!='`'){ if(input[i]=='\\' && i+1<n && input[i+1]=='`') ++i; ++i; } }else if(c=='('){ ++depth; }else if(c==')'){ --depth; } ++i; // (cmdSubstClose(): quotes, escapes, backticks, nested // "$(" and case patterns' lone ')' are all skipped) { size_t close=cmdSubstClose(input,i+1); i=(close<n) ? close+1 : n; } continue; } Loading Loading @@ -2003,13 +2008,8 @@ std::vector<std::string> fresh::splitPipelineStages(const std::string &segment, // that one specifically needs its own branch) silently split into // two broken pipeline stages instead of one command. if(c=='$' && i+1<n && segment[i+1]=='('){ size_t j=i+2; int depth=1; while(j<n && depth>0){ if(segment[j]=='(') ++depth; else if(segment[j]==')') --depth; ++j; } size_t j=cmdSubstClose(segment,i+2); if(j<n) ++j; current+=segment.substr(i,j-i); i=j; continue; Loading Loading @@ -2169,53 +2169,12 @@ bool fresh::extractSubshellBody(const std::string &stage,std::string &body,std:: if(i>=n || stage[i]!='(') return false; // Depth-tracked scan for the matching ')', skipping over quoted and // backtick-quoted spans so a '(' or ')' inside one of those (e.g. an // embedded "echo ')'") doesn't desync the count -- same approach as // splitPipelineStages()'s "$(...)" handling just above. size_t j=i+1; int depth=1; while(j<n && depth>0){ char c=stage[j]; if(c=='\'' || c=='"'){ char q=c; ++j; // Double-quoted: a backslash-escaped quote (`\"`) is NOT this // span's own closing quote -- same escape-aware fix already // applied to tokenizeShellWords()'s own "(...)" absorption scan // (see its comment); without it, a body like // `( sed "s/\"//g" )` desyncs this depth count on the escaped // quote and returns a truncated/wrong body. Single quotes stay // fully literal (POSIX has no escapes inside them at all). if(q=='"'){ while(j<n && stage[j]!=q){ if(stage[j]=='\\' && j+1<n && (stage[j+1]=='"' || stage[j+1]=='\\' || stage[j+1]=='$' || stage[j+1]=='`')) j+=2; else ++j; } }else{ while(j<n && stage[j]!=q) ++j; } }else if(c=='`'){ ++j; while(j<n && stage[j]!='`'){ if(stage[j]=='\\' && j+1<n && stage[j+1]=='`') ++j; ++j; } }else if(c=='('){ ++depth; }else if(c==')'){ --depth; } ++j; } if(depth>0) // the matching ')' (quotes, escapes, backticks, nested "$(" and case // patterns skipped -- see cmdSubstClose()) size_t close=cmdSubstClose(stage,i+1); if(close>=n) return false; // unterminated "(" -- treat as not a subshell at all size_t j=close+1; body=stage.substr(i+1,(j-1)-(i+1)); suffix=stage.substr(j); Loading Loading
commands/shell/for.cpp +13 −3 Changes for commands/shell/for.cpp: 13 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -46,14 +46,19 @@ namespace fresh { namespace cmd { std::string remainder=(argc>1 && argv[1]) ? argv[1] : ""; std::vector<ShellWord> words=tokenizeShellWords(remainder); if(words.size()<2 || words[1].text!="in"){ // "for VAR; do" / "for VAR do" (no "in" list): the loop runs // over the positional parameters, "$@" (the kernel's // scripts/setlocalversion uses "for file; do") bool overArgs=words.size()>=2 && words[1].text!="in" && (words[1].text==";" || words[1].text=="do"); if(words.size()<2 || (words[1].text!="in" && !overArgs)){ std::cerr << "for: expected 'VAR in ...'" << std::endl; return 2; } std::string varname=words[0].text; size_t doIdx=findShellKeyword(words,2,words.size(),"do"); size_t doIdx=findShellKeyword(words,overArgs ? 1 : 2,words.size(),"do"); if(doIdx==words.size()){ std::cerr << "for: expected 'do'" << std::endl; return 2; Loading @@ -65,11 +70,16 @@ namespace fresh { namespace cmd { return 2; } std::vector<std::string> items; if(overArgs){ items=Environment::instance().positionalArgs(); }else{ size_t itemsEnd=doIdx; if(itemsEnd>2 && words[itemsEnd-1].text==";") --itemsEnd; std::string itemtext=shellWordsRawText(remainder,words,2,itemsEnd); std::vector<std::string> items=tokenizeShellArgs(itemtext); items=tokenizeShellArgs(itemtext); } std::string body=shellWordsRawText(remainder,words,doIdx+1,doneIdx); std::string trailing=shellWordsRawText(remainder,words,doneIdx+1,words.size()); Loading
src/shell.cpp +80 −121 Changes for src/shell.cpp: 80 added lines, 121 removed lines. Original line number Diff line number Diff line Loading @@ -80,6 +80,70 @@ static bool isValidShellName(const std::string &s){ return true; } // The ')' closing a "$(" whose inner text starts at `from` (npos-safe: // s.size() when unterminated). Quoted text, backslash escapes, backticks // and nested "$(" are skipped, and a case pattern's unbalanced ')' // ("case x in a) ...;; esac") does not close anything -- counting bare // parens instead ended the kernel's // $(ld -v | sed -e 's/(compatible with [^)]*)//') // at the ')' inside the sed program. static size_t cmdSubstClose(const std::string &s,size_t from){ size_t n=s.size(),i=from; int depth=1; std::vector<int> caseAt; // paren depth at each open "case" auto wordAt=[&](size_t k,const char *w){ size_t l=strlen(w); if(s.compare(k,l,w)!=0) return false; bool startOk=(k==0) || strchr(" \t\n;&|(",s[k-1]); bool endOk=(k+l>=n) || strchr(" \t\n;&|)",s[k+l]); return startOk && endOk; }; while(i<n){ char c=s[i]; if(c=='\\'){ i+=2; continue; } if(c=='\''){ size_t e=s.find('\'',i+1); i=(e==std::string::npos) ? n : e+1; continue; } if(c=='"'){ size_t k=i+1; while(k<n && s[k]!='"'){ if(s[k]=='\\') { k+=2; continue; } if(s[k]=='$' && k+1<n && s[k+1]=='('){ k=cmdSubstClose(s,k+2)+1; continue; } if(s[k]=='`'){ size_t e=s.find('`',k+1); k=(e==std::string::npos) ? n : e+1; continue; } k++; } i=k+1; continue; } if(c=='`'){ size_t k=i+1; while(k<n && s[k]!='`'){ if(s[k]=='\\') k++; k++; } i=k+1; continue; } if(c=='#' && (i==from || strchr(" \t\n;",s[i-1]))){ size_t e=s.find('\n',i); i=(e==std::string::npos) ? n : e; continue; } if(c=='$' && i+1<n && s[i+1]=='('){ i=cmdSubstClose(s,i+2)+1; continue; } if(wordAt(i,"case")){ caseAt.push_back(depth); i+=4; continue; } if(wordAt(i,"esac") && !caseAt.empty()){ caseAt.pop_back(); i+=4; continue; } if(c=='('){ depth++; i++; continue; } if(c==')'){ if(!caseAt.empty() && caseAt.back()==depth){ i++; continue; } // a case pattern if(--depth==0) return i; i++; continue; } i++; } return n; } static void pushGlobExpanded(std::vector<std::string> &tokens,const std::string &word){ if(word.find_first_of("*?[")==std::string::npos){ tokens.push_back(word); Loading Loading @@ -526,13 +590,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ ++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; } size_t k=cmdSubstClose(segment,j+2); raw+=Shell::instance().runCaptured(segment.substr(j+2,k-(j+2))); j=(k<n) ? k+1 : k; }else if(cj=='`'){ Loading Loading @@ -610,13 +668,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ // as the rest of this tokenizer; a "$(...)" written inside "..." // is not expanded, just copied like any other quoted text. if(c=='$' && i+1<n && segment[i+1]=='(' && !(i+2<n && segment[i+2]=='(')){ size_t j=i+2; int depth=1; while(j<n && depth>0){ if(segment[j]=='(') ++depth; else if(segment[j]==')') --depth; if(depth>0) ++j; } size_t j=cmdSubstClose(segment,i+2); std::string inner=segment.substr(i+2,j-(i+2)); flushPending(); current+=Shell::instance().runCaptured(inner); Loading Loading @@ -767,13 +819,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ ++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; } size_t k=cmdSubstClose(segment,j+2); rawExpr+=Shell::instance().runCaptured(segment.substr(j+2,k-(j+2))); j=(k<n) ? k+1 : k; }else if(cj=='\\' && j+1<n && segment[j+1]=='`'){ Loading Loading @@ -827,13 +873,7 @@ std::vector<std::string> fresh::tokenizeShellArgs(const std::string &segment){ // limit the top-level "$(...)" handling already has. if(segment[i]=='$' && i+1<n && segment[i+1]=='(' && !(i+2<n && segment[i+2]=='(')){ size_t j=i+2; int pdepth=1; while(j<n && pdepth>0){ if(segment[j]=='(') ++pdepth; else if(segment[j]==')') --pdepth; if(pdepth>0) ++j; } size_t j=cmdSubstClose(segment,i+2); std::string inner=segment.substr(i+2,j-(i+2)); flushRaw(); dqResult+=Shell::instance().runCaptured(inner); Loading Loading @@ -1084,46 +1124,11 @@ std::vector<fresh::ShellWord> fresh::tokenizeShellWords(const std::string &input // real ';'-respecting parse of its contents once this // whole span reaches it as one piece). Same depth/quote/ // backtick-aware scan as extractSubshellBody() itself. int depth=1; ++i; while(i<n && depth>0){ char c=input[i]; if(c=='\'' || c=='"'){ char q=c; ++i; // Double-quoted: a backslash-escaped quote char // (`\"`) is NOT this span's own closing quote -- // same recognized escape set as needsMoreInput()/ // tokenizeShellArgs() elsewhere in this file. Single // quotes stay fully literal (no escapes at all, // matching POSIX): the `else` scan below handles // that case unchanged. if(q=='"'){ while(i<n && input[i]!=q){ if(input[i]=='\\' && i+1<n && (input[i+1]=='"' || input[i+1]=='\\' || input[i+1]=='$' || input[i+1]=='`')) i+=2; else ++i; } }else{ while(i<n && input[i]!=q) ++i; } }else if(c=='`'){ ++i; while(i<n && input[i]!='`'){ if(input[i]=='\\' && i+1<n && input[i+1]=='`') ++i; ++i; } }else if(c=='('){ ++depth; }else if(c==')'){ --depth; } ++i; // (cmdSubstClose(): quotes, escapes, backticks, nested // "$(" and case patterns' lone ')' are all skipped) { size_t close=cmdSubstClose(input,i+1); i=(close<n) ? close+1 : n; } continue; } Loading Loading @@ -2003,13 +2008,8 @@ std::vector<std::string> fresh::splitPipelineStages(const std::string &segment, // that one specifically needs its own branch) silently split into // two broken pipeline stages instead of one command. if(c=='$' && i+1<n && segment[i+1]=='('){ size_t j=i+2; int depth=1; while(j<n && depth>0){ if(segment[j]=='(') ++depth; else if(segment[j]==')') --depth; ++j; } size_t j=cmdSubstClose(segment,i+2); if(j<n) ++j; current+=segment.substr(i,j-i); i=j; continue; Loading Loading @@ -2169,53 +2169,12 @@ bool fresh::extractSubshellBody(const std::string &stage,std::string &body,std:: if(i>=n || stage[i]!='(') return false; // Depth-tracked scan for the matching ')', skipping over quoted and // backtick-quoted spans so a '(' or ')' inside one of those (e.g. an // embedded "echo ')'") doesn't desync the count -- same approach as // splitPipelineStages()'s "$(...)" handling just above. size_t j=i+1; int depth=1; while(j<n && depth>0){ char c=stage[j]; if(c=='\'' || c=='"'){ char q=c; ++j; // Double-quoted: a backslash-escaped quote (`\"`) is NOT this // span's own closing quote -- same escape-aware fix already // applied to tokenizeShellWords()'s own "(...)" absorption scan // (see its comment); without it, a body like // `( sed "s/\"//g" )` desyncs this depth count on the escaped // quote and returns a truncated/wrong body. Single quotes stay // fully literal (POSIX has no escapes inside them at all). if(q=='"'){ while(j<n && stage[j]!=q){ if(stage[j]=='\\' && j+1<n && (stage[j+1]=='"' || stage[j+1]=='\\' || stage[j+1]=='$' || stage[j+1]=='`')) j+=2; else ++j; } }else{ while(j<n && stage[j]!=q) ++j; } }else if(c=='`'){ ++j; while(j<n && stage[j]!='`'){ if(stage[j]=='\\' && j+1<n && stage[j+1]=='`') ++j; ++j; } }else if(c=='('){ ++depth; }else if(c==')'){ --depth; } ++j; } if(depth>0) // the matching ')' (quotes, escapes, backticks, nested "$(" and case // patterns skipped -- see cmdSubstClose()) size_t close=cmdSubstClose(stage,i+1); if(close>=n) return false; // unterminated "(" -- treat as not a subshell at all size_t j=close+1; body=stage.substr(i+1,(j-1)-(i+1)); suffix=stage.substr(j); Loading