Commit 38d13c41 authored by jan.koester's avatar jan.koester
Browse files

extended commands

parent 4d6f7b72
Loading
Loading
Loading
Loading
+1 −0
Changes for commands/shell/CMakeLists.txt: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ target_sources(fresh PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}/echo.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/printf.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/export.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/local.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/variables.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/unset.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/read.cpp
+69 −0
Changes for commands/shell/local.cpp: 69 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2022, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/


#include <string>
#include <iostream>

#include "shell.h"
#include "envoirment.h"

namespace fresh { namespace cmd {

    // local [-r|-x|...] NAME[=VALUE]... -- variables scoped to the running
    // function call (Shell::declareLocal()). Without a value the variable
    // starts out unset, as in bash and dash.
    class LocalCommand : public ShellCommand {
    public:
        LocalCommand():ShellCommand("local"){}

        int runCommand(int argc,const char **argv){
            Shell &sh=Shell::instance();
            Environment &env=Environment::instance();
            int status=0;
            for(int i=1;i<argc;i++){
                std::string arg=argv[i];
                if(arg.size()>1 && arg[0]=='-' && arg.find('=')==std::string::npos)
                    continue; // attribute flags (-r, -x, -i, ...): accepted, not tracked
                size_t eq=arg.find('=');
                std::string name=eq==std::string::npos ? arg : arg.substr(0,eq);
                if(!sh.declareLocal(name)){
                    std::cerr << "fresh: local: can only be used in a function" << std::endl;
                    return 1;
                }
                if(eq!=std::string::npos)
                    env.setVariable(name,arg.substr(eq+1));
                else
                    env.unsetVariable(name);
            }
            return status;
        }
    };

    FRESH_REGISTER_COMMAND(LocalCommand);

}}
+114 −57
Changes for commands/shell/read.cpp: 114 added lines, 57 removed lines.
Original line number Diff line number Diff line
@@ -52,6 +52,10 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include <unistd.h>

#include <poll.h>
#include <termios.h>
#include <cstring>
#include <cstdlib>
#include "shell.h"
#include "envoirment.h"

@@ -61,96 +65,149 @@ namespace fresh { namespace cmd {
    public:
        ReadCommand():ShellCommand("read"){}

        // read [-r] [-s] [-d DELIM] [-n N] [-p PROMPT] [-t SECS] [-u FD] [NAME...]
        // Fields are split at $IFS (default: space, tab, newline); "IFS= read"
        // keeps the line exactly, "-d ''" reads up to a NUL byte (the
        // "find -print0 | while read -r -d '' f" idiom).
        int runCommand(int argc,const char **argv){
            bool raw=false;
            bool raw=false,silent=false;
            char delim='\n';
            long maxChars=-1;
            double timeout=-1;
            int fd=0;
            std::string prompt;
            int argi=1;
            for(; argi<argc; ++argi){
                std::string a=argv[argi];
                if(a=="--"){ ++argi; break; }
                if(a=="-r"){ raw=true; continue; }
                if(!a.empty() && a[0]=='-' && a!="-"){
                    std::cerr << "fresh: read: " << a << ": unsupported option" << std::endl;
                if(a.size()<2 || a[0]!='-') break;
                bool stop=false;
                for(size_t k=1;k<a.size() && !stop;++k){
                    char c=a[k];
                    if(c=='r'){ raw=true; continue; }
                    if(c=='s'){ silent=true; continue; }
                    if(strchr("dnptu",c)){
                        std::string v=a.substr(k+1);
                        if(v.empty() && k+1==a.size()){
                            if(argi+1>=argc){
                                std::cerr << "fresh: read: -" << c << ": option requires an argument" << std::endl;
                                return 2;
                            }
                            v=argv[++argi];
                        }
                        switch(c){
                            case 'd': delim=v.empty() ? '\0' : v[0]; break;
                            case 'n': maxChars=atol(v.c_str()); break;
                            case 'p': prompt=v; break;
                            case 't': timeout=atof(v.c_str()); break;
                            case 'u': fd=atoi(v.c_str()); break;
                        }
                        stop=true;
                        continue;
                    }
                    std::cerr << "fresh: read: -" << c << ": invalid option" << std::endl;
                    return 2;
                }
                break;
            }

            std::vector<std::string> names;
            for(; argi<argc; ++argi)
                names.push_back(argv[argi]);
            if(names.empty())
            bool reply=names.empty();
            if(reply)
                names.push_back("REPLY");

            // POSIX: "\<newline>" is a line continuation removed entirely
            // (both characters dropped, the logical line keeps going); any
            // other "\X" has just the backslash removed, keeping X literal
            // -- unless "-r" was given, in which case backslash is fully
            // ordinary input. A trailing, unpaired backslash right at EOF
            // is kept as-is, matching bash.
            if(!prompt.empty() && isatty(fd)){
                std::cerr << prompt;
                std::cerr.flush();
            }
            struct termios saved;
            bool restoreTerm=false;
            if(silent && isatty(fd) && tcgetattr(fd,&saved)==0){
                struct termios t=saved;
                t.c_lflag&=~(ECHO);
                tcsetattr(fd,TCSANOW,&t);
                restoreTerm=true;
            }

            // POSIX: "\<newline>" is a line continuation removed entirely;
            // any other "\X" keeps X literal -- unless -r was given, in
            // which case backslash is ordinary input.
            std::string line;
            bool sawEOF=false;
            bool sawEOF=false,timedOut=false;
            long count=0;
            auto getc=[&](char &c)->int{
                if(timeout>=0){
                    struct pollfd p;
                    p.fd=fd; p.events=POLLIN; p.revents=0;
                    int r;
                    do{ r=poll(&p,1,(int)(timeout*1000)); }while(r<0 && errno==EINTR);
                    if(r==0){ timedOut=true; return 0; }
                }
                ssize_t n;
                do{ n=::read(fd,&c,1); }while(n<0 && errno==EINTR);
                return n>0 ? 1 : 0;
            };
            for(;;){
                if(maxChars>=0 && count>=maxChars) break;
                char c;
                ssize_t n;
                do{
                    n=::read(0,&c,1);
                }while(n<0 && errno==EINTR);

                if(n<=0){
                    sawEOF=true;
                    break;
                }
                if(c=='\n')
                    break;

                if(!getc(c)){ sawEOF=true; break; }
                if(c==delim) break;
                if(!raw && c=='\\'){
                    char next;
                    ssize_t n2;
                    do{
                        n2=::read(0,&next,1);
                    }while(n2<0 && errno==EINTR);
                    if(n2<=0){
                        line+=c;
                        sawEOF=true;
                        break;
                    }
                    if(next=='\n')
                        continue;
                    if(!getc(next)){ line+=c; sawEOF=true; break; }
                    if(next=='\n') continue;
                    line+=next;
                    ++count;
                    continue;
                }

                line+=c;
                ++count;
            }
            if(restoreTerm){
                tcsetattr(fd,TCSANOW,&saved);
                std::cerr << std::endl;
            }

            // Split on plain whitespace -- fresh has no real $IFS variable
            // anywhere else either (word-splitting elsewhere in this shell
            // is hardcoded to space/tab/newline too), so this matches the
            // rest of the shell rather than adding IFS support read alone
            // would need. The last name gets whatever remains of the line
            // (internal spacing preserved, only its own trailing
            // whitespace trimmed) -- standard read behavior for
            // "read a b" against more than two fields.
            Environment &env=Environment::instance();
            std::string ifs=" \t\n";
            bool ifsSet=env.getVariable("IFS",ifs);
            if(!ifsSet) ifs=" \t\n";

            if(reply){
                // plain "read": REPLY gets the line as is
                env.setVariable("REPLY",line);
            }else if(ifs.empty()){
                // IFS empty: no splitting, no trimming
                env.setVariable(names[0],line);
                for(size_t k=1;k<names.size();++k) env.setVariable(names[k],"");
            }else{
                auto isIfs=[&](char c){ return ifs.find(c)!=std::string::npos; };
                auto isIfsWs=[&](char c){ return (c==' ' || c=='\t' || c=='\n') && isIfs(c); };
                size_t i=0,len=line.size();
            while(i<len && (line[i]==' ' || line[i]=='\t'))
                ++i;
                while(i<len && isIfsWs(line[i])) ++i;
                for(size_t k=0;k<names.size();++k){
                bool last=(k+1==names.size());
                if(last){
                    if(k+1==names.size()){
                        // the last name gets the rest, trailing IFS whitespace trimmed
                        size_t end=len;
                    while(end>i && (line[end-1]==' ' || line[end-1]=='\t'))
                        --end;
                    Environment::instance().setVariable(names[k],line.substr(i,end-i));
                        while(end>i && isIfsWs(line[end-1])) --end;
                        env.setVariable(names[k],line.substr(i,end-i));
                        break;
                    }
                    size_t start=i;
                while(i<len && line[i]!=' ' && line[i]!='\t')
                    ++i;
                Environment::instance().setVariable(names[k],line.substr(start,i-start));
                while(i<len && (line[i]==' ' || line[i]=='\t'))
                    while(i<len && !isIfs(line[i])) ++i;
                    env.setVariable(names[k],line.substr(start,i-start));
                    // one non-whitespace separator (plus surrounding whitespace)
                    while(i<len && isIfsWs(line[i])) ++i;
                    if(i<len && isIfs(line[i]) && !isIfsWs(line[i])){
                        ++i;
                        while(i<len && isIfsWs(line[i])) ++i;
                    }

                }
            }
            if(timedOut) return 142;
            // like bash: EOF before the delimiter is a failure (the variables
            // still get what was read)
            return sawEOF ? 1 : 0;
        }
    };
+28 −0
Changes for src/shell.cpp: 28 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -1462,7 +1462,19 @@ int fresh::Shell::callFunction(const std::string &name,const std::vector<std::st
    _loopDepth=0;

    ++_functionDepth;
    _localFrames.emplace_back();
    int status=parse(it->second);
    // undo this call's "local"s, newest first
    std::vector<LocalSave> frame=std::move(_localFrames.back());
    _localFrames.pop_back();
    for(auto l=frame.rbegin(); l!=frame.rend(); ++l){
        if(l->wasSet){
            env.setVariable(l->name,l->value);
            if(l->wasExported) env.exportVariable(l->name);
        }else{
            env.unsetVariable(l->name);
        }
    }
    --_functionDepth;

    _loopDepth=savedLoopDepth;
@@ -1479,6 +1491,22 @@ int fresh::Shell::callFunction(const std::string &name,const std::vector<std::st
    return status;
}

bool fresh::Shell::declareLocal(const std::string &name){
    if(_localFrames.empty())
        return false;
    std::vector<LocalSave> &frame=_localFrames.back();
    for(auto &l: frame)
        if(l.name==name)
            return true; // already local in this call
    Environment &env=Environment::instance();
    LocalSave save;
    save.name=name;
    save.wasSet=env.getVariable(name,save.value);
    save.wasExported=save.wasSet && env.isExported(name);
    frame.push_back(save);
    return true;
}

fresh::Shell::~Shell(){
}

+13 −0
Changes for src/shell.h: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -482,6 +482,11 @@ namespace fresh {
        // via "return" -- this is the one place that ever does so.
        int callFunction(const std::string &name,const std::vector<std::string> &args);

        // "local NAME[=VALUE]": makes NAME local to the innermost running
        // function call -- its previous state (value, set or not, exported)
        // is restored when that call returns. False outside any function.
        bool declareLocal(const std::string &name);

        // Top-level input lines only (not sub-fragments run recursively by
        // if/for/while), recorded by main()'s REPL/script loop so the
        // "history" builtin has something to show.
@@ -644,6 +649,14 @@ namespace fresh {
        // name -> raw compound-command source text, see
        // collectFunctionDefinitions()'s own comment.
        std::map<std::string,std::string> _functions;
        struct LocalSave {
            std::string name;
            bool wasSet;
            std::string value;
            bool wasExported;
        };
        // one frame per running callFunction(), innermost last
        std::vector<std::vector<LocalSave>> _localFrames;
        bool _returning;
        int _returnCode;
        // Depth of nested callFunction() calls currently on the (real,