#include "command.h" #include "phase3.h" #include "environment.h" #include "fdset.h" #include "builtins.h" #include "mpw-shell.h" #include "error.h" #include "value.h" #include #include #include #include #include #include #include #include "cxx/filesystem.h" #include "cxx/string_splitter.h" #include #include #include #include #include #include #if defined(__APPLE__) #include #endif extern std::atomic control_c; namespace fs = filesystem; extern fs::path mpw_path(); namespace ToolBox { std::string MacToUnix(const std::string path); std::string UnixToMac(const std::string path); } typedef std::vector token_vector; namespace { /* * returns: * <0 -> error * 0 -> false * >0 -> true */ int bad_if(const char *name) { fprintf(stderr, "### %s - Missing if keyword.\n", name); fprintf(stderr, "# Usage - %s [if expression...]\n", name); return -3; } int bad_exit() { fprintf(stderr, "### Exit - Missing if keyword.\n"); fprintf(stderr, "# Usage - Exit [Number] [if expression...]\n"); return -3; } int evaluate(int type, token_vector &&tokens, Environment &env) { std::reverse(tokens.begin(), tokens.end()); int32_t e; switch(type) { default: return 0; // exit [number] [if expr] ([number has been removed]) case EXIT: case BREAK: case CONTINUE: case ELSE: tokens.pop_back(); if (tokens.empty()) return 1; if (strcasecmp(tokens.back().string.c_str(), "if") != 0) { const char *name = ""; switch(type) { case BREAK: name = "Break"; break; case CONTINUE: name = "Continue"; break; case ELSE: name = "Else"; break; case EXIT: name = "Exit"; return bad_exit(); break; } return bad_if(name); } // fall through. case IF: tokens.pop_back(); try { e = evaluate_expression(env, "If", std::move(tokens)); } catch (std::exception &ex) { fprintf(stderr, "%s\n", ex.what()); return -5; } break; } return !!e; } bool is_script(const fs::path &path) { #if defined(__APPLE__) /* check for a file type of TEXT */ uint8_t finfo[32]; int ok = getxattr(path.c_str(), XATTR_FINDERINFO_NAME,finfo, sizeof(finfo), 0, 0); if (ok < 4) return false; if (memcmp(finfo, "TEXT", 4) == 0) return true; return false; #else return path.extension() == ".script"; #endif } } fs::path which(const Environment &env, const std::string &name) { std::error_code ec; if (name.find_first_of("/:") != name.npos) { // canonical? fs::path p(name); if (fs::exists(p, ec)) return name; return ""; } std::string s = env.get("commands"); for (string_splitter ss(s, ','); ss; ++ss) { fs::path p(ToolBox::MacToUnix(*ss)); p /= name; if (fs::exists(p, ec)) return p; } // error.. return ""; } void launch_mpw(const Environment &env, const std::vector &argv, const fdmask &fds) { std::vector cargv; cargv.reserve(argv.size() + 3); cargv.push_back((char *)"mpw"); cargv.push_back((char *)"--shell"); unsigned offset = cargv.size(); std::transform(argv.begin(), argv.end(), std::back_inserter(cargv), [](const std::string &s) { return strdup(s.c_str()); } ); cargv.push_back(nullptr); // export environment... for (const auto &kv : env) { if (kv.second) { // exported std::string name = "mpw$" + kv.first; setenv(name.c_str(), kv.second.c_str(), 1); } } // handle any indirection... fds.dup(); // re-set all signal handlers. /* * execv modifies handlers as such: * blocked -> blocked * ignored -> ignored * caught -> default action */ struct sigaction sig_action; sig_action.sa_handler = SIG_DFL; sig_action.sa_flags = 0; sigemptyset(&sig_action.sa_mask); for (int i = 1; i < NSIG; ++i) { sigaction(i, &sig_action, NULL); } execv(mpw_path().c_str(), cargv.data()); perror("execvp: "); exit(EX_OSERR); // raise a signal? } namespace { std::string &lowercase(std::string &s) { std::transform(s.begin(), s.end(), s.begin(), [](char c){ return std::tolower(c); }); return s; } std::unordered_map &, const fdmask &)> builtins = { {"aboutbox", builtin_aboutbox}, {"alias", builtin_alias}, {"catenate", builtin_catenate}, {"directory", builtin_directory}, {"echo", builtin_echo}, {"execute", builtin_execute}, {"exists", builtin_exists}, {"export", builtin_export}, {"help", builtin_help}, {"parameters", builtin_parameters}, {"quit", builtin_quit}, {"quote", builtin_quote}, {"set", builtin_set}, {"shift", builtin_shift}, {"unalias", builtin_unalias}, {"unexport", builtin_unexport}, {"unset", builtin_unset}, {"version", builtin_version}, {"which", builtin_which}, // not in MPW. {"true", builtin_true}, {"false", builtin_false}, }; int execute_external(const Environment &env, const std::vector &argv, const fdmask &fds) { int status; int pid; struct sigaction ign, intact, quitact; sigset_t newsigblock, oldsigblock; sigemptyset(&newsigblock); sigaddset(&newsigblock, SIGCHLD); sigaddset(&newsigblock, SIGINT); sigaddset(&newsigblock, SIGQUIT); sigprocmask(SIG_BLOCK, &newsigblock, &oldsigblock); pid = fork(); if (pid < 0) { perror("fork: "); exit(EX_OSERR); } if (pid == 0) { //sigprocmask(SIG_SETMASK, &oldsigblock, NULL); launch_mpw(env, argv, fds); } /* ignore int/quit while waiting on child */ memset(&ign, 0, sizeof(ign)); ign.sa_handler = SIG_IGN; sigemptyset(&ign.sa_mask); sigaction(SIGINT, &ign, &intact); sigaction(SIGQUIT, &ign, &quitact); for(;;) { pid_t ok; ok = waitpid(pid, &status, 0); if (ok < 0) { if (errno == EINTR) continue; perror("waitpid:"); exit(EX_OSERR); } break; } sigaction(SIGINT, &intact, NULL); sigaction(SIGQUIT, &quitact, NULL); sigprocmask(SIG_SETMASK, &oldsigblock, NULL); if (WIFEXITED(status)) { return WEXITSTATUS(status); } if (WIFSIGNALED(status)) { return -9; // command-. user abort exits via -9. } fprintf(stderr, "waitpid - unexpected result\n"); exit(EX_OSERR); } } command::~command() {} /* * todo -- all errors should be handled the same, regardless of source. (throw, parse, etc). * echo and error should respect the active fdmask. */ template int exec(std::string command, Environment &env, const fdmask &fds, bool throwup, F &&fx) { bool echo = true; int rv = 0; if (control_c) throw execution_of_input_terminated(); try { process p; command = expand_vars(command, env, fds); auto tokens = tokenize(command, false); if (tokens.empty()) return 0; parse_tokens(std::move(tokens), p); env.echo("%s", command.c_str()); echo = false; if (p.arguments.empty()) return env.status(0); rv = fx(p); } catch (mpw_error &e) { if (echo) env.echo("%s", command.c_str()); fprintf(stderr, "### %s\n", e.what()); return env.status(e.status(), throwup); } catch (std::exception &e) { if (echo) env.echo("%s", command.c_str()); fprintf(stderr, "### %s\n", e.what()); return env.status(-4, throwup); } return env.status(rv, throwup); } int simple_command::execute(Environment &env, const fdmask &fds, bool throwup) { return exec(text, env, fds, throwup, [&](process &p){ fdmask newfds = p.fds | fds; std::string name = p.arguments.front(); lowercase(name); auto iter = builtins.find(name); if (iter != builtins.end()) { env.set("command", name); int status = iter->second(env, p.arguments, newfds); return status; } if (env.startup()) { fprintf(stderr, "### MPW Shell - startup file may not contain external commands.\n"); return 0; } name = p.arguments.front(); fs::path path = which(env, name); if (path.empty()) { std::string error = "MPW Shell - Command \"" + name + "\" was not found."; throw mpw_error(-1, error); //fprintf(stderr, "### MPW Shell - Command \"%s\" was not found.\n", name.c_str()); //return -1; } if (is_script(path)) { // scripts run with an isolated environment. Environment new_env = env.subshell_environment(); new_env.set("command", path); new_env.set_argv(p.arguments); try { return read_file(new_env, path, newfds); } catch (const exit_command_t &ex) { return ex.value; } } if (env.test()) return 0; env.set("command", path); p.arguments[0] = path; return execute_external(env, p.arguments, newfds); }); } template int eval_exec(std::string command, Environment &env, const fdmask &fds, bool throwup, F &&fx){ std::string name; bool echo = true; int rv = 0; if (control_c) throw execution_of_input_terminated(); try { command = expand_vars(command, env, fds); auto tokens = tokenize(command, true); if (tokens.empty()) return 0; env.echo("%s", command.c_str()); echo = false; name = tokens[0].string; rv = fx(tokens); } catch (const exit_command_t &ex) { // convert to execution of input terminated. throw execution_of_input_terminated(ex.value); } catch (mpw_error &e) { if (echo) env.echo("%s", command.c_str()); fprintf(stderr, "### %s\n", e.what()); return env.status(e.status(), throwup); } catch(std::exception &e) { // these should include the argv0 name. if (echo) env.echo("%s", command.c_str()); fprintf(stderr, "### %s - %s\n", name.c_str(), e.what()); return env.status(-4, throwup); } return env.status(rv, throwup); } int evaluate_command::execute(Environment &env, const fdmask &fds, bool throwup) { return eval_exec(text, env, fds, throwup, [&](token_vector &tokens){ env.set("command", "evaluate"); return builtin_evaluate(env, std::move(tokens), fds); }); } int break_command::execute(Environment &env, const fdmask &fds, bool throwup) { return eval_exec(text, env, fds, throwup, [&](token_vector &tokens){ env.set("command", "break"); if (!env.loop()) throw break_error(); int status = evaluate(BREAK, std::move(tokens), env); if (status > 0) throw break_command_t(); return status; }); } int continue_command::execute(Environment &env, const fdmask &fds, bool throwup) { return eval_exec(text, env, fds, throwup, [&](token_vector &tokens){ env.set("command", "continue"); if (!env.loop()) throw continue_error(); int status = evaluate(CONTINUE, std::move(tokens), env); if (status > 0) throw continue_command_t(); return status; }); } int exit_command::execute(Environment &env, const fdmask &fds, bool throwup) { // exit // exit [number] [if expr ]] // todo -- return eval_exec(text, env, fds, throwup, [&](token_vector &tokens){ env.set("command", "exit"); env.status(0); if (tokens.size() <= 1) { throw exit_command_t{}; } int status = 0; value v(tokens[1]); // remove the return value to make processing easier. if (v.is_number()) { tokens.erase(tokens.begin() + 1); } status = evaluate(EXIT, std::move(tokens), env); if (status) { int ok = v.to_number(0); throw exit_command_t{ok}; } return 0; }); } int or_command::execute(Environment &e, const fdmask &fds, bool throwup) { int rv = 0; for (auto &c : children) { if (!c) continue; rv = c->execute(e, fds, false); if (rv == 0) return rv; } return e.status(rv, throwup); } int and_command::execute(Environment &e, const fdmask &fds, bool throwup) { // mpw - false && true -> no exit // mpw - true && false -> exit int rv = 0; for (auto &c : children) { if (!c) continue; rv = c->execute(e, fds, false); if (rv != 0) return rv; } return e.status(rv, throwup); } int pipe_command::execute(Environment &e, const fdmask &fds, bool throwup) { // not yet supported! //fputs( "### MPW Shell - Pipes are not yet supported.\n", stderr); //return e.status(1, throwup); if (children[0] && children[1]) { int fd; int rv = 0; char temp[32] = "/tmp/mpw-shell-XXXXXXXX"; fdset pipe_fd; fd = mkstemp(temp); unlink(temp); pipe_fd.set(1, fd); rv = children[0]->execute(e, pipe_fd | fds, throwup); // fdset will close the fd ... pipe_fd.swap_in_out(); lseek(fd, 0, SEEK_SET); rv = children[1]->execute(e, pipe_fd | fds, throwup); return e.status(rv, throwup); } if (children[0]) return children[0]->execute(e, fds, throwup); if (children[1]) return children[1]->execute(e, fds, throwup); return e.status(0, throwup); } int vector_command::execute(Environment &e, const fdmask &fds, bool throwup) { int rv = 0; for (auto &c : children) { if (!c) continue; rv = c->execute(e, fds); } return e.status(rv); } int error_command::execute(Environment &e, const fdmask &fds, bool throwup) { if (control_c) throw execution_of_input_terminated(); if (type == ERROR) { fprintf(stderr, "%s\n", text.c_str()); return e.status(-3); } std::string s = expand_vars(text, e, fds); e.echo("%s", s.c_str()); switch(type) { case END: fprintf(stderr, "### MPW Shell - Extra END command.\n"); break; case RPAREN: fprintf(stderr, "### MPW Shell - Extra ) command.\n"); break; case ELSE: case ELSE_IF: fprintf(stderr, "### MPW Shell - ELSE must be within IF ... END.\n"); break; } return e.status(-3); } template int begin_end_exec(std::string begin, std::string end, Environment &env, const fdmask &fds, bool throwup, F &&fx) { bool echo = true; int rv = 0; if (control_c) throw execution_of_input_terminated(); try { process p; begin = expand_vars(begin, env, fds); end = expand_vars(end, env, fds); auto b = tokenize(begin, true); auto e = tokenize(end, false); parse_tokens(std::move(e), p); if (echo) env.echo("%s ... %s", begin.c_str(), end.c_str() ); rv = fx(b, p); } catch (execution_of_input_terminated &e) { // pass through. throw; } catch (mpw_error &e) { if (echo) env.echo("%s ... %s", begin.c_str(), end.c_str() ); fprintf(stderr, "### %s\n", e.what()); return env.status(e.status(), throwup); } catch (std::exception &e) { if (echo) env.echo("%s ... %s", begin.c_str(), end.c_str() ); fprintf(stderr, "### %s\n", e.what()); return env.status(-4, throwup); } return env.status(rv, throwup); } int begin_command::execute(Environment &env, const fdmask &fds, bool throwup) { return begin_end_exec(begin, end, env, fds, throwup, [&](token_vector &b, process &p){ env.set("command", type == BEGIN ? "end" : ")"); if (b.size() != 1) { fprintf(stderr, "### Begin - Too many parameters were specified.\n"); fprintf(stderr, "Usage - Begin\n"); return -3; } fdmask newfds = p.fds | fds; int rv; env.indent_and([&]{ rv = vector_command::execute(env, newfds); }); env.echo("%s", type == BEGIN ? "end" : ")"); return rv; }); } int loop_command::execute(Environment &env, const fdmask &fds, bool throwup) { return begin_end_exec(begin, end, env, fds, throwup, [&](token_vector &b, process &p){ env.set("command", "end"); if (b.size() != 1) { fprintf(stderr, "### Loop - Too many parameters were specified.\n"); fprintf(stderr, "Usage - Loop\n"); return -3; } fdmask newfds = p.fds | fds; int rv = 0; for(;;) { if (control_c) throw execution_of_input_terminated(); try { env.loop_indent_and([&]{ rv = vector_command::execute(env, newfds); }); } catch (break_command_t &ex) { env.echo("end"); break; } catch (continue_command_t &ex) {} env.echo("end"); } return rv; }); } int for_command::execute(Environment &env, const fdmask &fds, bool throwup) { return begin_end_exec(begin, end, env, fds, throwup, [&](token_vector &b, process &p){ env.set("command", "end"); if (b.size() < 3 || strcasecmp(b[2].string.c_str(), "in")) { fprintf(stderr, "### For - Missing in keyword.\n"); fprintf(stderr, "Usage - For name in [word...]\n"); return -3; } fdmask newfds = p.fds | fds; int rv = 0; for (int i = 3; i < b.size(); ++i ) { if (control_c) throw execution_of_input_terminated(); env.set(b[1].string, b[i].string); try { env.loop_indent_and([&]{ rv = vector_command::execute(env, newfds); }); } catch (break_command_t &ex) { env.echo("end"); break; } catch (continue_command_t &ex) {} env.echo("end"); } return rv; }); } /* * the entire command prints even if there is an error with the expression. * */ int if_command::execute(Environment &env, const fdmask &fds, bool throwup) { int rv = 0; int error = 0; bool skip = false; bool first = true; fdmask newfds; for (auto &c : clauses) { int tmp; if (first) { first = false; tmp = begin_end_exec(c->clause, end, env, fds, false, [&](token_vector &b, process &p){ newfds = p.fds | fds; int status = evaluate(c->type, std::move(b), env); if (status < 0) { error = status; } if (status > 0) { skip = true; env.indent_and([&](){ rv = c->execute(env, newfds); }); } return 0; }); if (tmp != 0) error = tmp; continue; } else { // second... tmp = eval_exec(c->clause, env, fds, false, [&](token_vector &b){ if (skip || error) return 0; int status = evaluate(c->type, std::move(b), env); if (status < 0) { error = status; } if (status > 0) { skip = true; env.indent_and([&](){ rv = c->execute(env, newfds); }); } return 0; }); if (tmp != 0 && !skip) error = tmp; } } env.echo("end"); if (error) return env.status(error, throwup); return env.status(rv, throwup); }