Commit 36fc2faa authored by jan.koester's avatar jan.koester
Browse files

while rewrite with until support

parent 38d13c41
Loading
Loading
Loading
Loading
+81 −74
Changes for commands/shell/while.cpp: 81 added lines, 74 removed lines.
Original line number Diff line number Diff line
@@ -30,6 +30,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// condition that never becomes false. Shell-only builtin, no symlink.

#include <iostream>
#include <cstring>
#include <cerrno>

#include <fcntl.h>
#include <unistd.h>
@@ -38,9 +40,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

namespace fresh { namespace cmd {

    class WhileCommand : public ShellCommand {
    // "while COND; do BODY; done [REDIRECTIONS]" and its inverse "until".
    // Redirections after "done" apply to the whole loop ("done < file",
    // "done > out 2>&1"), as does a here-document.
    class LoopCommand : public ShellCommand {
    public:
        WhileCommand():ShellCommand("while"){}
        LoopCommand(const char *name,bool until):ShellCommand(name),_until(until),_name(name){}

        int runCommand(int argc,const char **argv){
            std::string remainder=(argc>1 && argv[1]) ? argv[1] : "";
@@ -48,13 +53,13 @@ namespace fresh { namespace cmd {

            size_t doIdx=findShellKeyword(words,0,words.size(),"do");
            if(doIdx==words.size()){
                std::cerr << "while: expected 'do'" << std::endl;
                std::cerr << _name << ": expected 'do'" << std::endl;
                return 2;
            }

            size_t doneIdx=findShellKeyword(words,doIdx+1,words.size(),"done");
            if(doneIdx==words.size()){
                std::cerr << "while: expected 'done'" << std::endl;
                std::cerr << _name << ": expected 'done'" << std::endl;
                return 2;
            }

@@ -64,24 +69,10 @@ namespace fresh { namespace cmd {

            Shell &shell=Shell::instance();

            // "done <<WORD" -- a here-doc redirect on the loop AS A WHOLE
            // (e.g. the kernel's own tools/lib/subcmd/../sync-check.sh:
            // "while read -r f; do ...; done <<EOF\n$FILES\nEOF"), not more
            // statements after it (this builtin's other, ";  more..." use of
            // `trailing`, handled below). Must be applied to fd 0 BEFORE the
            // loop below ever runs: the loop's own CONDITION is frequently
            // exactly the "read" that's supposed to consume this body (as
            // in the sync-check.sh example above), so redirecting only
            // after the loop returns is already too late -- every previous
            // build of this shell instead left fd 0 pointed at whatever it
            // already was (a real terminal or an unrelated inherited pipe),
            // so that first "read" blocked forever waiting for input that
            // was never coming, or silently consumed the wrong stream.
            int savedStdin=-1;
            int savedStdin=-1,savedStdout=-1,savedStderr=-1;
            std::string heredocTempFile;
            HeredocSpec heredocSpec;
            bool hasHeredoc=extractHeredocSpec(trailing,heredocSpec);
            if(hasHeredoc){
            if(extractHeredocSpec(trailing,heredocSpec)){
                heredocTempFile=shell.collectHeredocBody(heredocSpec);
                if(!heredocTempFile.empty()){
                    int fd=open(heredocTempFile.c_str(),O_RDONLY);
@@ -91,42 +82,33 @@ namespace fresh { namespace cmd {
                        close(fd);
                    }
                }
                trailing.clear();
            }

            // the remaining trailing words are redirections for the loop
            std::string trailingCommand;
            if(!trailing.empty()){
                Redirection redir;
                std::vector<std::string> rest=extractRedirections(tokenizeShellArgs(trailing),redir);
                if(!rest.empty()){
                    trailingCommand=trailing; // not redirections: run it after the loop, as before
                }else if(!applyRedirections(redir,savedStdin,savedStdout,savedStderr)){
                    restore(savedStdin,savedStdout,savedStderr);
                    return 1;
                }
            }
            // Default for a loop that genuinely never iterates (condition
            // false first try) -- matching real shells. `condition` is
            // evaluated lazily inside the loop below, not yet here, so
            // (unlike if/for/case's own analogous fallback) there is
            // nothing to read back from shell.exitCode() yet at this
            // point; see the post-loop check below instead for the
            // "fatal error stopped the shell before any iteration" case.
            int status=0;
            const long maxiterations=1000000L;
            long iterations=0;

            int status=0;
            shell.enterLoop();
            // false is how the loop ends, so the condition must not
            // trigger set -e.
            while(shell.running() && !shell.returning() && !shell.breaking() &&
                  !shell.continuing() && shell.parse(condition,false)==0){
                if(++iterations>maxiterations){
                    std::cerr << "while: iteration limit exceeded" << std::endl;
            while(shell.running() && !shell.returning() && !shell.breaking() && !shell.continuing()){
                bool condTrue=shell.parse(condition,false)==0;
                if(condTrue==_until)
                    break;
                }
                if(!body.empty())
                    status=shell.parse(body);
                // A pending "return" (see Shell::returning()) must break
                // this loop for good, same as "exit" already does via
                // running() -- so it can keep unwinding up to the
                // callFunction() waiting to consume it, instead of the loop
                // re-testing its condition and running another iteration.
                if(!shell.running() || shell.returning())
                    break;

                // "break"/"continue" (see Shell::breaking()/continuing()):
                // this loop is the one that actually consumes either
                // signal, same reasoning as for.cpp's own handling (see
                // its comment for the full explanation of the
                // consume*Level() true/false split).
                if(shell.breaking()){
                    shell.consumeBreakLevel();
                    break;
@@ -139,45 +121,70 @@ namespace fresh { namespace cmd {
            }
            shell.exitLoop();

            // Restore whatever fd 0 really was before this loop redirected
            // it above, and drop the temp file backing the heredoc body --
            // both regardless of how the loop ended (normal exhaustion,
            // break, return, or a fatal error), same as any other
            // redirection's scope never outliving the construct it was
            // attached to.
            if(savedStdin>=0){
                dup2(savedStdin,0);
                close(savedStdin);
            }
            std::cout.flush();
            std::cerr.flush();
            restore(savedStdin,savedStdout,savedStderr);
            if(!heredocTempFile.empty())
                unlink(heredocTempFile.c_str());

            // Unlike if/for/case (whose condition/subject/list is always
            // evaluated into a separate local BEFORE their own "status"
            // fallback is set up, so a fatal trigger there is already
            // reflected by the time that fallback runs), this loop's
            // condition is evaluated lazily inside the `while(...)`
            // controlling expression itself -- including on the very
            // first pass, which happens AFTER `status` above was already
            // initialized. Re-check here instead: if the shell is still
            // running, nothing overrides a legitimately-computed `status`
            // (whether left at its initial 0 for a loop that genuinely
            // never iterated, or set by the loop body's own last run);
            // if a "${var?word}" fired during any condition check (first
            // pass or later) it already stopped the shell via
            // requestExit(), and that real code must win here instead of
            // being left at whatever `status` was before that happened.
            if(!shell.running())
                status=shell.exitCode();

            if(!trailing.empty() && shell.running() && !shell.returning() &&
            if(!trailingCommand.empty() && shell.running() && !shell.returning() &&
               !shell.breaking() && !shell.continuing())
                status=shell.parse(trailing);
                status=shell.parse(trailingCommand);

            return status;
        }

    private:
        bool _until;
        const char *_name;

        static bool redirect(int target,int fd,int &saved){
            if(fd<0) return false;
            if(saved<0) saved=dup(target);
            dup2(fd,target);
            close(fd);
            return true;
        }

        bool applyRedirections(const Redirection &r,int &in,int &out,int &err){
            if(r.hasStdin && !redirect(0,open(r.stdinFile.c_str(),O_RDONLY),in)){
                std::cerr << "fresh: " << r.stdinFile << ": " << strerror(errno) << std::endl;
                return false;
            }
            if(r.hasStdout && !redirect(1,open(r.stdoutFile.c_str(),O_WRONLY|O_CREAT|(r.append ? O_APPEND : O_TRUNC),0644),out)){
                std::cerr << "fresh: " << r.stdoutFile << ": " << strerror(errno) << std::endl;
                return false;
            }
            if(r.hasStderr && !redirect(2,open(r.stderrFile.c_str(),O_WRONLY|O_CREAT|(r.stderrAppend ? O_APPEND : O_TRUNC),0644),err)){
                std::cerr << "fresh: " << r.stderrFile << ": " << strerror(errno) << std::endl;
                return false;
            }
            if(r.stderrToStdout) redirect(2,dup(1),err);
            if(r.stdoutToStderr) redirect(1,dup(2),out);
            return true;
        }

        static void restore(int in,int out,int err){
            if(in>=0){ dup2(in,0); close(in); }
            if(out>=0){ dup2(out,1); close(out); }
            if(err>=0){ dup2(err,2); close(err); }
        }
    };

    class WhileCommand : public LoopCommand {
    public:
        WhileCommand():LoopCommand("while",false){}
    };

    class UntilCommand : public LoopCommand {
    public:
        UntilCommand():LoopCommand("until",true){}
    };

    FRESH_REGISTER_COMMAND(WhileCommand);
    FRESH_REGISTER_COMMAND(UntilCommand);

}}
+52 −8
Changes for src/shell.cpp: 52 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -1259,7 +1259,7 @@ size_t fresh::findShellKeyword(const std::vector<ShellWord> &words,size_t from,s
            i+=consumed-1;
            continue;
        }
        if(w=="if" || w=="for" || w=="while" || w=="case")
        if(w=="if" || w=="for" || w=="while" || w=="until" || w=="case")
            ++depth;
        else if(depth>0 && (w=="fi" || w=="done" || w=="esac"))
            --depth;
@@ -1285,7 +1285,7 @@ static bool isCompoundStart(const std::vector<fresh::ShellWord> &words,size_t at
    if(at>=words.size())
        return false;
    const std::string &w=words[at].text;
    return w=="if" || w=="for" || w=="while" || w=="case" || w=="{" || w=="!";
    return w=="if" || w=="for" || w=="while" || w=="until" || w=="case" || w=="{" || w=="!";
}

fresh::Shell::Shell(){
@@ -1879,7 +1879,8 @@ std::vector<std::string> fresh::splitPipelineStages(const std::string &segment,
    // "echo done" 's own "done" (cat's argument, not a real loop
    // terminator) can never desync this count.
    int keywordDepth=0;
    static const char *kwOpeners[]={"if","elif","while","for","case"};
    // "elif" is no opener: it continues an "if" that "fi" closes once
    static const char *kwOpeners[]={"if","while","until","for","case"};
    static const char *kwClosers[]={"fi","done","esac"};
    auto atKeywordStmtStart=[&](size_t pos)->bool{
        size_t p=pos;
@@ -1889,7 +1890,15 @@ std::vector<std::string> fresh::splitPipelineStages(const std::string &segment,
        char c=segment[p-1];
        if(c==';' || c=='\n' || c=='|') return true;
        if(c=='&' && p>=2 && segment[p-2]=='&') return true;
        return false;
        // right after a keyword that itself expects a command
        // ("do while ...", "then if ...", "! while ...")
        size_t wordEnd=p,wordStart=p;
        while(wordStart>0 && segment[wordStart-1]!=' ' && segment[wordStart-1]!='\t' &&
              segment[wordStart-1]!=';' && segment[wordStart-1]!='\n' && segment[wordStart-1]!='|')
            --wordStart;
        std::string word=segment.substr(wordStart,wordEnd-wordStart);
        return word=="do" || word=="then" || word=="else" || word=="elif" || word=="if" ||
               word=="while" || word=="until" || word=="!";
    };
    auto isWordBoundary=[&](size_t pos)->bool{
        return pos>=segment.size() || segment[pos]==' ' || segment[pos]=='\t' ||
@@ -2921,6 +2930,22 @@ int fresh::Shell::runSimple(const std::string &segment){
            stageRedirs.push_back(redir);
            continue;
        }
        // "... | while ...; done": a compound command stage is a body run
        // by parse() in the stage's child, like "{ }" above
        {
            size_t b=rawstages[i].find_first_not_of(" \t\r\n");
            std::string kw;
            if(b!=std::string::npos){
                size_t e=rawstages[i].find_first_of(" \t\r\n;",b);
                kw=rawstages[i].substr(b,e==std::string::npos ? std::string::npos : e-b);
            }
            if(kw=="while" || kw=="until" || kw=="for" || kw=="if" || kw=="case"){
                stageSubshellBody.push_back(rawstages[i]);
                stageTokens.push_back(std::vector<std::string>());
                stageRedirs.push_back(redir);
                continue;
            }
        }
        stageSubshellBody.push_back("");

        std::vector<std::string> tokens=tokenizeShellArgs(rawstages[i]);
@@ -3274,7 +3299,25 @@ int fresh::Shell::runSegmentWords(const std::string &input,const std::vector<She
        return (status==0) ? 1 : 0;
    }

    if(first=="if" || first=="for" || first=="while" || first=="case"){
    // "while ...; done | cmd", "{ ...; } | cmd": a compound command as
    // the first stage of a pipeline goes through runSimple(), which runs
    // each compound stage as a body of its own
    if(first=="if" || first=="for" || first=="while" || first=="until" || first=="case" || first=="{"){
        int d=0;
        for(size_t i=from;i<to;++i){
            const std::string &w=words[i].text;
            bool stmtStart=(i==from || words[i-1].text==";" || words[i-1].text=="&&" ||
                            words[i-1].text=="||" || words[i-1].text=="|" || words[i-1].text=="do" ||
                            words[i-1].text=="then" || words[i-1].text=="else" ||
                            words[i-1].text=="if" || words[i-1].text=="elif" ||
                            words[i-1].text=="while" || words[i-1].text=="until" || words[i-1].text=="!");
            if(stmtStart && (w=="if" || w=="for" || w=="while" || w=="until" || w=="case" || w=="{")) ++d;
            else if(stmtStart && d>0 && (w=="fi" || w=="done" || w=="esac" || w=="}")) --d;
            else if(d==0 && w=="|") return runSimple(shellWordsRawText(input,words,from,to));
        }
    }

    if(first=="if" || first=="for" || first=="while" || first=="until" || first=="case"){
        ShellCommand *cmd=find(first);
        if(!cmd){
            std::cerr << first << ": not supported" << std::endl;
@@ -3814,9 +3857,10 @@ int fresh::Shell::parse(const std::string &input,bool applyErrexit){
                           words[i-1].text=="&&" || words[i-1].text=="||" ||
                           words[i-1].text=="|" ||
                           words[i-1].text=="if" || words[i-1].text=="elif" ||
                           words[i-1].text=="while" ||
                           (i==segstart+1 && words[segstart].text=="!"));
        if(atStmtStart && (w=="if" || w=="for" || w=="while" || w=="case" || w=="{"))
                           words[i-1].text=="while" || words[i-1].text=="until" ||
                           words[i-1].text=="do" || words[i-1].text=="then" ||
                           words[i-1].text=="else" || words[i-1].text=="!");
        if(atStmtStart && (w=="if" || w=="for" || w=="while" || w=="until" || w=="case" || w=="{"))
            ++depth;
        else if(atStmtStart && depth>0 && (w=="fi" || w=="done" || w=="esac" || w=="}"))
            --depth;