From 2d5b8f9c1bcd09932f3322823a26ee4c0a543b46 Mon Sep 17 00:00:00 2001 From: Rob Landley Date: Wed, 10 Jun 2020 17:52:23 -0500 Subject: Chunk of job control pumbing. --- toys/pending/sh.c | 325 ++++++++++++++++++++++++++---------------------------- 1 file changed, 154 insertions(+), 171 deletions(-) diff --git a/toys/pending/sh.c b/toys/pending/sh.c index e77ebe6f..4e626c75 100644 --- a/toys/pending/sh.c +++ b/toys/pending/sh.c @@ -46,6 +46,11 @@ * reserved words * ! case coproc do done elif else esac fi for function if in select * then until while { } time [[ ]] + * + * Flow control statements: + * + * if/then/elif/else/fi, for select while until/do/done, case/esac, + * {/}, [[/]], (/), function assignment USE_SH(NEWTOY(cd, ">1LP[-LP]", TOYFLAG_NOFORK)) USE_SH(NEWTOY(eval, 0, TOYFLAG_NOFORK)) @@ -143,6 +148,21 @@ config EXPORT With no arguments list exported variables/attributes as "declare" statements. +config JOBS + bool + default n + depends on SH + help + usage: jobs [-lnprs] [%JOB | -x COMMAND...] + + List running/stopped background jobs. + + -l Include process ID in list + -n Show only new/changed processes + -p Show process IDs only + -r Show running processes + -s Show stopped processes + config SHIFT bool default n @@ -167,66 +187,50 @@ GLOBALS( } exec; }; - // keep lineno here, we use it to work around a compiler bug + // keep lineno here, we use it to work around a compiler limitation long lineno; char *ifs, *isexec; - struct double_list functions; unsigned options, jobcnt; - int hfd, pid, varslen, shift, cdcount; - unsigned long long SECONDS; + int hfd, pid, bangpid, varslen, shift, cdcount; + long long SECONDS; struct sh_vars { long flags; char *str; } *vars; - // Running jobs for job control. - 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 sh_process *next, *prev; - struct arg_list *delete; // expanded strings - // undo redirects, a=b at start, child PID, exit status, has ! - int *urd, envlen, pid, exit, not; - struct sh_arg arg; - } *procs, *proc; - } *jobs, *job; - - struct sh_process *pp; - struct sh_arg *arg; + // Parsed function + struct sh_function { + char *name; + struct sh_pipeline { // pipeline segments + struct sh_pipeline *next, *prev, *end; + int count, here, type; // TODO abuse type to replace count during parsing + struct sh_arg { + char **v; + int c; + } arg[1]; + } *pipeline; + struct double_list *expect; // should be zero at end of parsing + } *functions; + +// TODO ctrl-Z suspend should stop script + struct sh_process { + struct sh_process *next, *prev; // | && || + struct arg_list *delete; // expanded strings + // undo redirects, a=b at start, child PID, exit status, has !, job # + int *urd, envlen, pid, exit, not, job; + long long when; // when job backgrounded/suspended +// TODO struct sh_arg *raw; // for display + struct sh_arg arg; + } *pp; // currently running process + + struct sh_arg jobs, *arg; // job list, command line args for $* etc ) // Can't yet avoid this prototype. Fundamental problem is $($($(blah))) nests, // leading to function loop with run->parse->run static int sh_run(char *new); -// Pipeline segments -struct sh_pipeline { - struct sh_pipeline *next, *prev, *end; - int count, here, type; - struct sh_arg arg[1]; -}; - -// 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; -}; - #define BUGBUG 0 // call with NULL to just dump FDs @@ -843,6 +847,7 @@ if (BUGBUG) dprintf(255, "expand %s\n", str); qq += 2; while ((cc = str[ii++]) != '\'') new[oo++] = cc; } + // both types of subshell work the same, so do $( here not in '$' below // TODO $((echo hello) | cat) ala $(( becomes $( ( retroactively } else if (cc == '`' || (cc == '$' && strchr("([", str[ii]))) { @@ -939,7 +944,6 @@ dprintf(2, "TODO: do math for %.*s\n", kk, s); // x can be @* // TODO: $_ is last arg of last command, and exported as path to exe run -// TODO: $! is PID of most recent background job if (ifs); else if (cc == '-') { s = ifs = xmalloc(8); @@ -951,6 +955,7 @@ dprintf(2, "TODO: do math for %.*s\n", kk, s); } else if (cc == '?') ifs = xmprintf("%d", toys.exitval); else if (cc == '$') ifs = xmprintf("%d", TT.pid); else if (cc == '#') ifs = xmprintf("%d", TT.arg->c?TT.arg->c-1:0); + else if (cc == '!') ifs = xmprintf("%d"+2*!TT.bangpid, TT.bangpid); else if (cc == '*' || cc == '@') aa = TT.arg->v+1; else if (isdigit(cc)) { for (kk = ll = 0; kk file". No idea why. +// TODO If we just started a new pipeline, implicit parentheses (subshell) // TODO can't free sh_process delete until ready to dispose else no debug output +/* +TODO: a | b | c needs subshell for builtins? + - anything that can produce output + - echo declare dirs + (a; b; c) like { } but subshell + when to auto-exec? ps vs sh -c 'ps' vs sh -c '(ps)' +*/ + TT.hfd = 10; // iterate through pipeline segments while (pl) { - struct sh_arg *arg = pl->arg; - char *s = *arg->v, *ss = arg->v[1], *ctl = arg->v[arg->c]; -if (BUGBUG) dprintf(255, "%d runtype=%d %s %s\n", getpid(), pl->type, s, ctl); - // Is this an executable segment? - if (!pl->type) { + char *ctl = pl->end->arg->v[pl->end->arg->c], + *s = *pl->arg->v, *ss = pl->arg->v[1]; - // Skip disabled block + // Skip disabled blocks, handle pipes + if (pl->type<2) { if (blk && !blk->run) { - pl = pl->next; + pl = pl->end->next; continue; } if (pipe_segments(ctl, pipes, &urd)) break; + } - // If we just started a new pipeline, implicit parentheses (subshell) - -// TODO: "echo | read i" is backgroundable with ctrl-Z despite read = builtin. -// probably have to inline run_command here to do that? Implicit () -// also "X=42 | true; echo $X" doesn't get X. - - // TODO: bash supports "break &" and "break > file". No idea why. +if (BUGBUG) dprintf(255, "%d runtype=%d %s %s\n", getpid(), pl->type, s, ctl); + // Is this an executable segment? + if (!pl->type) { // Is it a flow control jump? These aren't handled as normal builtins // because they move *pl to other pipeline segments which is local here. @@ -2083,7 +2090,7 @@ if (BUGBUG) dprintf(255, "%d runtype=%d %s %s\n", getpid(), pl->type, s, ctl); // How many layers to peel off? i = ss ? atol(ss) : 0; if (i<1) i = 1; - if (!blk || arg->c>2 || ss[strspn(ss, "0123456789")]) { + if (!blk || pl->arg->c>2 || ss[strspn(ss, "0123456789")]) { syntax_err(s); break; } @@ -2092,30 +2099,19 @@ if (BUGBUG) dprintf(255, "%d runtype=%d %s %s\n", getpid(), pl->type, s, ctl); if (!--i && *s == 'c') pl = blk->start; else pl = pop_block(&blk, pipes); if (i) { - syntax_err("break outside loop"); + syntax_err("break"); break; } - pl = pl->next; - continue; - - // Parse and run next command } else { - -// TODO: "echo | read i" is backgroundable with ctrl-Z despite read = builtin. -// probably have to inline run_command here to do that? Implicit () -// also "X=42 | true; echo $X" doesn't get X. -// I.E. run_subshell() here sometimes? (But when?) - - dlist_add_nomalloc((void *)&pplist, (void *)run_command(arg)); - } - - if (*pipes == -1) { - toys.exitval = wait_pipeline(pplist); - llist_traverse(pplist, free_process); - pplist = 0; - // for && and || skip pipeline segment(s) based on return code - while (ctl && !strcmp(ctl, toys.exitval ? "&&" : "||")) - ctl = (pl = pl->type ? pl->end : pl->next)?pl->arg->v[pl->arg->c]:0; + // Parse and run next command, saving resulting process + dlist_add_nomalloc((void *)&pplist, (void *)run_command(pl->arg)); + + // Three cases: backgrounded&, pipelined|, last process in pipeline; + if (ctl && !strcmp(ctl, "&")) { + pplist->job = ++TT.jobcnt; + arg_add(&TT.jobs, (void *)pplist); + pplist = 0; + } } // Start of flow control block? @@ -2123,115 +2119,90 @@ if (BUGBUG) dprintf(255, "%d runtype=%d %s %s\n", getpid(), pl->type, s, ctl); struct sh_process *pp = 0; int rc; - // 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. - if (blk && !blk->run) { - pl = pl->end->next; - continue; - } + // Save new block and add it to the stack. + new = xzalloc(sizeof(*blk)); + new->next = blk; + blk = new; + blk->start = pl; + blk->run = 1; + + // push pipe and redirect context into block + blk->pout = *pipes; + *pipes = -1; + pp = expand_redir(pl->end->arg, 1, blk->urd = urd); + urd = 0; + rc = pp->exit; + if (pp->arg.c) { + syntax_err(*pp->arg.v); + rc = 1; + } - // If previous piped into this block, save context until block end - if (pipe_segments(pl->end->arg->v[pl->end->arg->c], pipes, &urd)) break; - - // 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->run = 1; - - // save context until block end - blk->pout = *pipes; - blk->urd = urd; - urd = 0; - *pipes = -1; - - // Perform redirects listed at end of block - pp = expand_redir(pl->end->arg, 1, blk->urd); - blk->urd = pp->urd; - rc = pp->exit; - if (pp->arg.c) { - syntax_err(*pp->arg.v); - rc = 1; - } + // Cleanup if we're not doing a subshell + if (rc || strcmp(s, "(")) { llist_traverse(pp->delete, free); free(pp); if (rc) { toys.exitval = rc; break; } + } else { + // Create new process + if (!CFG_TOYBOX_FORK) { + ss = pl2str(pl->next); + pp->pid = run_subshell(ss, strlen(ss)); + free(ss); + } else if (!(pp->pid = fork())) { + run_function(pl->next); + _exit(toys.exitval); + } + + // add process to current pipeline same as type 0 + dlist_add_nomalloc((void *)&pplist, (void *)pp); + pl = pl->end; + continue; } +pp = 0; // What flow control statement is this? // {/} if/then/elif/else/fi, while until/do/done - no special handling - // for select/do/done + // for select/do/done: populate blk->farg with expanded arguments (if any) if (!strcmp(s, "for") || !strcmp(s, "select")) { - if (blk->loop); + if (blk->loop); // TODO: still needed? 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 == 's') { - for (i = 1; inext->arg->c; i++) - if (expand_arg(&blk->farg, pl->next->arg->v[i], 0, &blk->fdelete)) - break; - if (i != pl->next->arg->c) { - pl = pop_block(&blk, pipes)->next; - continue; - } - // this can't fail - } else expand_arg(&blk->farg, "\"$@\"", 0, &blk->fdelete); - } + // in LIST + } else if (pl->next->type == 's') { + for (i = 1; inext->arg->c; i++) + if (expand_arg(&blk->farg, pl->next->arg->v[i], 0, &blk->fdelete)) + break; + if (i != pl->next->arg->c) pl = pop_block(&blk, pipes); + // in without LIST. (This expansion can't fail.) + } else expand_arg(&blk->farg, "\"$@\"", 0, &blk->fdelete); // TODO case/esac [[/]] ((/)) function/} -/* -TODO: a | b | c needs subshell for builtins? - - anything that can produce output - - echo declare dirs - (a; b; c) like { } but subshell - when to auto-exec? ps vs sh -c 'ps' vs sh -c '(ps)' -*/ - - // subshell - } else if (!strcmp(s, "(")) { - if (!CFG_TOYBOX_FORK) { - ss = pl2str(pl->next); - pp->pid = run_subshell(ss, strlen(ss)); - free(ss); - } else { - if (!(pp->pid = fork())) { - run_function(pl->next); - _exit(toys.exitval); - } - } - - dlist_add_nomalloc((void *)&pplist, (void *)pp); - pl = pl->end; - continue; } // gearshift from block start to block body (end of flow control test) } else if (pl->type == 2) { - // Handle if statement + blk->middle = pl; + + // Handle if/else/elif statement if (!strcmp(s, "then")) blk->run = blk->run && !toys.exitval; else if (!strcmp(s, "else") || !strcmp(s, "elif")) blk->run = !blk->run; + + // Loop 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 = pl->end; - continue; - } else if (!strncmp(blk->fvar, "((", 2)) { + else if (blk->loop >= blk->farg.c) pl = pop_block(&blk, pipes); + else if (!strncmp(blk->fvar, "((", 2)) { dprintf(2, "TODO skipped running for((;;)), need math parser\n"); } else setvarval(blk->fvar, blk->farg.v[blk->loop++]); } @@ -2239,18 +2210,30 @@ dprintf(2, "TODO skipped running for((;;)), need math parser\n"); // end of block, may have trailing redirections and/or pipe } else if (pl->type == 3) { - // if we end a block we're not in, we started in a block. + // if we end a block we're not in, we started in a block (subshell) if (!blk) break; // repeating block? if (blk->run && !strcmp(s, "done")) { - pl = blk->start; + pl = blk->middle; continue; } pop_block(&blk, pipes); } else if (pl->type == 'f') pl = add_function(s, pl); + // If we ran a process and didn't pipe output or background, wait for exit + if (pplist && *pipes == -1) { + toys.exitval = wait_pipeline(pplist); + llist_traverse(pplist, free_process); + pplist = 0; + } + + // for && and || skip pipeline segment(s) based on return code + if (pl->type == 1 || pl->type == 3) + while (ctl && !strcmp(ctl, toys.exitval ? "&&" : "||")) + ctl = (pl = pl->type ? pl->end : pl->next)?pl->arg->v[pl->arg->c]:0; + pl = pl->next; } -- cgit v1.2.3