| /* sh.c - toybox shell |
| * |
| * Copyright 2006 Rob Landley <rob@landley.net> |
| * |
| * The POSIX-2008/SUSv4 spec for this is at: |
| * http://opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html |
| * and http://opengroup.org/onlinepubs/9699919799/utilities/sh.html |
| * |
| * The first link describes the following shell builtins: |
| * |
| * break colon continue dot eval exec exit export readonly return set shift |
| * times trap unset |
| * |
| * The second link (the utilities directory) also contains specs for the |
| * following shell builtins: |
| * |
| * alias bg cd command fc fg getopts hash jobs kill read type ulimit |
| * umask unalias wait |
| * |
| * Things like the bash man page are good to read too. |
| * |
| * TODO: "make sh" doesn't work (nofork builtins need to be included) |
| * TODO: test that $PS1 color changes work without stupid \[ \] hack |
| * TODO: make fake pty wrapper for test infrastructure |
| * TODO: // Handle embedded NUL bytes in the command line. |
| * TODO: var=val command |
| * existing but considered builtins: false kill pwd true time |
| * buitins: alias bg command fc fg getopts jobs newgrp read umask unalias wait |
| * "special" builtins: break continue : . eval exec export readonly return set |
| * shift times trap unset |
| * | & ; < > ( ) $ ` \ " ' <space> <tab> <newline> |
| * * ? [ # ~ = % |
| * ! { } case do done elif else esac fi for if in then until while |
| * [[ ]] function select |
| * $@ $* $# $? $- $$ $! $0 |
| * ENV HOME IFS LANG LC_ALL LINENO PATH PPID PS1 PS2 PS4 PWD |
| * label: |
| * TODO: test exit from "trap EXIT" doesn't recurse |
| * TODO: ! history expansion |
| * |
| * bash man page: |
| * control operators || & && ; ;; ;& ;;& ( ) | |& <newline> |
| * reserved words |
| * ! case coproc do done elif else esac fi for function if in select |
| * then until while { } time [[ ]] |
| |
| |
| |
| USE_SH(NEWTOY(cd, NULL, TOYFLAG_NOFORK)) |
| USE_SH(NEWTOY(exit, NULL, TOYFLAG_NOFORK)) |
| |
| USE_SH(NEWTOY(sh, "c:i", TOYFLAG_BIN)) |
| USE_SH(OLDTOY(toysh, sh, TOYFLAG_BIN)) |
| USE_SH(OLDTOY(bash, sh, TOYFLAG_BIN)) |
| // Login lies in argv[0], so add some aliases to catch that |
| USE_SH(OLDTOY(-sh, sh, 0)) |
| USE_SH(OLDTOY(-toysh, sh, 0)) |
| USE_SH(OLDTOY(-bash, sh, 0)) |
| |
| config SH |
| bool "sh (toysh)" |
| default n |
| help |
| usage: sh [-c command] [script] |
| |
| Command shell. Runs a shell script, or reads input interactively |
| and responds to it. |
| |
| -c command line to execute |
| -i interactive mode (default when STDIN is a tty) |
| |
| # These are here for the help text, they're not selectable and control nothing |
| config CD |
| bool |
| default n |
| depends on SH |
| help |
| usage: cd [-PL] [path] |
| |
| Change current directory. With no arguments, go $HOME. |
| |
| -P Physical path: resolve symlinks in path |
| -L Local path: .. trims directories off $PWD (default) |
| |
| config EXIT |
| bool |
| default n |
| depends on SH |
| help |
| usage: exit [status] |
| |
| Exit shell. If no return value supplied on command line, use value |
| of most recent command, or 0 if none. |
| */ |
| |
| #define FOR_sh |
| #include "toys.h" |
| |
| GLOBALS( |
| char *command; |
| |
| long lineno; |
| |
| struct double_list functions; |
| unsigned options; |
| |
| // Running jobs. |
| struct sh_job { |
| struct sh_job *next, *prev; |
| unsigned jobno; |
| |
| // Every pipeline has at least one set of arguments or it's Not A Thing |
| struct sh_arg { |
| char **v; |
| int c; |
| } pipeline; |
| |
| // null terminated array of running processes in pipeline |
| struct sh_process { |
| struct string_list *delete; // expanded strings |
| int pid, exit; // status? Stopped? Exited? |
| struct sh_arg arg; |
| } *procs, *proc; |
| } *jobs, *job; |
| unsigned jobcnt; |
| ) |
| |
| #define SH_NOCLOBBER 1 // set -C |
| |
| void cd_main(void) |
| { |
| char *dest = *toys.optargs ? *toys.optargs : getenv("HOME"); |
| |
| // TODO: -LPE@ |
| // TODO: cd .. goes up $PWD path we used to get here, not ./.. |
| xchdir(dest ? dest : "/"); |
| } |
| |
| void exit_main(void) |
| { |
| exit(*toys.optargs ? atoi(*toys.optargs) : 0); |
| } |
| |
| // like error_msg() but exit from shell scripts |
| void syntax_err(char *msg, ...) |
| { |
| va_list va; |
| |
| va_start(va, msg); |
| verror_msg(msg, 0, va); |
| va_end(va); |
| |
| if (*toys.optargs) xexit(); |
| } |
| |
| // Print prompt, parsing escapes |
| static void do_prompt(char *prompt) |
| { |
| char *s, c, cc; |
| |
| if (!prompt) prompt = "\\$ "; |
| while (*prompt) { |
| c = *(prompt++); |
| |
| if (c=='!') { |
| if (*prompt=='!') prompt++; |
| else { |
| printf("%ld", TT.lineno); |
| continue; |
| } |
| } else if (c=='\\') { |
| int i = 0; |
| |
| cc = *(prompt++); |
| if (!cc) goto down; |
| |
| // \nnn \dD{}hHjlstT@AuvVwW!#$ |
| // Ignore bash's "nonprintable" hack; query our cursor position instead. |
| if (cc=='[' || cc==']') continue; |
| else if (cc=='$') putchar(getuid() ? '$' : '#'); |
| else if (cc=='h' || cc=='H') { |
| *toybuf = 0; |
| gethostname(toybuf, sizeof(toybuf)-1); |
| if (cc=='h' && (s = strchr(toybuf, '.'))) *s = 0; |
| fputs(toybuf, stdout); |
| } else if (cc=='s') fputs(getbasename(*toys.argv), stdout); |
| else { |
| if (!(c = unescape(cc))) { |
| c = '\\'; |
| prompt--; |
| } |
| i++; |
| } |
| if (!i) continue; |
| } |
| down: |
| putchar(c); |
| } |
| fflush(stdout); |
| } |
| |
| // quote removal, brace, tilde, parameter/variable, $(command), |
| // $((arithmetic)), split, path |
| #define NO_PATH (1<<0) |
| #define NO_SPLIT (1<<1) |
| // TODO: ${name:?error} causes an error/abort here (syntax_err longjmp?) |
| // TODO: $1 $@ $* need args marshalled down here: function+structure? |
| // arg = append to this |
| // new = string to expand |
| // flags = type of expansions (not) to do |
| // delete = append new allocations to this so they can be freed later |
| // TODO: at_args: $1 $2 $3 $* $@ |
| static void expand_arg(struct sh_arg *arg, char *new, unsigned flags, |
| struct string_list **delete) |
| { |
| if (!(arg->c&32)) arg->v = xrealloc(arg->v, sizeof(void *)*(arg->c+33)); |
| |
| arg->v[arg->c++] = new; |
| arg->v[arg->c] = 0; |
| |
| /* |
| char *s = word, *new = 0; |
| |
| // replacement |
| while (*s) { |
| if (*s == '$') { |
| s++; |
| } else if (*strchr("*?[{", *s)) { |
| s++; |
| } else if (*s == '<' || *s == '>') { |
| s++; |
| } else s++; |
| } |
| |
| return new; |
| */ |
| } |
| |
| // Assign one variable |
| // s: key=val |
| // type: 0 = whatever it was before, local otherwise |
| #define TAKE_MEM 0x80000000 |
| // declare -aAilnrux |
| // ft |
| void setvar(char *s, unsigned type) |
| { |
| if (type&TAKE_MEM) type ^= TAKE_MEM; |
| else s = xstrdup(s); |
| |
| // local, export, readonly, integer... |
| xsetenv(s, 0); |
| } |
| |
| char *getvar(char *s) |
| { |
| return getenv(s); |
| } |
| |
| // return length of match found at this point |
| static int anystart(char *s, char **try) |
| { |
| while (*try) { |
| if (strstart(&s, *try)) return strlen(*try); |
| try++; |
| } |
| |
| return 0; |
| } |
| |
| static int anystr(char *s, char **try) |
| { |
| while (*try) if (!strcmp(s, *try++)) return 1; |
| |
| return 0; |
| } |
| |
| // return length of valid prefix that could go before redirect |
| int redir_prefix(char *word) |
| { |
| char *s = word; |
| |
| if (*s == '{') { |
| for (s++; isalnum(*s) || *s=='_'; s++); |
| if (*s == '}' && s != word+1) s++; |
| else s = word; |
| } else while (isdigit(*s)) s++; |
| |
| return s-word; |
| } |
| |
| // TODO |& |
| |
| // rd[0] = next, 1 = prev, 2 = len, 3-x = to/from redirection pairs. |
| // Execute the commands in a pipeline segment |
| struct sh_process *run_command(struct sh_arg *arg, int **rdlist) |
| { |
| struct sh_process *pp = xzalloc(sizeof(struct sh_process)); |
| struct toy_list *tl; |
| char *s, *ss, *sss; |
| unsigned envlen, j; |
| int fd, here = 0, rdcount = 0, *rd = 0, *rr, hfd = 0; |
| |
| // Grab variable assignments |
| for (envlen = 0; envlen<arg->c; envlen++) { |
| s = arg->v[envlen]; |
| for (j=0; s[j] && (s[j]=='_' || !ispunct(s[j])); j++); |
| if (!j || s[j] != '=') break; |
| } |
| |
| // perform assignments locally if there's no command |
| if (envlen == arg->c) { |
| for (j = 0; j<envlen; j++) { |
| struct sh_arg aa; |
| |
| aa.c = 0; |
| expand_arg(&aa, arg->v[j], NO_PATH|NO_SPLIT, 0); |
| setvar(*aa.v, TAKE_MEM); |
| free(aa.v); |
| } |
| free(pp); |
| |
| return 0; |
| } |
| |
| // We vfork() instead of fork to support nommu systems, and do |
| // redirection setup in the parent process. Open new filehandles |
| // and move them to temporary values >10. The rd[] array has pairs of |
| // filehandles: child replaces fd1 with fd2 via dup2() and close() after |
| // the vfork(). fd2 is <<1, if bottom bit set don't close it (dup instead). |
| // If fd2 < 0 it's a here document (parent process writes to a pipe later). |
| |
| // Expand arguments and perform redirections |
| for (j = envlen; j<arg->c; j++) { |
| |
| // Is this a redirect? |
| ss = (s = arg->v[j]) + redir_prefix(arg->v[j]); |
| if (!anystr(ss, (char *[]){"<<<", "<<-", "<<", "<&", "<>", "<", ">>", ">&", |
| ">|", ">", "&>>", "&>", 0})) |
| { |
| // Nope: save/expand argument and loop |
| expand_arg(&pp->arg, s, 0, 0); |
| |
| continue; |
| } |
| |
| // Yes. Expand rd[] and find first unused filehandle >10 |
| if (!(rdcount&31)) { |
| if (rd) dlist_lpop((void *)rdlist); |
| rd = xrealloc(rd, (2*rdcount+3+2*32)*sizeof(int *)); |
| dlist_add_nomalloc((void *)rdlist, (void *)rd); |
| } |
| rr = rd+3+rdcount; |
| if (!hfd) |
| for (hfd = 10; hfd<99999; hfd++) if (-1 == fcntl(hfd, F_GETFL)) break; |
| |
| // error check: premature EOF, target fd too high, or redirect file splits |
| if (++j == arg->c || (isdigit(*s) && ss-s>5)) goto flush; |
| fd = pp->arg.c; |
| |
| // expand arguments for everything but << and <<- |
| if (strncmp(ss, "<<", 2) || ss[2] == '<') { |
| expand_arg(&pp->arg, arg->v[j], NO_PATH|(NO_SPLIT*!strcmp(ss, "<<<")), 0); |
| if (fd+1 != pp->arg.c) goto flush; |
| sss = pp->arg.v[--pp->arg.c]; |
| } else dlist_add((void *)&pp->delete, sss = xstrdup(arg->v[j])); |
| |
| // rd[] entries come in pairs: first is which fd gets redirected after |
| // vfork(), I.E. the [n] part of [n]<word |
| |
| if (isdigit(*ss)) fd = atoi(ss); |
| else if (*ss == '{') { |
| ss++; |
| // when we close a filehandle, we _read_ from {var}, not write to it |
| if ((!strcmp(s, "<&") || !strcmp(s, ">&")) && !strcmp(sss, "-")) { |
| ss = xstrndup(ss, (s-ss)-1); |
| sss = getvar(ss); |
| free(ss); |
| fd = -1; |
| if (sss) fd = atoi(sss); |
| if (fd<0) goto flush; |
| if (fd>2) { |
| rr[0] = fd; |
| rr[1] = fd<<1; // close it |
| rdcount++; |
| } |
| continue; |
| } else setvar(xmprintf("%.*s=%d", (int)(s-ss), ss, hfd), TAKE_MEM); |
| } else fd = *ss != '<'; |
| *rr = fd; |
| |
| // at this point for [n]<word s = start of [n], ss = start of <, sss = word |
| |
| // second entry in this rd[] pair is new fd to dup2() after vfork(), |
| // I.E. for [n]<word the fd if you open("word"). It's stored <<1 and the |
| // low bit set means don't close(rr[1]) after dup2(rr[1]>>1, rr[0]); |
| |
| // fd<0 means HERE document. Canned input stored earlier, becomes pipe later |
| if (!strcmp(s, "<<<") || !strcmp(s, "<<-") || !strcmp(s, "<<")) { |
| fd = --here<<2; |
| if (s[2] == '-') fd += 1; // zap tabs |
| if (s[strcspn(s, "\"'")]) fd += 2; // it was quoted so no expansion |
| rr[1] = fd; |
| rdcount++; |
| |
| continue; |
| } |
| |
| // Handle file descriptor duplication/close (&> &>> <& >& with number or -) |
| if (strchr(ss, '&') && ss[2] != '>') { |
| char *dig = sss; |
| |
| // These redirect existing fd so nothing to open() |
| while (isdigit(dig)) dig++; |
| if (dig-sss>5) { |
| s = sss; |
| goto flush; |
| } |
| |
| // TODO can't check if fd is open here, must do it when actual redirects happen |
| if (!*dig || (*dig=='-' && !dig[1])) { |
| rr[1] = (((dig==sss) ? *rr : atoi(sss))<<1)+(*dig != '-'); |
| rdcount++; |
| |
| continue; |
| } |
| } |
| |
| // Permissions to open external file with: < > >> <& >& <> >| &>> &> |
| if (!strcmp(ss, "<>")) fd = O_CREAT|O_RDWR; |
| else if (strstr(ss, ">>")) fd = O_CREAT|O_APPEND; |
| else { |
| fd = (*ss != '<') ? O_CREAT|O_WRONLY|O_TRUNC : O_RDONLY; |
| if (!strcmp(ss, ">") && (TT.options&SH_NOCLOBBER)) { |
| struct stat st; |
| |
| // Not _just_ O_EXCL: > /dev/null allowed |
| if (stat(sss, &st) || !S_ISREG(st.st_mode)) fd |= O_EXCL; |
| } |
| } |
| |
| // Open the file |
| // TODO: /dev/fd/# /dev/{stdin,stdout,stderr} /dev/{tcp,udp}/host/port |
| if (-1 == (fd = xcreate(sss, fd|WARN_ONLY, 777)) || hfd != dup2(fd, hfd)) { |
| pp->exit = 1; |
| s = 0; |
| |
| goto flush; |
| } |
| if (fd != hfd) close(fd); |
| rr[1] = hfd<<1; |
| rdcount++; |
| |
| // queue up a 2>&1 ? |
| if (strchr(ss, '&')) { |
| if (!(31&++rdcount)) rd = xrealloc(rd, (2*rdcount+66)*sizeof(int *)); |
| rr = rd+3+rdcount; |
| rr[0] = 2; |
| rr[1] = 1+(1<<1); |
| rdcount++; |
| } |
| } |
| if (rd) rd[2] = rdcount; |
| |
| // TODO: ok, now _use_ in_rd[in_rdcount] and rd[rdcount]. :) |
| |
| // TODO: handle ((math)) here |
| |
| // TODO use envlen |
| // TODO: check for functions |
| |
| // Is this command a builtin that should run in this process? |
| if ((tl = toy_find(*pp->arg.v)) |
| && (tl->flags & (TOYFLAG_NOFORK|TOYFLAG_MAYFORK))) |
| { |
| struct toy_context temp; |
| sigjmp_buf rebound; |
| |
| // This fakes lots of what toybox_main() does. |
| memcpy(&temp, &toys, sizeof(struct toy_context)); |
| memset(&toys, 0, sizeof(struct toy_context)); |
| |
| // TODO: redirect stdin/out |
| if (!sigsetjmp(rebound, 1)) { |
| toys.rebound = &rebound; |
| // must be null terminated |
| toy_init(tl, pp->arg.v); |
| tl->toy_main(); |
| } |
| pp->exit = toys.exitval; |
| if (toys.optargs != toys.argv+1) free(toys.optargs); |
| if (toys.old_umask) umask(toys.old_umask); |
| memcpy(&toys, &temp, sizeof(struct toy_context)); |
| } else { |
| int pipe[2]; |
| |
| pipe[0] = 0; |
| pipe[1] = 1; |
| // TODO: redirect and pipe |
| // TODO: redirecting stderr needs xpopen3() or rethink |
| if (-1 == (pp->pid = xpopen_both(pp->arg.v, pipe))) |
| perror_msg("%s: vfork", *pp->arg.v); |
| // TODO: don't close stdin/stdout! |
| else pp->exit = xpclose_both(pp->pid, 0); |
| } |
| |
| s = 0; |
| flush: |
| if (s) { |
| syntax_err("bad %s", s); |
| if (!pp->exit) pp->exit = 1; |
| } |
| for (j = 0; j<rdcount; j++) if (rd[4+2*j]>6) close(rd[4+2*j]>>1); |
| if (rdcount) free(dlist_lpop((void *)rdlist)); |
| |
| return pp; |
| } |
| |
| // parse next word from command line. Returns end, or 0 if need continuation |
| // caller eats leading spaces |
| static char *parse_word(char *start) |
| { |
| int i, j, quote = 0, q, qc = 0; |
| char *end = start, *s; |
| |
| // (( is a special quote at the start of a word |
| if (strstart(&end, "((")) toybuf[quote++] = 255; |
| |
| // find end of this word |
| while (*end) { |
| i = 0; |
| |
| // barf if we're near overloading quote stack (nesting ridiculously deep) |
| if (quote>4000) { |
| syntax_err("tilt"); |
| return (void *)1; |
| } |
| |
| q = quote ? toybuf[quote-1] : 0; |
| // Handle quote contexts |
| if (q) { |
| |
| // when waiting for parentheses, they nest |
| if ((q == ')' || q == '\xff') && (*end == '(' || *end == ')')) { |
| if (*end == '(') qc++; |
| else if (qc) qc--; |
| else if (q == '\xff') { |
| // (( can end with )) or retroactively become two (( if we hit one ) |
| if (strstart(&end, "))")) quote--; |
| else return start+1; |
| } |
| end++; |
| |
| // end quote? |
| } else if (*end == q) quote--, end++; |
| |
| // single quote claims everything |
| else if (q == '\'') end++; |
| else i++; |
| |
| // loop if we already handled a symbol |
| if (!i) continue; |
| } else { |
| // Things that only matter when unquoted |
| |
| if (isspace(*end)) break; |
| |
| // Things we should only return at the _start_ of a word |
| |
| // Redirections. 123<<file- parses as 2 args: "123<<" "file-". |
| // Greedy matching: >&; becomes >& ; not > &; |
| s = end + redir_prefix(end); |
| j = anystart(s, (char *[]){"<<<", "<<-", "<<", "<&", "<>", "<", ">>", |
| ">&", ">|", ">", 0}); |
| if (j) s += j; |
| |
| // Control characters |
| else s = end + anystart(end, (char *[]){";;&", ";;", ";&", ";", "||", |
| "|&", "|", "&&", "&>>", "&>", "&", "(", ")", 0}); |
| if (s != end) return (end == start) ? s : end; |
| i++; |
| } |
| |
| // Things the same unquoted or in most non-single-quote contexts |
| |
| // start new quote context? |
| if (strchr("\"'`", *end)) toybuf[quote++] = *end++; |
| else if (q != '"' && (strstart(&end, "<(") || strstart(&end,">("))) |
| toybuf[quote++]=')'; |
| |
| // backslash escapes |
| else if (*end == '\\') { |
| if (!end[1] || (end[1]=='\n' && !end[2])) return 0; |
| end += 2; |
| } else if (*end++ == '$') { |
| if (-1 != (i = stridx("({[", *end))) { |
| toybuf[quote++] = ")}]"[i]; |
| end++; |
| } |
| } |
| } |
| |
| return quote ? 0 : end; |
| } |
| |
| // if then fi for while until select done done case esac break continue return |
| |
| // Allocate more space for arg, and possibly terminator |
| void argxtend(struct sh_arg *arg) |
| { |
| if (!(arg->c&31)) arg->v = xrealloc(arg->v, (33+arg->c)*sizeof(void *)); |
| } |
| |
| // Pipeline segments |
| struct sh_pipeline { |
| struct sh_pipeline *next, *prev; |
| int count, here, type; |
| struct sh_arg arg[1]; |
| }; |
| |
| // run a series of "command | command && command" with redirects. |
| int run_pipeline(struct sh_pipeline **pl, int *rd) |
| { |
| struct sh_process *pp; |
| int rc = 0; |
| |
| for (;;) { |
| // TODO job control |
| if (!(pp = run_command((*pl)->arg, &rd))) rc = 0; |
| else { |
| //wait4(pp); |
| llist_traverse(pp->delete, free); |
| rc = pp->exit; |
| free(pp); |
| } |
| |
| if ((*pl)->next && !(*pl)->next->type) *pl = (*pl)->next; |
| else return rc; |
| } |
| } |
| |
| |
| |
| // scratch space (state held between calls). Don't want to make it global yet |
| // because this could be reentrant. |
| struct sh_function { |
| char *name; |
| struct sh_pipeline *pipeline; |
| struct double_list *expect; |
| // TODO: lifetime rules for arg? remember "shift" command. |
| struct sh_arg *arg; // arguments to function call |
| char *end; |
| }; |
| |
| // Free one pipeline segment. |
| void free_pipeline(void *pipeline) |
| { |
| struct sh_pipeline *pl = pipeline; |
| int i, j; |
| |
| if (pl) for (j=0; j<=pl->count; j++) { |
| for (i = 0; i<=pl->arg->c; i++) free(pl->arg[j].v[i]); |
| free(pl->arg[j].v); |
| } |
| free(pl); |
| } |
| |
| // Return end of current block, or NULL if we weren't in block and fell off end. |
| struct sh_pipeline *block_end(struct sh_pipeline *pl) |
| { |
| int i = 0; |
| |
| while (pl) { |
| if (pl->type == 1 || pl->type == 'f') i++; |
| else if (pl->type == 3) if (--i<1) break; |
| pl = pl->next; |
| } |
| |
| return 0; |
| } |
| |
| void free_function(struct sh_function *sp) |
| { |
| llist_traverse(sp->pipeline, free_pipeline); |
| llist_traverse(sp->expect, free); |
| memset(sp, 0, sizeof(struct sh_function)); |
| } |
| |
| // TODO this has to add to a namespace context. Functions within functions... |
| struct sh_pipeline *add_function(char *name, struct sh_pipeline *pl) |
| { |
| dprintf(2, "stub add_function"); |
| |
| return block_end(pl->next); |
| } |
| |
| // Add a line of shell script to a shell function. Returns 0 if finished, |
| // 1 to request another line of input (> prompt), -1 for syntax err |
| static int parse_line(char *line, struct sh_function *sp) |
| { |
| char *start = line, *delete = 0, *end, *last = 0, *s, *ex, done = 0; |
| struct sh_pipeline *pl = sp->pipeline ? sp->pipeline->prev : 0; |
| struct sh_arg *arg = 0; |
| long i; |
| |
| // Resume appending to last statement? |
| if (pl) { |
| arg = pl->arg; |
| |
| // Extend/resume quoted block |
| if (arg->c<0) { |
| delete = start = xmprintf("%s%s", arg->v[arg->c = (-arg->c)-1], start); |
| free(arg->v[arg->c]); |
| arg->v[arg->c] = 0; |
| |
| // is a HERE document in progress? |
| } else if (pl->count != pl->here) { |
| arg += 1+pl->here; |
| |
| argxtend(arg); |
| if (strcmp(line, arg->v[arg->c])) { |
| // Add this line |
| arg->v[arg->c+1] = arg->v[arg->c]; |
| arg->v[arg->c++] = xstrdup(line); |
| // EOF hit, end HERE document |
| } else { |
| arg->v[arg->c] = 0; |
| pl->here++; |
| } |
| start = 0; |
| |
| // Nope, new segment |
| } else pl = 0; |
| } |
| |
| // Parse words, assemble argv[] pipelines, check flow control and HERE docs |
| if (start) for (;;) { |
| ex = sp->expect ? sp->expect->prev->data : 0; |
| |
| // Look for << HERE redirections in completed pipeline segment |
| if (pl && pl->count == -1) { |
| pl->count = 0; |
| arg = pl->arg; |
| |
| // find arguments of the form [{n}]<<[-] with another one after it |
| for (i = 0; i<arg->c; i++) { |
| s = arg->v[i] + redir_prefix(arg->v[i]); |
| if (strcmp(s, "<<") && strcmp(s, "<<-") && strcmp(s, "<<<")) continue; |
| if (i+1 == arg->c) goto flush; |
| |
| // Add another arg[] to the pipeline segment (removing/readding to list |
| // because realloc can move pointer) |
| dlist_lpop(&sp->pipeline); |
| pl = xrealloc(pl, sizeof(*pl) + ++pl->count*sizeof(struct sh_arg)); |
| dlist_add_nomalloc((void *)&sp->pipeline, (void *)pl); |
| |
| // queue up HERE EOF so input loop asks for more lines. |
| arg[pl->count].v = xzalloc(2*sizeof(void *)); |
| *arg[pl->count].v = arg->v[++i]; |
| arg[pl->count].c = -(s[2] == '<'); // note <<< as c = -1 |
| } |
| pl = 0; |
| } |
| if (done) break; |
| s = 0; |
| |
| // skip leading whitespace/comment here to know where next word starts |
| for (;;) { |
| if (isspace(*start)) ++start; |
| else if (*start=='#') while (*start && *start != '\n') ++start; |
| else break; |
| } |
| |
| // Parse next word and detect overflow (too many nested quotes). |
| if ((end = parse_word(start)) == (void *)1) |
| goto flush; |
| |
| // Is this a new pipeline segment? |
| if (!pl) { |
| pl = xzalloc(sizeof(struct sh_pipeline)); |
| arg = pl->arg; |
| dlist_add_nomalloc((void *)&sp->pipeline, (void *)pl); |
| } |
| argxtend(arg); |
| |
| // Do we need to request another line to finish word (find ending quote)? |
| if (!end) { |
| // Save unparsed bit of this line, we'll need to re-parse it. |
| arg->v[arg->c] = xstrndup(start, strlen(start)); |
| arg->c = -(arg->c+1); |
| free(delete); |
| |
| return 1; |
| } |
| |
| // Ok, we have a word. What does it _mean_? |
| |
| // Did we hit end of line or ) outside a function declaration? |
| // ) is only saved at start of a statement, ends current statement |
| if (end == start || (arg->c && *start == ')' && pl->type!='f')) { |
| arg->v[arg->c] = 0; |
| |
| if (pl->type == 'f' && arg->c<3) { |
| s = "function()"; |
| goto flush; |
| } |
| |
| // "for" on its own line is an error. |
| if (arg->c == 1 && ex && !memcmp(ex, "do\0A", 4)) { |
| s = "newline"; |
| goto flush; |
| } |
| |
| // don't save blank pipeline segments |
| if (!arg->c) free_pipeline(dlist_lpop(&sp->pipeline)); |
| |
| // stop at EOL, else continue with new pipeline segment for ) |
| if (end == start) done++; |
| pl->count = -1; |
| last = 0; |
| |
| continue; |
| } |
| |
| // Save argument (strdup) and check for flow control |
| s = arg->v[arg->c] = xstrndup(start, end-start); |
| start = end; |
| if (strchr(";|&", *s)) { |
| |
| // flow control without a statement is an error |
| if (!arg->c) goto flush; |
| |
| // treat ; as newline so we don't have to check both elsewhere. |
| if (!strcmp(s, ";")) { |
| arg->v[arg->c] = 0; |
| free(s); |
| s = 0; |
| } |
| last = s; |
| pl->count = -1; |
| |
| continue; |
| } else arg->v[++arg->c] = 0; |
| |
| // is a function() in progress? |
| if (arg->c>1 && !strcmp(s, "(")) pl->type = 'f'; |
| if (pl->type=='f') { |
| if (arg->c == 2 && strcmp(s, "(")) goto flush; |
| if (arg->c == 3) { |
| if (strcmp(s, ")")) goto flush; |
| |
| // end function segment, expect function body |
| pl->count = -1; |
| last = 0; |
| dlist_add(&sp->expect, "}"); |
| dlist_add(&sp->expect, 0); |
| dlist_add(&sp->expect, "{"); |
| |
| continue; |
| } |
| |
| // a for/select must have at least one additional argument on same line |
| } else if (ex && !memcmp(ex, "do\0A", 4)) { |
| |
| // Sanity check and break the segment |
| if (strncmp(s, "((", 2) && strchr(s, '=')) goto flush; |
| pl->count = -1; |
| sp->expect->prev->data = "do\0C"; |
| |
| continue; |
| |
| // flow control is the first word of a pipeline segment |
| } else if (arg->c>1) continue; |
| |
| // Do we expect something that _must_ come next? (no multiple statements) |
| if (ex) { |
| // When waiting for { it must be next symbol, but can be on a new line. |
| if (!strcmp(ex, "{")) { |
| if (strcmp(s, "{")) goto flush; |
| free(arg->v[--arg->c]); // don't save the {, function starts the block |
| free(dlist_lpop(&sp->expect)); |
| |
| continue; |
| |
| // The "test" part of for/select loops can have (at most) one "in" line, |
| // for {((;;))|name [in...]} do |
| } else if (!memcmp(ex, "do\0C", 4)) { |
| if (strcmp(s, "do")) { |
| // can only have one "in" line between for/do, but not with for(()) |
| if (!pl->prev->type) goto flush; |
| if (!strncmp(pl->prev->arg->v[1], "((", 2)) goto flush; |
| else if (strcmp(s, "in")) goto flush; |
| |
| continue; |
| } |
| } |
| } |
| |
| // start of a new block? |
| |
| // for/select requires variable name on same line, can't break segment yet |
| if (!strcmp(s, "for") || !strcmp(s, "select")) { |
| if (!pl->type) pl->type = 1; |
| dlist_add(&sp->expect, "do\0A"); |
| |
| continue; |
| } |
| |
| end = 0; |
| if (!strcmp(s, "if")) end = "then"; |
| else if (!strcmp(s, "while") || !strcmp(s, "until")) end = "do\0B"; |
| else if (!strcmp(s, "case")) end = "esac"; |
| else if (!strcmp(s, "{")) end = "}"; |
| else if (!strcmp(s, "[[")) end = "]]"; |
| else if (!strcmp(s, "(")) end = ")"; |
| |
| // Expecting NULL means a statement: I.E. any otherwise unrecognized word |
| else if (sp->expect && !ex) { |
| free(dlist_lpop(&sp->expect)); |
| continue; |
| } else if (!ex) goto check; |
| |
| // Did we start a new statement? |
| if (end) { |
| pl->type = 1; |
| |
| // Only innermost statement needed in { { { echo ;} ;} ;} and such |
| if (sp->expect && !sp->expect->prev->data) free(dlist_lpop(&sp->expect)); |
| |
| // If we got here we expect a specific word to end this block: is this it? |
| } else if (!strcmp(s, ex)) { |
| // can't "if | then" or "while && do", only ; & or newline works |
| if (last && (strcmp(ex, "then") || strcmp(last, "&"))) { |
| s = end; |
| goto flush; |
| } |
| |
| free(dlist_lpop(&sp->expect)); |
| pl->type = anystr(s, (char *[]){"fi", "done", "esac", "}", "]]", ")", 0}) |
| ? 3 : 2; |
| |
| // if it's a multipart block, what comes next? |
| if (!strcmp(s, "do")) end = "done"; |
| else if (!strcmp(s, "then")) end = "fi\0A"; |
| |
| // fi could have elif, which queues a then. |
| } else if (!strcmp(ex, "fi")) { |
| if (!strcmp(s, "elif")) { |
| free(dlist_lpop(&sp->expect)); |
| end = "then"; |
| // catch duplicate else while we're here |
| } else if (!strcmp(s, "else")) { |
| if (ex[3] != 'A') { |
| s = "2 else"; |
| goto flush; |
| } |
| free(dlist_lpop(&sp->expect)); |
| end = "fi\0B"; |
| } |
| } |
| |
| // Do we need to queue up the next thing to expect? |
| if (end) { |
| if (!pl->type) pl->type = 2; |
| dlist_add(&sp->expect, end); |
| dlist_add(&sp->expect, 0); // they're all preceded by a statement |
| pl->count = -1; |
| } |
| |
| check: |
| // syntax error check: these can't be the first word in an unexpected place |
| if (!pl->type && anystr(s, (char *[]){"then", "do", "esac", "}", "]]", ")", |
| "done", "fi", "elif", "else", 0})) goto flush; |
| } |
| free(delete); |
| |
| // advance past <<< arguments (stored as here documents, but no new input) |
| pl = sp->pipeline->prev; |
| while (pl->count<pl->here && pl->arg[pl->count].c<0) |
| pl->arg[pl->count++].c = 0; |
| |
| // return if HERE document pending or more flow control needed to complete |
| if (sp->expect) return 1; |
| if (sp->pipeline && pl->count != pl->here) return 1; |
| dlist_terminate(sp->pipeline); |
| |
| // Don't need more input, can start executing. |
| |
| return 0; |
| |
| flush: |
| if (s) syntax_err("bad %s", s); |
| free_function(sp); |
| |
| return 0-!!s; |
| } |
| |
| static void dump_state(struct sh_function *sp) |
| { |
| struct sh_pipeline *pl; |
| int q = 0; |
| long i; |
| |
| if (sp->expect) { |
| struct double_list *dl; |
| |
| for (dl = sp->expect; dl; dl = (dl->next == sp->expect) ? 0 : dl->next) |
| dprintf(2, "expecting %s\n", dl->data); |
| if (sp->pipeline) |
| dprintf(2, "pipeline count=%d here=%d\n", sp->pipeline->prev->count, |
| sp->pipeline->prev->here); |
| } |
| |
| for (pl = sp->pipeline; pl ; pl = (pl->next == sp->pipeline) ? 0 : pl->next) { |
| for (i = 0; i<pl->arg->c; i++) |
| printf("arg[%d][%ld]=%s\n", q, i, pl->arg->v[i]); |
| printf("type=%d term[%d]=%s\n", pl->type, q++, pl->arg->v[pl->arg->c]); |
| } |
| } |
| |
| /* Flow control statements: |
| |
| if/then/elif/else/fi, for select while until/do/done, case/esac, |
| {/}, [[/]], (/), function assignment |
| */ |
| |
| |
| |
| // run a shell function, handling flow control statements |
| static void run_function(struct sh_function *sp) |
| { |
| struct sh_pipeline *pl = sp->pipeline, *end; |
| struct blockstack { |
| struct blockstack *next; |
| struct sh_pipeline *start, *end; |
| int run, loop, *redir; |
| |
| struct sh_arg farg; // for/select arg stack |
| struct string_list *fdelete; // farg's cleanup list |
| char *fvar; // for/select's iteration variable name |
| } *blk = 0, *new; |
| long i; |
| |
| // iterate through the commands |
| while (pl) { |
| char *s = *pl->arg->v, *ss = pl->arg->v[1]; |
| //dprintf(2, "s=%s %s %d %s %d\n", s, ss, pl->type, blk ? blk->start->arg->v[0] : "X", blk ? blk->run : 0); |
| // Normal executable statement? |
| if (!pl->type) { |
| // TODO: break & is supported? Seriously? Also break > potato |
| // TODO: break multiple aguments |
| if (!strcmp(s, "break") || !strcmp(s, "continue")) { |
| |
| // How many layers to peel off? |
| i = ss ? atol(ss) : 0; |
| if (i<1) i = 1; |
| if (!blk || pl->arg->c>2 || ss[strspn(ss, "0123456789")]) { |
| syntax_err("bad %s", s); |
| break; |
| } |
| i = atol(ss); |
| if (!i) i++; |
| while (i && blk) { |
| if (--i && *s == 'c') { |
| pl = blk->start; |
| break; |
| } |
| pl = blk->end; |
| llist_traverse(blk->fdelete, free); |
| free(llist_pop(&blk)); |
| } |
| pl = pl->next; |
| |
| continue; |
| } |
| |
| // inherit redirects? |
| // returns last statement of pipeline |
| if (!blk) toys.exitval = run_pipeline(&pl, 0); |
| else if (blk->run) toys.exitval = run_pipeline(&pl, blk->redir); |
| else while (pl->next && !pl->next->type) pl = pl->next; |
| |
| // Starting a new block? |
| } else if (pl->type == 1) { |
| |
| // are we entering this block (rather than looping back to it)? |
| if (!blk || blk->start != pl) { |
| |
| // If it's a nested block we're not running, skip ahead. |
| end = block_end(pl->next); |
| if (blk && !blk->run) { |
| pl = end; |
| if (pl) pl = pl->next; |
| continue; |
| } |
| |
| // It's a new block we're running, save context and add it to the stack. |
| new = xzalloc(sizeof(*blk)); |
| new->next = blk; |
| blk = new; |
| blk->start = pl; |
| blk->end = end; |
| blk->run = 1; |
| // TODO perform block end redirects to blk->redir |
| } |
| |
| // What flow control statement is this? |
| |
| // if/then/elif/else/fi, while until/do/done - no special handling needed |
| |
| // for select/do/done |
| if (!strcmp(s, "for") || !strcmp(s, "select")) { |
| if (blk->loop); |
| else if (!strncmp(blk->fvar = ss, "((", 2)) { |
| blk->loop = 1; |
| dprintf(2, "TODO skipped init for((;;)), need math parser\n"); |
| } else { |
| |
| // populate blk->farg with expanded arguments |
| if (!pl->next->type) { |
| for (i = 1; i<pl->next->arg->c; i++) |
| expand_arg(&blk->farg, pl->next->arg->v[i], 0, &blk->fdelete); |
| } else expand_arg(&blk->farg, "\"$@\"", 0, &blk->fdelete); |
| } |
| pl = pl->next; |
| } |
| |
| /* TODO |
| case/esac |
| {/} |
| [[/]] |
| (/) |
| ((/)) |
| function/} |
| */ |
| |
| // gearshift from block start to block body |
| } else if (pl->type == 2) { |
| |
| // Handle if statement |
| if (!strcmp(s, "then")) blk->run = blk->run && !toys.exitval; |
| else if (!strcmp(s, "else") || !strcmp(s, "elif")) blk->run = !blk->run; |
| else if (!strcmp(s, "do")) { |
| ss = *blk->start->arg->v; |
| if (!strcmp(ss, "while")) blk->run = blk->run && !toys.exitval; |
| else if (!strcmp(ss, "until")) blk->run = blk->run && toys.exitval; |
| else if (blk->loop >= blk->farg.c) { |
| blk->run = 0; |
| pl = block_end(pl); |
| continue; |
| } else if (!strncmp(blk->fvar, "((", 2)) { |
| dprintf(2, "TODO skipped running for((;;)), need math parser\n"); |
| } else setvar(xmprintf("%s=%s", blk->fvar, blk->farg.v[blk->loop++]), |
| TAKE_MEM); |
| } |
| |
| // end of block |
| } else if (pl->type == 3) { |
| |
| // repeating block? |
| if (blk->run && !strcmp(s, "done")) { |
| pl = blk->start; |
| continue; |
| } |
| |
| // if ending a block, pop stack. |
| llist_traverse(blk->fdelete, free); |
| free(llist_pop(&blk)); |
| |
| // TODO unwind redirects (cleanup blk->redir) |
| |
| } else if (pl->type == 'f') pl = add_function(s, pl); |
| |
| pl = pl->next; |
| } |
| |
| // Cleanup from syntax_err(); |
| while (blk) { |
| llist_traverse(blk->fdelete, free); |
| free(llist_pop(&blk)); |
| } |
| |
| return; |
| } |
| |
| void subshell_imports(void) |
| { |
| /* |
| // TODO cull local variables because 'env "()=42" env | grep 42' works. |
| |
| // vfork() means subshells have to export and then re-import locals/functions |
| sprintf(toybuf, "(%d#%d)", getpid(), getppid()); |
| if ((s = getenv(toybuf))) { |
| char *from, *to, *ss; |
| |
| unsetenv(toybuf); |
| ss = s; |
| |
| // Loop through packing \\ until \0 |
| for (from = to = s; *from; from++, to++) { |
| *to = *from; |
| if (*from != '\\') continue; |
| if (from[1] == '\\' || from[1] == '0') from++; |
| if (from[1] != '0') continue; |
| *to = 0; |
| |
| // save chunk |
| for (ss = s; ss<to; ss++) { |
| if (*ss == '=') { |
| // first char of name is variable type ala declare |
| if (s+1<ss && strchr("aAilnru", *s)) { |
| setvar(ss, *s); |
| |
| break; |
| } |
| } else if (!strncmp(ss, "(){", 3)) { |
| FILE *ff = fmemopen(s, to-s, "r"); |
| |
| while ((new = xgetline(ff, 0))) { |
| if ((prompt = parse_line(new, &scratch))<0) break; |
| free(new); |
| } |
| if (!prompt) { |
| add_function(s, scratch.pipeline); |
| free_function(&scratch); |
| break; |
| } |
| fclose(ff); |
| } else if (!isspace(*s) && !ispunct(*s)) continue; |
| |
| error_exit("bad locals"); |
| } |
| s = from+1; |
| } |
| } |
| */ |
| } |
| |
| void sh_main(void) |
| { |
| FILE *f; |
| char *new; |
| struct sh_function scratch; |
| int prompt = 0; |
| |
| // Set up signal handlers and grab control of this tty. |
| |
| // Read environment for exports from parent shell |
| subshell_imports(); |
| |
| memset(&scratch, 0, sizeof(scratch)); |
| if (TT.command) f = fmemopen(TT.command, strlen(TT.command), "r"); |
| else if (*toys.optargs) f = xfopen(*toys.optargs, "r"); |
| else { |
| f = stdin; |
| if (isatty(0)) toys.optflags |= FLAG_i; |
| } |
| |
| for (;;) { |
| |
| // Prompt and read line |
| if (f == stdin) { |
| char *s = getenv(prompt ? "PS2" : "PS1"); |
| |
| if (!s) s = prompt ? "> " : (getpid() ? "\\$ " : "# "); |
| do_prompt(s); |
| } else TT.lineno++; |
| if (!(new = xgetline(f ? f : stdin, 0))) break; |
| |
| // TODO if (!isspace(*new)) add_to_history(line); |
| |
| // returns 0 if line consumed, command if it needs more data |
| prompt = parse_line(new, &scratch); |
| if (0) dump_state(&scratch); |
| if (prompt != 1) { |
| // TODO: ./blah.sh one two three: put one two three in scratch.arg |
| if (!prompt) run_function(&scratch); |
| free_function(&scratch); |
| prompt = 0; |
| } |
| free(new); |
| } |
| |
| if (prompt) error_exit("%ld:unfinished line"+4*!TT.lineno, TT.lineno); |
| toys.exitval = f && ferror(f); |
| } |