ref: 9f76a7f6819ac04552b4fb6588156f3e4089d1d7
dir: /appl/cmd/sh/sh.y/
%{
include "sys.m";
sys: Sys;
sprint: import sys;
include "draw.m";
include "bufio.m";
bufio: Bufio;
include "string.m";
str: String;
include "filepat.m";
filepat: Filepat;
include "env.m";
env: Env;
include "sh.m";
myself: Sh;
myselfbuiltin: Shellbuiltin;
YYSTYPE: adt {
node: ref Node;
word: string;
redir: ref Redir;
optype: int;
};
YYLEX: adt {
lval: YYSTYPE;
err: string; # if error has occurred
errline: int; # line it occurred on.
path: string; # name of file that's being read.
# free caret state
wasdollar: int;
atendword: int;
eof: int;
cbuf: array of int; # last chars read
ncbuf: int; # number of chars in cbuf
f: ref Bufio->Iobuf;
s: string;
strpos: int; # string pos/cbuf index
linenum: int;
prompt: string;
lastnl: int;
initstring: fn(s: string): ref YYLEX;
initfile: fn(fd: ref Sys->FD, path: string): ref YYLEX;
lex: fn(l: self ref YYLEX): int;
error: fn(l: self ref YYLEX, err: string);
getc: fn(l: self ref YYLEX): int;
ungetc: fn(l: self ref YYLEX);
EOF: con -1;
};
Options: adt {
lflag,
nflag: int;
ctxtflags: int;
carg: string;
};
%}
%module Sh {
# module definition is in shell.m
}
%token DUP REDIR WORD OP END ERROR ANDAND OROR
%type <node> redir word nlsimple simple cmd shell assign
%type <node> cmdsan cmdsa pipe comword line body list and2 or2
%type <redir> DUP REDIR '|'
%type <optype> OP '='
%type <word> WORD
%start shell
%%
shell: line end {yylex.lval.node = $line; return 0;}
| error end {yylex.lval.node = nil; return 0;}
end: END
| '\n'
line: or2
| cmdsa line {$$ = mkseq($cmdsa, $line); }
body: or2
| cmdsan body {$$ = mkseq($cmdsan, $body); }
cmdsa: or2 ';' {$$ = $or2; }
| or2 '&' {$$ = ref Node(n_NOWAIT, $or2, nil, nil, nil); }
cmdsan: cmdsa
| or2 '\n' {$$ = $or2; }
or2: and2
| or2 OROR and2 {
$$ = mk(n_ADJ,
mk(n_ADJ,
ref Node(n_WORD,nil,nil,"or",nil),
mk(n_BLOCK, $or2, nil)
),
mk(n_BLOCK,$and2,nil)
);
}
and2: pipe
| and2 ANDAND pipe {
$$ = mk(n_ADJ,
mk(n_ADJ,
ref Node(n_WORD,nil,nil,"and",nil),
mk(n_BLOCK, $and2, nil)
),
mk(n_BLOCK,$pipe,nil)
);
}
pipe: {$$ = nil;}
| cmd
| pipe '|' optnl cmd {$$ = ref Node(n_PIPE, $pipe, $cmd, nil, $2); }
cmd: simple
| redir cmd {$$ = mk(n_ADJ, $redir, $cmd); }
| redir
| assign
assign: word '=' assign {$$ = mk($2, $word, $assign); }
| word '=' simple {$$ = mk($2, $word, $simple); }
| word '=' {$$ = mk($2, $word, nil); }
redir: DUP {$$ = ref Node(n_DUP, nil, nil, nil, $DUP); }
| REDIR word {$$ = ref Node(n_REDIR, $word, nil, nil, $REDIR); }
simple: word
| simple word {$$ = mk(n_ADJ, $simple, $word); }
| simple redir {$$ = mk(n_ADJ, $simple, $redir); }
list: optnl {$$ = nil;}
| nlsimple optnl
nlsimple: optnl word {$$ = $word; }
| nlsimple optnl word {$$ = mk(n_ADJ, $nlsimple, $word); }
| nlsimple optnl redir {$$ = mk(n_ADJ, $nlsimple, $redir); }
word: comword
| word '^' optnl comword {$$ = mk(n_CONCAT, $word, $comword); }
comword: WORD {$$ = ref Node(n_WORD, nil, nil, $WORD, nil); }
| OP comword {$$ = mk($OP, $comword, nil); }
| '(' list ')' {$$ = mk(n_LIST, $list, nil); }
| '{' body '}' {$$ = mk(n_BLOCK, $body, nil); }
optnl: # null
| optnl '\n'
%%
EPERM: con "permission denied";
EPIPE: con "write on closed pipe";
#SHELLRC: con "lib/profile";
LIBSHELLRC: con "/lib/sh/profile";
BUILTINPATH: con "/dis/sh";
DEBUG: con 0;
ENVSEP: con 0; # word seperator in external environment
ENVHASHSIZE: con 7; # XXX profile usage of this...
OAPPEND: con 16r80000; # make sure this doesn't clash with O* constants in sys.m
OMASK: con 7;
usage()
{
sys->fprint(stderr(), "usage: sh [-ilexn] [-c command] [file [arg...]]\n");
raise "fail:usage";
}
badmodule(path: string)
{
sys->fprint(sys->fildes(2), "sh: cannot load %s: %r\n", path);
raise "fail:bad module" ;
}
initialise()
{
if (sys == nil) {
sys = load Sys Sys->PATH;
filepat = load Filepat Filepat->PATH;
if (filepat == nil) badmodule(Filepat->PATH);
str = load String String->PATH;
if (str == nil) badmodule(String->PATH);
bufio = load Bufio Bufio->PATH;
if (bufio == nil) badmodule(Bufio->PATH);
myself = load Sh "$self";
if (myself == nil) badmodule("$self(Sh)");
myselfbuiltin = load Shellbuiltin "$self";
if (myselfbuiltin == nil) badmodule("$self(Shellbuiltin)");
env = load Env Env->PATH;
}
}
blankopts: Options;
init(drawcontext: ref Draw->Context, argv: list of string)
{
initialise();
opts := blankopts;
if (argv != nil) {
if ((hd argv)[0] == '-')
opts.lflag++;
argv = tl argv;
}
interactive := 0;
loop: while (argv != nil && hd argv != nil && (hd argv)[0] == '-') {
for (i := 1; i < len hd argv; i++) {
c := (hd argv)[i];
case c {
'i' =>
interactive = Context.INTERACTIVE;
'l' =>
opts.lflag++; # login (read $home/lib/profile)
'n' =>
opts.nflag++; # don't fork namespace
'e' =>
opts.ctxtflags |= Context.ERROREXIT;
'x' =>
opts.ctxtflags |= Context.EXECPRINT;
'c' =>
arg: string;
if (i < len hd argv - 1) {
arg = (hd argv)[i + 1:];
} else if (tl argv == nil || hd tl argv == "") {
usage();
} else {
arg = hd tl argv;
argv = tl argv;
}
argv = tl argv;
opts.carg = arg;
continue loop;
}
}
argv = tl argv;
}
sys->pctl(Sys->FORKFD, nil);
if (!opts.nflag)
sys->pctl(Sys->FORKNS, nil);
ctxt := Context.new(drawcontext);
ctxt.setoptions(opts.ctxtflags, 1);
if (opts.carg != nil) {
status := ctxt.run(stringlist2list("{" + opts.carg + "}" :: argv), !interactive);
if (!interactive) {
if (status != nil)
raise "fail:" + status;
exit;
}
setstatus(ctxt, status);
}
# if login shell, run standard init script
if (opts.lflag)
runscript(ctxt, LIBSHELLRC, nil, 0);
if (argv == nil) {
# if (opts.lflag)
# runscript(ctxt, SHELLRC, nil, 0);
if (isconsole(sys->fildes(0)))
interactive |= ctxt.INTERACTIVE;
ctxt.setoptions(interactive, 1);
runfile(ctxt, sys->fildes(0), "stdin", nil);
} else {
ctxt.setoptions(interactive, 1);
runscript(ctxt, hd argv, stringlist2list(tl argv), 1);
}
}
# XXX should this refuse to parse a non braced-block?
parse(s: string): (ref Node, string)
{
initialise();
lex := YYLEX.initstring(s);
return doparse(lex, "", 0);
}
system(drawctxt: ref Draw->Context, cmd: string): string
{
initialise();
{
(n, err) := parse(cmd);
if (err != nil)
return err;
if (n == nil)
return nil;
return Context.new(drawctxt).run(ref Listnode(n, nil) :: nil, 0);
} exception e {
"fail:*" =>
return failurestatus(e);
}
}
run(drawctxt: ref Draw->Context, argv: list of string): string
{
initialise();
{
return Context.new(drawctxt).run(stringlist2list(argv), 0);
} exception e {
"fail:*" =>
return failurestatus(e);
}
}
isconsole(fd: ref Sys->FD): int
{
(ok1, d1) := sys->fstat(fd);
(ok2, d2) := sys->stat("/dev/cons");
if (ok1 < 0 || ok2 < 0)
return 0;
return d1.dtype == d2.dtype && d1.qid.path == d2.qid.path;
}
# run commands from file _path_
runscript(ctxt: ref Context, path: string, args: list of ref Listnode, reporterr: int)
{
{
fd := sys->open(path, Sys->OREAD);
if (fd != nil)
runfile(ctxt, fd, path, args);
else if (reporterr)
ctxt.fail("bad script path", sys->sprint("sh: cannot open %s: %r", path));
} exception {
"fail:*" =>
if(!reporterr)
return;
raise;
}
}
# run commands from the opened file fd.
# if interactive is non-zero, print a command prompt at appropriate times.
runfile(ctxt: ref Context, fd: ref Sys->FD, path: string, args: list of ref Listnode)
{
ctxt.push();
{
ctxt.setlocal("0", stringlist2list(path :: nil));
ctxt.setlocal("*", args);
lex := YYLEX.initfile(fd, path);
if (DEBUG) debug(sprint("parse(interactive == %d)", (ctxt.options() & ctxt.INTERACTIVE) != 0));
prompt := "" :: "" :: nil;
laststatus: string;
while (!lex.eof) {
interactive := ctxt.options() & ctxt.INTERACTIVE;
if (interactive) {
prompt = list2stringlist(ctxt.get("prompt"));
if (prompt == nil)
prompt = "; " :: "" :: nil;
sys->fprint(stderr(), "%s", hd prompt);
if (tl prompt == nil) {
prompt = hd prompt :: "" :: nil;
}
}
(n, err) := doparse(lex, hd tl prompt, !interactive);
if (err != nil) {
sys->fprint(stderr(), "sh: %s\n", err);
if (!interactive)
raise "fail:parse error";
} else if (n != nil) {
if (interactive) {
{
laststatus = walk(ctxt, n, 0);
} exception e2 {
"fail:*" =>
laststatus = failurestatus(e2);
}
} else
laststatus = walk(ctxt, n, 0);
setstatus(ctxt, laststatus);
if ((ctxt.options() & ctxt.ERROREXIT) && laststatus != nil)
break;
}
}
if (laststatus != nil)
raise "fail:" + laststatus;
ctxt.pop();
}
exception {
"fail:*" =>
ctxt.pop();
raise;
}
}
nonexistent(e: string): int
{
errs := array[] of {"does not exist", "directory entry not found"};
for (i := 0; i < len errs; i++){
j := len errs[i];
if (j <= len e && e[len e-j:] == errs[i])
return 1;
}
return 0;
}
Redirword: adt {
fd: ref Sys->FD;
w: string;
r: Redir;
};
Redirlist: adt {
r: list of Redirword;
};
# a hack so that the structure of walk() doesn't change much
# to accomodate echo|wc&
# transform the above into {echo|wc}$*&
# which should amount to exactly the same thing.
pipe2cmd(n: ref Node): ref Node
{
if (n == nil || n.ntype != n_PIPE)
return n;
return mk(n_ADJ, mk(n_BLOCK,n,nil), mk(n_VAR,ref Node(n_WORD,nil,nil,"*",nil),nil));
}
# walk a node tree.
# last is non-zero if this walk is the last action
# this shell process will take before exiting (i.e. redirections
# don't require a new process to avoid side effects)
walk(ctxt: ref Context, n: ref Node, last: int): string
{
if (DEBUG) debug(sprint("walking: %s", cmd2string(n)));
# avoid tail recursion stack explosion
while (n != nil && n.ntype == n_SEQ) {
status := walk(ctxt, n.left, 0);
if (ctxt.options() & ctxt.ERROREXIT && status != nil)
raise "fail:" + status;
setstatus(ctxt, status);
n = n.right;
}
if (n == nil)
return nil;
case (n.ntype) {
n_PIPE =>
return waitfor(ctxt, walkpipeline(ctxt, n, nil, -1));
n_ASSIGN or n_LOCAL =>
assign(ctxt, n);
return nil;
* =>
bg := 0;
if (n.ntype == n_NOWAIT) {
bg = 1;
n = pipe2cmd(n.left);
}
redirs := ref Redirlist(nil);
line := glob(glom(ctxt, n, redirs, nil));
if (bg) {
startchan := chan of (int, ref Expropagate);
spawn runasync(ctxt, 1, line, redirs, startchan);
(pid, nil) := <-startchan;
redirs = nil;
if (DEBUG) debug("started background process "+ string pid);
ctxt.set("apid", ref Listnode(nil, string pid) :: nil);
return nil;
} else {
return runsync(ctxt, line, redirs, last);
}
}
}
assign(ctxt: ref Context, n: ref Node): list of ref Listnode
{
redirs := ref Redirlist;
val: list of ref Listnode;
if (n.right != nil && (n.right.ntype == n_ASSIGN || n.right.ntype == n_LOCAL))
val = assign(ctxt, n.right);
else
val = glob(glom(ctxt, n.right, redirs, nil));
vars := glom(ctxt, n.left, redirs, nil);
if (vars == nil)
ctxt.fail("bad assign", "sh: nil variable name");
if (redirs.r != nil)
ctxt.fail("bad assign", "sh: redirections not allowed in assignment");
tval := val;
for (; vars != nil; vars = tl vars) {
vname := deglob((hd vars).word);
if (vname == nil)
ctxt.fail("bad assign", "sh: bad variable name");
v: list of ref Listnode = nil;
if (tl vars == nil)
v = tval;
else if (tval != nil)
v = hd tval :: nil;
if (n.ntype == n_ASSIGN)
ctxt.set(vname, v);
else
ctxt.setlocal(vname, v);
if (tval != nil)
tval = tl tval;
}
return val;
}
walkpipeline(ctxt: ref Context, n: ref Node, wrpipe: ref Sys->FD, wfdno: int): list of int
{
if (n == nil)
return nil;
fds := array[2] of ref Sys->FD;
pids: list of int;
rfdno := -1;
if (n.ntype == n_PIPE) {
if (sys->pipe(fds) == -1)
ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
nwfdno := -1;
if (n.redir != nil) {
(fd1, fd2) := (n.redir.fd2, n.redir.fd1);
if (fd2 == -1)
(fd1, fd2) = (fd2, fd1);
(nwfdno, rfdno) = (fd2, fd1);
}
pids = walkpipeline(ctxt, n.left, fds[1], nwfdno);
fds[1] = nil;
n = n.right;
}
r := ref Redirlist(nil);
rlist := glob(glom(ctxt, n, r, nil));
if (fds[0] != nil) {
if (rfdno == -1)
rfdno = 0;
r.r = Redirword(fds[0], nil, Redir(Sys->OREAD, rfdno, -1)) :: r.r;
}
if (wrpipe != nil) {
if (wfdno == -1)
wfdno = 1;
r.r = Redirword(wrpipe, nil, Redir(Sys->OWRITE, wfdno, -1)) :: r.r;
}
startchan := chan of (int, ref Expropagate);
spawn runasync(ctxt, 1, rlist, r, startchan);
(pid, nil) := <-startchan;
if (DEBUG) debug("started pipe process "+string pid);
return pid :: pids;
}
makeredir(f: string, mode: int, fd: int): Redirword
{
return Redirword(nil, f, Redir(mode, fd, -1));
}
# expand substitution operators in a node list
glom(ctxt: ref Context, n: ref Node, redirs: ref Redirlist, onto: list of ref Listnode)
: list of ref Listnode
{
if (n == nil) return nil;
if (n.ntype != n_ADJ)
return listjoin(glomoperation(ctxt, n, redirs), onto);
nlist := glom(ctxt, n.right, redirs, onto);
if (n.left.ntype != n_ADJ) {
# if it's a terminal node
nlist = listjoin(glomoperation(ctxt, n.left, redirs), nlist);
} else
nlist = glom(ctxt, n.left, redirs, nlist);
return nlist;
}
listjoin(left, right: list of ref Listnode): list of ref Listnode
{
l: list of ref Listnode;
for (; left != nil; left = tl left)
l = hd left :: l;
for (; l != nil; l = tl l)
right = hd l :: right;
return right;
}
pipecmd(ctxt: ref Context, cmd: list of ref Listnode, redir: ref Redir): ref Sys->FD
{
if(redir.fd2 != -1 || (redir.rtype & OAPPEND))
ctxt.fail("bad redir", "sh: bad redirection");
r := *redir;
case redir.rtype {
Sys->OREAD =>
r.rtype = Sys->OWRITE;
Sys->OWRITE =>
r.rtype = Sys->OREAD;
}
p := array[2] of ref Sys->FD;
if(sys->pipe(p) == -1)
ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
startchan := chan of (int, ref Expropagate);
spawn runasync(ctxt, 1, cmd, ref Redirlist((p[1], nil, r) :: nil), startchan);
p[1] = nil;
<-startchan;
return p[0];
}
glomoperation(ctxt: ref Context, n: ref Node, redirs: ref Redirlist): list of ref Listnode
{
if (n == nil)
return nil;
nlist: list of ref Listnode;
case n.ntype {
n_WORD =>
nlist = ref Listnode(nil, n.word) :: nil;
n_REDIR =>
wlist := glob(glom(ctxt, n.left, ref Redirlist(nil), nil));
if (len wlist != 1)
ctxt.fail("bad redir", "sh: single redirection operand required");
if((hd wlist).cmd != nil){
fd := pipecmd(ctxt, wlist, n.redir);
redirs.r = Redirword(fd, nil, (n.redir.rtype, fd.fd, -1)) :: redirs.r;
nlist = ref Listnode(nil, "/fd/"+string fd.fd) :: nil;
}else{
redirs.r = Redirword(nil, (hd wlist).word, *n.redir) :: redirs.r;
}
n_DUP =>
redirs.r = Redirword(nil, "", *n.redir) :: redirs.r;
n_LIST =>
nlist = glom(ctxt, n.left, redirs, nil);
n_CONCAT =>
nlist = concat(ctxt, glom(ctxt, n.left, redirs, nil), glom(ctxt, n.right, redirs, nil));
n_VAR or n_SQUASH or n_COUNT =>
arg := glom(ctxt, n.left, ref Redirlist(nil), nil);
if (len arg == 1 && (hd arg).cmd != nil)
nlist = subsbuiltin(ctxt, (hd arg).cmd.left);
else if (len arg != 1 || (hd arg).word == nil)
ctxt.fail("bad $ arg", "sh: bad variable name");
else
nlist = ctxt.get(deglob((hd arg).word));
case n.ntype {
n_VAR =>;
n_COUNT =>
nlist = ref Listnode(nil, string len nlist) :: nil;
n_SQUASH =>
# XXX could squash with first char of $ifs, perhaps
nlist = ref Listnode(nil, squash(list2stringlist(nlist), " ")) :: nil;
}
n_BQ or n_BQ2 =>
arg := glom(ctxt, n.left, ref Redirlist(nil), nil);
seps := "";
if (n.ntype == n_BQ) {
seps = squash(list2stringlist(ctxt.get("ifs")), "");
if (seps == nil)
seps = " \t\n\r";
}
(nlist, nil) = bq(ctxt, glob(arg), seps);
n_BLOCK =>
nlist = ref Listnode(n, "") :: nil;
n_ASSIGN or n_LOCAL =>
ctxt.fail("bad assign", "sh: assignment in invalid context");
* =>
panic("bad node type "+string n.ntype+" in glomop");
}
return nlist;
}
subsbuiltin(ctxt: ref Context, n: ref Node): list of ref Listnode
{
if (n == nil || n.ntype == n_SEQ ||
n.ntype == n_PIPE || n.ntype == n_NOWAIT)
ctxt.fail("bad $ arg", "sh: invalid argument to ${} operator");
r := ref Redirlist;
cmd := glob(glom(ctxt, n, r, nil));
if (r.r != nil)
ctxt.fail("bad $ arg", "sh: redirection not allowed in substitution");
r = nil;
if (cmd == nil || (hd cmd).word == nil || (hd cmd).cmd != nil)
ctxt.fail("bad $ arg", "sh: bad builtin name");
(nil, bmods) := findbuiltin(ctxt.env.sbuiltins, (hd cmd).word);
if (bmods == nil)
ctxt.fail("builtin not found",
sys->sprint("sh: builtin %s not found", (hd cmd).word));
return (hd bmods)->runsbuiltin(ctxt, myself, cmd);
}
#
# backquote substitution (could be done in a builtin)
#
getbq(nil: ref Context, fd: ref Sys->FD, seps: string): list of ref Listnode
{
buf := array[Sys->ATOMICIO] of byte;
buflen := 0;
while ((n := sys->read(fd, buf[buflen:], len buf - buflen)) > 0) {
buflen += n;
if (buflen == len buf) {
nbuf := array[buflen * 2] of byte;
nbuf[0:] = buf[0:];
buf = nbuf;
}
}
l: list of string;
if (seps != nil)
(nil, l) = sys->tokenize(string buf[0:buflen], seps);
else
l = string buf[0:buflen] :: nil;
buf = nil;
return stringlist2list(l);
}
bq(ctxt: ref Context, cmd: list of ref Listnode, seps: string): (list of ref Listnode, string)
{
fds := array[2] of ref Sys->FD;
if (sys->pipe(fds) == -1)
ctxt.fail("no pipe", sys->sprint("sh: cannot make pipe: %r"));
r := rdir(fds[1]);
fds[1] = nil;
startchan := chan of (int, ref Expropagate);
spawn runasync(ctxt, 0, cmd, r, startchan);
(exepid, exprop) := <-startchan;
r = nil;
bqlist := getbq(ctxt, fds[0], seps);
waitfor(ctxt, exepid :: nil);
if (exprop.name != nil)
raise exprop.name;
return (bqlist, nil);
}
# get around compiler temporaries bug
rdir(fd: ref Sys->FD): ref Redirlist
{
return ref Redirlist(Redirword(fd, nil, Redir(Sys->OWRITE, 1, -1)) :: nil);
}
#
# concatenation
#
concatwords(p1, p2: ref Listnode): ref Listnode
{
if (p1.word == nil && p1.cmd != nil)
p1.word = cmd2string(p1.cmd);
if (p2.word == nil && p2.cmd != nil)
p2.word = cmd2string(p2.cmd);
return ref Listnode(nil, p1.word + p2.word);
}
concat(ctxt: ref Context, nl1, nl2: list of ref Listnode): list of ref Listnode
{
if (nl1 == nil || nl2 == nil) {
if (nl1 == nil && nl2 == nil)
return nil;
ctxt.fail("bad concatenation", "sh: null list in concatenation");
}
ret: list of ref Listnode;
if (tl nl1 == nil || tl nl2 == nil) {
for (p1 := nl1; p1 != nil; p1 = tl p1)
for (p2 := nl2; p2 != nil; p2 = tl p2)
ret = concatwords(hd p1, hd p2) :: ret;
} else {
if (len nl1 != len nl2)
ctxt.fail("bad concatenation", "sh: lists of differing sizes can't be concatenated");
while (nl1 != nil) {
ret = concatwords(hd nl1, hd nl2) :: ret;
(nl1, nl2) = (tl nl1, tl nl2);
}
}
return revlist(ret);
}
Expropagate: adt {
name: string;
};
# run an asynchronous process, first redirecting its I/O
# as specified in _redirs_.
# it sends its process ID down _startchan_ before executing.
# it has to jump through one or two hoops to make sure
# Sys->FD ref counting is done correctly. this code
# is more sensitive than you might think.
runasync(ctxt: ref Context, copyenv: int, argv: list of ref Listnode, redirs: ref Redirlist,
startchan: chan of (int, ref Expropagate))
{
status: string;
pid := sys->pctl(sys->FORKFD, nil);
if (DEBUG) debug(sprint("in async (len redirs: %d)", len redirs.r));
ctxt = ctxt.copy(copyenv);
exprop := ref Expropagate;
{
newfdl := doredirs(ctxt, redirs);
redirs = nil;
if (newfdl != nil)
sys->pctl(Sys->NEWFD, newfdl);
# stop the old waitfd from holding the intermediate
# file descriptor group open.
ctxt.waitfd = waitfd();
# N.B. it's important that the sync is done here, not
# before doredirs, as otherwise there's some sort of
# race condition that leads to pipe non-completion.
startchan <-= (pid, exprop);
startchan = nil;
status = ctxt.run(argv, copyenv);
} exception e {
"fail:*" =>
exprop.name = e;
if (startchan != nil)
startchan <-= (pid, exprop);
raise e;
}
if (status != nil) {
# don't propagate bad status as an exception.
raise "fail:" + status;
}
}
# run a synchronous process
runsync(ctxt: ref Context, argv: list of ref Listnode,
redirs: ref Redirlist, last: int): string
{
if (DEBUG) debug(sys->sprint("in sync (len redirs: %d; last: %d)", len redirs.r, last));
if (redirs.r != nil && !last) {
# a new process is required to shield redirection side effects
startchan := chan of (int, ref Expropagate);
spawn runasync(ctxt, 0, argv, redirs, startchan);
(pid, exprop) := <-startchan;
redirs = nil;
r := waitfor(ctxt, pid :: nil);
if (exprop.name != nil)
raise exprop.name;
return r;
} else {
newfdl := doredirs(ctxt, redirs);
redirs = nil;
if (newfdl != nil)
sys->pctl(Sys->NEWFD, newfdl);
return ctxt.run(argv, last);
}
}
# path is prefixed with: "/", "#", "./" or "../"
absolute(p: string): int
{
if (len p < 2)
return 0;
if (p[0] == '/' || p[0] == '#')
return 1;
if (len p < 3 || p[0] != '.')
return 0;
if (p[1] == '/')
return 1;
if (p[1] == '.' && p[2] == '/')
return 1;
return 0;
}
runexternal(ctxt: ref Context, args: list of ref Listnode, last: int): string
{
progname := (hd args).word;
disfile := 0;
if (len progname >= 4 && progname[len progname-4:] == ".dis")
disfile = 1;
pathlist: list of string;
if (absolute(progname))
pathlist = list of {""};
else if ((pl := ctxt.get("path")) != nil)
pathlist = list2stringlist(pl);
else
pathlist = list of {"/dis", "."};
err := "";
do {
path: string;
if (hd pathlist != "")
path = hd pathlist + "/" + progname;
else
path = progname;
npath := path;
if (!disfile)
npath += ".dis";
mod := load Command npath;
if (mod != nil) {
argv := list2stringlist(args);
export(ctxt.env.localenv);
if (last) {
{
sys->pctl(Sys->NEWFD, ctxt.keepfds);
mod->init(ctxt.drawcontext, argv);
exit;
} exception e {
EPIPE =>
return EPIPE;
"fail:*" =>
return failurestatus(e);
}
}
extstart := chan of int;
spawn externalexec(mod, ctxt.drawcontext, argv, extstart, ctxt.keepfds);
pid := <-extstart;
if (DEBUG) debug("started external externalexec; pid is "+string pid);
return waitfor(ctxt, pid :: nil);
}
err = sys->sprint("%r");
if (nonexistent(err)) {
# try and run it as a shell script
if (!disfile && (fd := sys->open(path, Sys->OREAD)) != nil) {
(ok, info) := sys->fstat(fd);
# make permission checking more accurate later
if (ok == 0 && (info.mode & Sys->DMDIR) == 0
&& (info.mode & 8r111) != 0)
return runhashpling(ctxt, fd, path, tl args, last);
};
err = sys->sprint("%r");
}
pathlist = tl pathlist;
} while (pathlist != nil && nonexistent(err));
diagnostic(ctxt, sys->sprint("%s: %s", progname, err));
return err;
}
failurestatus(e: string): string
{
s := e[5:];
while(s != nil && (s[0] == ' ' || s[0] == '\t'))
s = s[1:];
if(s != nil)
return s;
return "failed";
}
runhashpling(ctxt: ref Context, fd: ref Sys->FD,
path: string, argv: list of ref Listnode, last: int): string
{
header := array[1024] of byte;
n := sys->read(fd, header, len header);
for (i := 0; i < n; i++)
if (header[i] == byte '\n')
break;
if (i == n || i < 3 || header[0] != byte('#') || header[1] != byte('!')) {
diagnostic(ctxt, "bad script header on " + path);
return "bad header";
}
(nil, args) := sys->tokenize(string header[2:i], " \t");
if (args == nil) {
diagnostic(ctxt, "empty header on " + path);
return "bad header";
}
header = nil;
fd = nil;
nargs: list of ref Listnode;
for (; args != nil; args = tl args)
nargs = ref Listnode(nil, hd args) :: nargs;
nargs = ref Listnode(nil, path) :: nargs;
for (; argv != nil; argv = tl argv)
nargs = hd argv :: nargs;
return runexternal(ctxt, revlist(nargs), last);
}
runblock(ctxt: ref Context, args: list of ref Listnode, last: int): string
{
# block execute (we know that hd args represents a block)
cmd := (hd args).cmd;
if (cmd == nil) {
# parse block from first argument
lex := YYLEX.initstring((hd args).word);
err: string;
(cmd, err) = doparse(lex, "", 0);
if (cmd == nil)
ctxt.fail("parse error", "sh: "+err);
(hd args).cmd = cmd;
}
# now we've got a parsed block
ctxt.push();
{
ctxt.setlocal("0", hd args :: nil);
ctxt.setlocal("*", tl args);
if (cmd != nil && cmd.ntype == n_BLOCK)
cmd = cmd.left;
status := walk(ctxt, cmd, last);
ctxt.pop();
return status;
} exception {
"fail:*" =>
ctxt.pop();
raise;
}
}
# return (ok, val) where ok is non-zero is builtin was found,
# val is return status of builtin
trybuiltin(ctxt: ref Context, args: list of ref Listnode, lseq: int)
: (int, string)
{
(nil, bmods) := findbuiltin(ctxt.env.builtins, (hd args).word);
if (bmods == nil)
return (0, nil);
return (1, (hd bmods)->runbuiltin(ctxt, myself, args, lseq));
}
keepfdstr(ctxt: ref Context): string
{
s := "";
for (f := ctxt.keepfds; f != nil; f = tl f) {
s += string hd f;
if (tl f != nil)
s += ",";
}
return s;
}
externalexec(mod: Command,
drawcontext: ref Draw->Context, argv: list of string, startchan: chan of int, keepfds: list of int)
{
if (DEBUG) debug(sprint("externalexec(%s,... [%d args])", hd argv, len argv));
sys->pctl(Sys->NEWFD, keepfds);
startchan <-= sys->pctl(0, nil);
{
mod->init(drawcontext, argv);
}
exception {
EPIPE =>
raise "fail:" + EPIPE;
}
}
dup(ctxt: ref Context, fd1, fd2: int): int
{
# shuffle waitfd out of the way if it's being attacked
if (ctxt.waitfd.fd == fd2) {
ctxt.waitfd = waitfd();
if (ctxt.waitfd.fd == fd2)
panic(sys->sprint("reopen of waitfd gave same fd (%d)", ctxt.waitfd.fd));
}
return sys->dup(fd1, fd2);
}
# with thanks to tiny/sh.b
# return error status if redirs failed
doredirs(ctxt: ref Context, redirs: ref Redirlist): list of int
{
if (redirs.r == nil)
return nil;
keepfds := ctxt.keepfds;
rl := redirs.r;
redirs = nil;
for (; rl != nil; rl = tl rl) {
(rfd, path, (mode, fd1, fd2)) := hd rl;
if (path == nil && rfd == nil) {
# dup
if (fd1 == -1 || fd2 == -1)
ctxt.fail("bad redir", "sh: invalid dup");
if (dup(ctxt, fd2, fd1) == -1)
ctxt.fail("bad redir", sys->sprint("sh: cannot dup: %r"));
keepfds = fd1 :: keepfds;
continue;
}
# redir
if (fd1 == -1) {
if ((mode & OMASK) == Sys->OWRITE)
fd1 = 1;
else
fd1 = 0;
}
if (rfd == nil) {
(append, omode) := (mode & OAPPEND, mode & ~OAPPEND);
err := "";
case mode {
Sys->OREAD =>
rfd = sys->open(path, omode);
Sys->OWRITE | OAPPEND or
Sys->ORDWR =>
rfd = sys->open(path, omode);
err = sprint("%r");
if (rfd == nil && nonexistent(err)) {
rfd = sys->create(path, omode, 8r666);
err = nil;
}
Sys->OWRITE =>
rfd = sys->create(path, omode, 8r666);
err = sprint("%r");
if (rfd == nil && err == EPERM) {
# try open; can't create on a file2chan (pipe)
rfd = sys->open(path, omode);
nerr := sprint("%r");
if(!nonexistent(nerr))
err = nerr;
}
}
if (rfd == nil) {
if (err == nil)
err = sprint("%r");
ctxt.fail("bad redir", sys->sprint("sh: cannot open %s: %s", path, err));
}
if (append)
sys->seek(rfd, big 0, Sys->SEEKEND); # not good enough, but alright for some purposes.
}
# XXX what happens if rfd.fd == fd1?
# it probably gets closed automatically... which is not what we want!
dup(ctxt, rfd.fd, fd1);
keepfds = fd1 :: keepfds;
}
ctxt.keepfds = keepfds;
return ctxt.waitfd.fd :: keepfds;
}
#
# waiter utility routines
#
waitfd(): ref Sys->FD
{
wf := string sys->pctl(0, nil) + "/wait";
waitfd := sys->open("#p/"+wf, Sys->OREAD);
if (waitfd == nil)
waitfd = sys->open("/prog/"+wf, Sys->OREAD);
if (waitfd == nil)
panic(sys->sprint("cannot open wait file: %r"));
return waitfd;
}
waitfor(ctxt: ref Context, pids: list of int): string
{
if (pids == nil)
return nil;
status := array[len pids] of string;
wcount := len status;
buf := array[Sys->WAITLEN] of byte;
onebad := 0;
for(;;){
n := sys->read(ctxt.waitfd, buf, len buf);
if(n < 0)
panic(sys->sprint("error on wait read: %r"));
(who, line, s) := parsewaitstatus(ctxt, string buf[0:n]);
if (s != nil) {
if (len s >= 5 && s[0:5] == "fail:")
s = failurestatus(s);
else
diagnostic(ctxt, line);
}
for ((i, pl) := (0, pids); pl != nil; (i, pl) = (i+1, tl pl))
if (who == hd pl)
break;
if (i < len status) {
# wait returns two records for a killed process...
if (status[i] == nil || s != "killed") {
onebad += s != nil;
status[i] = s;
if (wcount-- <= 1)
break;
}
}
}
if (!onebad)
return nil;
r := status[len status - 1];
for (i := len status - 2; i >= 0; i--)
r += "|" + status[i];
return r;
}
parsewaitstatus(ctxt: ref Context, status: string): (int, string, string)
{
for (i := 0; i < len status; i++)
if (status[i] == ' ')
break;
if (i == len status - 1 || status[i+1] != '"')
ctxt.fail("bad wait read",
sys->sprint("sh: bad exit status '%s'", status));
for (i+=2; i < len status; i++)
if (status[i] == '"')
break;
if (i > len status - 2 || status[i+1] != ':')
ctxt.fail("bad wait read",
sys->sprint("sh: bad exit status '%s'", status));
return (int status, status, status[i+2:]);
}
panic(s: string)
{
sys->fprint(stderr(), "sh panic: %s\n", s);
raise "panic";
}
diagnostic(ctxt: ref Context, s: string)
{
if (ctxt.options() & Context.VERBOSE)
sys->fprint(stderr(), "sh: %s\n", s);
}
#
# Sh environment stuff
#
Context.new(drawcontext: ref Draw->Context): ref Context
{
initialise();
if (env != nil)
env->clone();
ctxt := ref Context(
ref Environment(
ref Builtins(nil, 0),
ref Builtins(nil, 0),
nil,
newlocalenv(nil)
),
waitfd(),
drawcontext,
0 :: 1 :: 2 :: nil
);
myselfbuiltin->initbuiltin(ctxt, myself);
ctxt.env.localenv.flags = ctxt.VERBOSE;
for (vl := ctxt.get("autoload"); vl != nil; vl = tl vl)
if ((hd vl).cmd == nil && (hd vl).word != nil)
loadmodule(ctxt, (hd vl).word);
return ctxt;
}
Context.copy(ctxt: self ref Context, copyenv: int): ref Context
{
# XXX could check to see that we are definitely in a
# new process, because there'll be problems if not (two processes
# simultaneously reading the same wait file)
nctxt := ref Context(ctxt.env, waitfd(), ctxt.drawcontext, ctxt.keepfds);
if (copyenv) {
if (env != nil)
env->clone();
nctxt.env = ref Environment(
copybuiltins(ctxt.env.sbuiltins),
copybuiltins(ctxt.env.builtins),
ctxt.env.bmods,
copylocalenv(ctxt.env.localenv)
);
}
return nctxt;
}
Context.set(ctxt: self ref Context, name: string, val: list of ref Listnode)
{
e := ctxt.env.localenv;
idx := hashfn(name, len e.vars);
for (;;) {
v := hashfind(e.vars, idx, name);
if (v == nil) {
if (e.pushed == nil) {
flags := Var.CHANGED;
if (noexport(name))
flags |= Var.NOEXPORT;
hashadd(e.vars, idx, ref Var(name, val, flags));
return;
}
} else {
v.val = val;
v.flags |= Var.CHANGED;
return;
}
e = e.pushed;
}
}
Context.get(ctxt: self ref Context, name: string): list of ref Listnode
{
if (name == nil)
return nil;
idx := -1;
# cope with $1, $2, etc
if (name[0] > '0' && name[0] <= '9') {
i: int;
for (i = 0; i < len name; i++)
if (name[i] < '0' || name[i] > '9')
break;
if (i >= len name) {
idx = int name - 1;
name = "*";
}
}
v := varfind(ctxt.env.localenv, name);
if (v != nil) {
if (idx != -1)
return index(v.val, idx);
return v.val;
}
return nil;
}
# return the whole environment.
Context.envlist(ctxt: self ref Context): list of (string, list of ref Listnode)
{
t := array[ENVHASHSIZE] of list of ref Var;
for (e := ctxt.env.localenv; e != nil; e = e.pushed) {
for (i := 0; i < len e.vars; i++) {
for (vl := e.vars[i]; vl != nil; vl = tl vl) {
v := hd vl;
idx := hashfn(v.name, len e.vars);
if (hashfind(t, idx, v.name) == nil)
hashadd(t, idx, v);
}
}
}
l: list of (string, list of ref Listnode);
for (i := 0; i < ENVHASHSIZE; i++) {
for (vl := t[i]; vl != nil; vl = tl vl) {
v := hd vl;
l = (v.name, v.val) :: l;
}
}
return l;
}
Context.setlocal(ctxt: self ref Context, name: string, val: list of ref Listnode)
{
e := ctxt.env.localenv;
idx := hashfn(name, len e.vars);
v := hashfind(e.vars, idx, name);
if (v == nil) {
flags := Var.CHANGED;
if (noexport(name))
flags |= Var.NOEXPORT;
hashadd(e.vars, idx, ref Var(name, val, flags));
} else {
v.val = val;
v.flags |= Var.CHANGED;
}
}
Context.push(ctxt: self ref Context)
{
ctxt.env.localenv = newlocalenv(ctxt.env.localenv);
}
Context.pop(ctxt: self ref Context)
{
if (ctxt.env.localenv.pushed == nil)
panic("unbalanced contexts in shell environment");
else {
oldv := ctxt.env.localenv.vars;
ctxt.env.localenv = ctxt.env.localenv.pushed;
for (i := 0; i < len oldv; i++) {
for (vl := oldv[i]; vl != nil; vl = tl vl) {
if ((v := varfind(ctxt.env.localenv, (hd vl).name)) != nil)
v.flags |= Var.CHANGED;
else
ctxt.set((hd vl).name, nil);
}
}
}
}
Context.run(ctxt: self ref Context, args: list of ref Listnode, last: int): string
{
if (args == nil || ((hd args).cmd == nil && (hd args).word == nil))
return nil;
cmd := hd args;
if (cmd.cmd != nil || cmd.word[0] == '{') # }
return runblock(ctxt, args, last);
if (ctxt.options() & ctxt.EXECPRINT)
sys->fprint(stderr(), "%s\n", quoted(args, 0));
(doneit, status) := trybuiltin(ctxt, args, last);
if (!doneit)
status = runexternal(ctxt, args, last);
return status;
}
Context.addmodule(ctxt: self ref Context, name: string, mod: Shellbuiltin)
{
mod->initbuiltin(ctxt, myself);
ctxt.env.bmods = (name, mod->getself()) :: ctxt.env.bmods;
}
Context.addbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
{
addbuiltin(c.env.builtins, name, mod);
}
Context.removebuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
{
removebuiltin(c.env.builtins, name, mod);
}
Context.addsbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
{
addbuiltin(c.env.sbuiltins, name, mod);
}
Context.removesbuiltin(c: self ref Context, name: string, mod: Shellbuiltin)
{
removebuiltin(c.env.sbuiltins, name, mod);
}
varfind(e: ref Localenv, name: string): ref Var
{
idx := hashfn(name, len e.vars);
for (; e != nil; e = e.pushed)
for (vl := e.vars[idx]; vl != nil; vl = tl vl)
if ((hd vl).name == name)
return hd vl;
return nil;
}
Context.fail(ctxt: self ref Context, ename: string, err: string)
{
if (ctxt.options() & Context.VERBOSE)
sys->fprint(stderr(), "%s\n", err);
raise "fail:" + ename;
}
Context.setoptions(ctxt: self ref Context, flags, on: int): int
{
old := ctxt.env.localenv.flags;
if (on)
ctxt.env.localenv.flags |= flags;
else
ctxt.env.localenv.flags &= ~flags;
return old;
}
Context.options(ctxt: self ref Context): int
{
return ctxt.env.localenv.flags;
}
hashfn(s: string, n: int): int
{
h := 0;
m := len s;
for(i:=0; i<m; i++){
h = 65599*h+s[i];
}
return (h & 16r7fffffff) % n;
}
# the following two functions cheat by getting the caller
# to calculate the actual hash function. this is to avoid
# the hash function being calculated once in every scope
# of a context until the variable is found (or stored).
hashfind(ht: array of list of ref Var, idx: int, n: string): ref Var
{
for (ent := ht[idx]; ent != nil; ent = tl ent)
if ((hd ent).name == n)
return hd ent;
return nil;
}
hashadd(ht: array of list of ref Var, idx: int, v: ref Var)
{
ht[idx] = v :: ht[idx];
}
copylocalenv(e: ref Localenv): ref Localenv
{
nvars := array[len e.vars] of list of ref Var;
flags := e.flags;
for (; e != nil; e = e.pushed)
for (i := 0; i < len nvars; i++)
for (vl := e.vars[i]; vl != nil; vl = tl vl) {
idx := hashfn((hd vl).name, len nvars);
if (hashfind(nvars, idx, (hd vl).name) == nil)
hashadd(nvars, idx, ref *(hd vl));
}
return ref Localenv(nvars, nil, flags);
}
# make new local environment. if it's got no pushed levels,
# then get all variables from the global environment.
newlocalenv(pushed: ref Localenv): ref Localenv
{
e := ref Localenv(array[ENVHASHSIZE] of list of ref Var, pushed, 0);
if (pushed == nil && env != nil) {
for (vl := env->getall(); vl != nil; vl = tl vl) {
(name, val) := hd vl;
hashadd(e.vars, hashfn(name, len e.vars), ref Var(name, envstringtoval(val), 0));
}
}
if (pushed != nil)
e.flags = pushed.flags;
return e;
}
copybuiltins(b: ref Builtins): ref Builtins
{
nb := ref Builtins(array[b.n] of (string, list of Shellbuiltin), b.n);
nb.ba[0:] = b.ba[0:b.n];
return nb;
}
findbuiltin(b: ref Builtins, name: string): (int, list of Shellbuiltin)
{
lo := 0;
hi := b.n - 1;
while (lo <= hi) {
mid := (lo + hi) / 2;
(bname, bmod) := b.ba[mid];
if (name < bname)
hi = mid - 1;
else if (name > bname)
lo = mid + 1;
else
return (mid, bmod);
}
return (lo, nil);
}
removebuiltin(b: ref Builtins, name: string, mod: Shellbuiltin)
{
(n, bmods) := findbuiltin(b, name);
if (bmods == nil)
return;
if (hd bmods == mod) {
if (tl bmods != nil)
b.ba[n] = (name, tl bmods);
else {
b.ba[n:] = b.ba[n+1:b.n];
b.ba[--b.n] = (nil, nil);
}
}
}
# add builtin; if it already exists, then replace it. if mod is nil then remove it.
# builtins that refer to myselfbuiltin are special - they
# are never removed, neither are they entirely replaced, only covered.
# no external module can redefine the name "builtin"
addbuiltin(b: ref Builtins, name: string, mod: Shellbuiltin)
{
if (mod == nil || (name == "builtin" && mod != myselfbuiltin))
return;
(n, bmods) := findbuiltin(b, name);
if (bmods != nil) {
if (hd bmods == myselfbuiltin)
b.ba[n] = (name, mod :: bmods);
else
b.ba[n] = (name, mod :: nil);
} else {
if (b.n == len b.ba) {
nb := array[b.n + 10] of (string, list of Shellbuiltin);
nb[0:] = b.ba[0:b.n];
b.ba = nb;
}
b.ba[n+1:] = b.ba[n:b.n];
b.ba[n] = (name, mod :: nil);
b.n++;
}
}
removebuiltinmod(b: ref Builtins, mod: Shellbuiltin)
{
j := 0;
for (i := 0; i < b.n; i++) {
(name, bmods) := b.ba[i];
if (hd bmods == mod)
bmods = tl bmods;
if (bmods != nil)
b.ba[j++] = (name, bmods);
}
b.n = j;
for (; j < i; j++)
b.ba[j] = (nil, nil);
}
export(e: ref Localenv)
{
if (env == nil)
return;
if (e.pushed != nil)
export(e.pushed);
for (i := 0; i < len e.vars; i++) {
for (vl := e.vars[i]; vl != nil; vl = tl vl) {
v := hd vl;
# a bit inefficient: a local variable will get several putenvs.
if ((v.flags & Var.CHANGED) && !(v.flags & Var.NOEXPORT)) {
setenv(v.name, v.val);
v.flags &= ~Var.CHANGED;
}
}
}
}
noexport(name: string): int
{
case name {
"0" or "*" or "status" => return 1;
}
return 0;
}
index(val: list of ref Listnode, k: int): list of ref Listnode
{
for (; k > 0 && val != nil; k--)
val = tl val;
if (val != nil)
val = hd val :: nil;
return val;
}
getenv(name: string): list of ref Listnode
{
if (env == nil)
return nil;
return envstringtoval(env->getenv(name));
}
envstringtoval(v: string): list of ref Listnode
{
return stringlist2list(str->unquoted(v));
}
XXXenvstringtoval(v: string): list of ref Listnode
{
if (len v == 0)
return nil;
start := len v;
val: list of ref Listnode;
for (i := start - 1; i >= 0; i--) {
if (v[i] == ENVSEP) {
val = ref Listnode(nil, v[i+1:start]) :: val;
start = i;
}
}
return ref Listnode(nil, v[0:start]) :: val;
}
setenv(name: string, val: list of ref Listnode)
{
if (env == nil)
return;
env->setenv(name, quoted(val, 1));
}
#
# globbing and general wildcard handling
#
containswildchar(s: string): int
{
# try and avoid being fooled by GLOB characters in quoted
# text. we'll only be fooled if the GLOB char is followed
# by a wildcard char, or another GLOB.
for (i := 0; i < len s; i++) {
if (s[i] == GLOB && i < len s - 1) {
case s[i+1] {
'*' or '[' or '?' or GLOB =>
return 1;
}
}
}
return 0;
}
# remove GLOBs, and quote other wildcard characters
patquote(word: string): string
{
outword := "";
for (i := 0; i < len word; i++) {
case word[i] {
'[' or '*' or '?' or '\\' =>
outword[len outword] = '\\';
GLOB =>
i++;
if (i >= len word)
return outword;
if(word[i] == '[' && i < len word - 1 && word[i+1] == '~')
word[i+1] = '^';
}
outword[len outword] = word[i];
}
return outword;
}
# get rid of GLOB characters
deglob(s: string): string
{
j := 0;
for (i := 0; i < len s; i++) {
if (s[i] != GLOB) {
if (i != j) # a worthy optimisation???
s[j] = s[i];
j++;
}
}
if (i == j)
return s;
return s[0:j];
}
# expand wildcards in _nl_
glob(nl: list of ref Listnode): list of ref Listnode
{
new: list of ref Listnode;
while (nl != nil) {
n := hd nl;
if (containswildchar(n.word)) {
qword := patquote(n.word);
files := filepat->expand(qword);
if (files == nil)
files = deglob(n.word) :: nil;
while (files != nil) {
new = ref Listnode(nil, hd files) :: new;
files = tl files;
}
} else
new = n :: new;
nl = tl nl;
}
ret := revlist(new);
return ret;
}
#
# general list manipulation utility routines
#
# return string equivalent of nl
list2stringlist(nl: list of ref Listnode): list of string
{
ret: list of string = nil;
while (nl != nil) {
newel: string;
el := hd nl;
if (el.word != nil || el.cmd == nil)
newel = el.word;
else
el.word = newel = cmd2string(el.cmd);
ret = newel::ret;
nl = tl nl;
}
sl := revstringlist(ret);
return sl;
}
stringlist2list(sl: list of string): list of ref Listnode
{
ret: list of ref Listnode;
while (sl != nil) {
ret = ref Listnode(nil, hd sl) :: ret;
sl = tl sl;
}
return revlist(ret);
}
revstringlist(l: list of string): list of string
{
t: list of string;
while(l != nil) {
t = hd l :: t;
l = tl l;
}
return t;
}
revlist(l: list of ref Listnode): list of ref Listnode
{
t: list of ref Listnode;
while(l != nil) {
t = hd l :: t;
l = tl l;
}
return t;
}
#
# node to string conversion functions
#
fdassignstr(isassign: int, redir: ref Redir): string
{
l: string = nil;
if (redir.fd1 >= 0)
l = string redir.fd1;
if (isassign) {
r: string = nil;
if (redir.fd2 >= 0)
r = string redir.fd2;
return "[" + l + "=" + r + "]";
}
return "[" + l + "]";
}
redirstr(rtype: int): string
{
case rtype {
* or
Sys->OREAD => return "<";
Sys->OWRITE => return ">";
Sys->OWRITE|OAPPEND => return ">>";
Sys->ORDWR => return "<>";
}
}
cmd2string(n: ref Node): string
{
if (n == nil)
return "";
s: string;
case n.ntype {
n_BLOCK => s = "{" + cmd2string(n.left) + "}";
n_VAR => s = "$" + cmd2string(n.left);
# XXX can this ever occur?
if (n.right != nil)
s += "(" + cmd2string(n.right) + ")";
n_SQUASH => s = "$\"" + cmd2string(n.left);
n_COUNT => s = "$#" + cmd2string(n.left);
n_BQ => s = "`" + cmd2string(n.left);
n_BQ2 => s = "\"" + cmd2string(n.left);
n_REDIR => s = redirstr(n.redir.rtype);
if (n.redir.fd1 != -1)
s += fdassignstr(0, n.redir);
s += cmd2string(n.left);
n_DUP => s = redirstr(n.redir.rtype) + fdassignstr(1, n.redir);
n_LIST => s = "(" + cmd2string(n.left) + ")";
n_SEQ => s = cmd2string(n.left) + ";" + cmd2string(n.right);
n_NOWAIT => s = cmd2string(n.left) + "&";
n_CONCAT => s = cmd2string(n.left) + "^" + cmd2string(n.right);
n_PIPE => s = cmd2string(n.left) + "|";
if (n.redir != nil && (n.redir.fd1 != -1 || n.redir.fd2 != -1))
s += fdassignstr(n.redir.fd2 != -1, n.redir);
s += cmd2string(n.right);
n_ASSIGN => s = cmd2string(n.left) + "=" + cmd2string(n.right);
n_LOCAL => s = cmd2string(n.left) + ":=" + cmd2string(n.right);
n_ADJ => s = cmd2string(n.left) + " " + cmd2string(n.right);
n_WORD => s = quote(n.word, 1);
* => s = sys->sprint("unknown%d", n.ntype);
}
return s;
}
# convert s into a suitable format for reparsing.
# if glob is true, then GLOB chars are significant.
# XXX it might be faster in the more usual cases
# to run through the string first and only build up
# a new string once we've discovered it's necessary.
quote(s: string, glob: int): string
{
needquote := 0;
t := "";
for (i := 0; i < len s; i++) {
case s[i] {
'{' or '}' or '(' or ')' or '`' or '&' or ';' or '=' or '>' or '<' or '#' or
'|' or '*' or '[' or '?' or '$' or '^' or ' ' or '\t' or '\n' or '\r' =>
needquote = 1;
'\'' =>
t[len t] = '\'';
needquote = 1;
GLOB =>
if (glob) {
if (i < len s - 1)
i++;
}
}
t[len t] = s[i];
}
if (needquote || t == nil)
t = "'" + t + "'";
return t;
}
squash(l: list of string, sep: string): string
{
if (l == nil)
return nil;
s := hd l;
for (l = tl l; l != nil; l = tl l)
s += sep + hd l;
return s;
}
debug(s: string)
{
if (DEBUG) sys->fprint(stderr(), "%s\n", string sys->pctl(0, nil) + ": " + s);
}
#
# built-in commands
#
initbuiltin(c: ref Context, nil: Sh): string
{
names := array[] of {"load", "unload", "loaded", "builtin", "syncenv", "whatis", "run", "exit", "@"};
for (i := 0; i < len names; i++)
c.addbuiltin(names[i], myselfbuiltin);
c.addsbuiltin("loaded", myselfbuiltin);
c.addsbuiltin("quote", myselfbuiltin);
c.addsbuiltin("bquote", myselfbuiltin);
c.addsbuiltin("unquote", myselfbuiltin);
c.addsbuiltin("builtin", myselfbuiltin);
return nil;
}
whatis(nil: ref Sh->Context, nil: Sh, nil: string, nil: int): string
{
return nil;
}
runsbuiltin(ctxt: ref Context, nil: Sh, argv: list of ref Listnode): list of ref Listnode
{
case (hd argv).word {
"loaded" => return sbuiltin_loaded(ctxt, argv);
"bquote" => return sbuiltin_quote(ctxt, argv, 0);
"quote" => return sbuiltin_quote(ctxt, argv, 1);
"unquote" => return sbuiltin_unquote(ctxt, argv);
"builtin" => return sbuiltin_builtin(ctxt, argv);
}
return nil;
}
runbuiltin(ctxt: ref Context, nil: Sh, args: list of ref Listnode, lseq: int): string
{
status := "";
name := (hd args).word;
case name {
"load" => status = builtin_load(ctxt, args, lseq);
"loaded" => status = builtin_loaded(ctxt, args, lseq);
"unload" => status = builtin_unload(ctxt, args, lseq);
"builtin" => status = builtin_builtin(ctxt, args, lseq);
"whatis" => status = builtin_whatis(ctxt, args, lseq);
"run" => status = builtin_run(ctxt, args, lseq);
"exit" => status = builtin_exit(ctxt, args, lseq);
"syncenv" => export(ctxt.env.localenv);
"@" => status = builtin_subsh(ctxt, args, lseq);
}
return status;
}
sbuiltin_loaded(ctxt: ref Context, nil: list of ref Listnode): list of ref Listnode
{
v: list of ref Listnode;
for (bl := ctxt.env.bmods; bl != nil; bl = tl bl) {
(name, nil) := hd bl;
v = ref Listnode(nil, name) :: v;
}
return v;
}
sbuiltin_quote(nil: ref Context, argv: list of ref Listnode, quoteblocks: int): list of ref Listnode
{
return ref Listnode(nil, quoted(tl argv, quoteblocks)) :: nil;
}
sbuiltin_builtin(ctxt: ref Context, args: list of ref Listnode): list of ref Listnode
{
if (args == nil || tl args == nil)
builtinusage(ctxt, "builtin command [args ...]");
name := (hd tl args).word;
(nil, mods) := findbuiltin(ctxt.env.sbuiltins, name);
for (; mods != nil; mods = tl mods)
if (hd mods == myselfbuiltin)
return (hd mods)->runsbuiltin(ctxt, myself, tl args);
ctxt.fail("builtin not found", sys->sprint("sh: builtin %s not found", name));
return nil;
}
sbuiltin_unquote(ctxt: ref Context, argv: list of ref Listnode): list of ref Listnode
{
argv = tl argv;
if (argv == nil || tl argv != nil)
builtinusage(ctxt, "unquote arg");
arg := (hd argv).word;
if (arg == nil && (hd argv).cmd != nil)
arg = cmd2string((hd argv).cmd);
return stringlist2list(str->unquoted(arg));
}
getself(): Shellbuiltin
{
return myselfbuiltin;
}
builtinusage(ctxt: ref Context, s: string)
{
ctxt.fail("usage", "sh: usage: " + s);
}
builtin_exit(nil: ref Context, nil: list of ref Listnode, nil: int): string
{
# XXX using this primitive can cause
# environment stack not to be popped properly.
exit;
}
builtin_subsh(ctxt: ref Context, args: list of ref Listnode, nil: int): string
{
if (tl args == nil)
return nil;
startchan := chan of (int, ref Expropagate);
spawn runasync(ctxt, 0, tl args, ref Redirlist, startchan);
(exepid, exprop) := <-startchan;
status := waitfor(ctxt, exepid :: nil);
if (exprop.name != nil)
raise exprop.name;
return status;
}
builtin_loaded(ctxt: ref Context, nil: list of ref Listnode, nil: int): string
{
b := ctxt.env.builtins;
for (i := 0; i < b.n; i++) {
(name, bmods) := b.ba[i];
sys->print("%s\t%s\n", name, modname(ctxt, hd bmods));
}
b = ctxt.env.sbuiltins;
for (i = 0; i < b.n; i++) {
(name, bmods) := b.ba[i];
sys->print("${%s}\t%s\n", name, modname(ctxt, hd bmods));
}
return nil;
}
# it's debateable whether this should throw an exception or
# return a failed exit status - however, most scripts don't
# check the status and do need the module they're loading,
# so i think the exception is probably more useful...
builtin_load(ctxt: ref Context, args: list of ref Listnode, nil: int): string
{
if (tl args == nil || (hd tl args).word == nil)
builtinusage(ctxt, "load path...");
args = tl args;
if (args == nil)
builtinusage(ctxt, "load path...");
for (; args != nil; args = tl args) {
s := loadmodule(ctxt, (hd args).word);
if (s != nil)
raise "fail:" + s;
}
return nil;
}
builtin_unload(ctxt: ref Context, args: list of ref Listnode, nil: int): string
{
if (tl args == nil)
builtinusage(ctxt, "unload path...");
status := "";
for (args = tl args; args != nil; args = tl args)
if ((s := unloadmodule(ctxt, (hd args).word)) != nil)
status = s;
return status;
}
builtin_run(ctxt: ref Context, args: list of ref Listnode, nil: int): string
{
if (tl args == nil || (hd tl args).word == nil)
builtinusage(ctxt, "run path");
ctxt.push();
{
ctxt.setoptions(ctxt.INTERACTIVE, 0);
runscript(ctxt, (hd tl args).word, tl tl args, 1);
ctxt.pop();
return nil;
} exception e {
"fail:*" =>
ctxt.pop();
return failurestatus(e);
}
}
# four categories:
# environment variables
# substitution builtins
# braced blocks
# builtins (including those defined by externally loaded modules)
# or external programs
# other
builtin_whatis(ctxt: ref Context, args: list of ref Listnode, nil: int): string
{
if (len args < 2)
builtinusage(ctxt, "whatis name ...");
err := "";
for (args = tl args; args != nil; args = tl args)
if ((e := whatisit(ctxt, hd args)) != nil)
err = e;
return err;
}
whatisit(ctxt: ref Context, el: ref Listnode): string
{
if (el.cmd != nil) {
sys->print("%s\n", cmd2string(el.cmd));
return nil;
}
found := 0;
name := el.word;
if (name != nil && name[0] == '{') { #}
sys->print("%s\n", name);
return nil;;
}
if (name == nil)
return nil; # XXX questionable
w: string;
val := ctxt.get(name);
if (val != nil) {
found++;
w += sys->sprint("%s=%s\n", quote(name, 0), quoted(val, 0));
}
(nil, mods) := findbuiltin(ctxt.env.sbuiltins, name);
if (mods != nil) {
mod := hd mods;
if (mod == myselfbuiltin)
w += "${builtin " + name + "}\n";
else {
mw := mod->whatis(ctxt, myself, name, Shellbuiltin->SBUILTIN);
if (mw == nil)
mw = "${" + name + "}";
w += "load " + modname(ctxt, mod) + "; " + mw + "\n";
}
found++;
}
(nil, mods) = findbuiltin(ctxt.env.builtins, name);
if (mods != nil) {
mod := hd mods;
if (mod == myselfbuiltin)
sys->print("builtin %s\n", name);
else {
mw := mod->whatis(ctxt, myself, name, Shellbuiltin->BUILTIN);
if (mw == nil)
mw = name;
w += "load " + modname(ctxt, mod) + "; " + mw + "\n";
}
found++;
} else {
disfile := 0;
if (len name >= 4 && name[len name-4:] == ".dis")
disfile = 1;
pathlist: list of string;
if (len name >= 2 && (name[0] == '/' || name[0:2] == "./"))
pathlist = list of {""};
else if ((pl := ctxt.get("path")) != nil)
pathlist = list2stringlist(pl);
else
pathlist = list of {"/dis", "."};
foundpath := "";
while (pathlist != nil) {
path: string;
if (hd pathlist != "")
path = hd pathlist + "/" + name;
else
path = name;
if (!disfile && (fd := sys->open(path, Sys->OREAD)) != nil) {
if (executable(sys->fstat(fd), 8r111)) {
foundpath = path;
break;
}
}
if (!disfile)
path += ".dis";
if (executable(sys->stat(path), 8r444)) {
foundpath = path;
break;
}
pathlist = tl pathlist;
}
if (foundpath != nil)
w += foundpath + "\n";
}
for (bmods := ctxt.env.bmods; bmods != nil; bmods = tl bmods) {
(modname, mod) := hd bmods;
if ((mw := mod->whatis(ctxt, myself, name, Shellbuiltin->OTHER)) != nil)
w += "load " + modname + "; " + mw + "\n";
}
if (w == nil) {
sys->fprint(stderr(), "%s: not found\n", name);
return "not found";
}
sys->print("%s", w);
return nil;
}
# execute a command ignoring names defined by externally defined modules
builtin_builtin(ctxt: ref Context, args: list of ref Listnode, last: int): string
{
if (len args < 2)
builtinusage(ctxt, "builtin command [args ...]");
name := (hd tl args).word;
if (name == nil || name[0] == '{') {
diagnostic(ctxt, name + " not found");
return "not found";
}
(nil, mods) := findbuiltin(ctxt.env.builtins, name);
for (; mods != nil; mods = tl mods)
if (hd mods == myselfbuiltin)
return (hd mods)->runbuiltin(ctxt, myself, tl args, last);
if (ctxt.options() & ctxt.EXECPRINT)
sys->fprint(stderr(), "%s\n", quoted(tl args, 0));
return runexternal(ctxt, tl args, last);
}
modname(ctxt: ref Context, mod: Shellbuiltin): string
{
for (ml := ctxt.env.bmods; ml != nil; ml = tl ml) {
(bname, bmod) := hd ml;
if (bmod == mod)
return bname;
}
return "builtin";
}
loadmodule(ctxt: ref Context, name: string): string
{
# avoid loading the same module twice (it's convenient
# to have load be a null-op if the module required is already loaded)
for (bl := ctxt.env.bmods; bl != nil; bl = tl bl) {
(bname, nil) := hd bl;
if (bname == name)
return nil;
}
path := name;
if (len path < 4 || path[len path-4:] != ".dis")
path += ".dis";
if (path[0] != '/' && path[0:2] != "./")
path = BUILTINPATH + "/" + path;
mod := load Shellbuiltin path;
if (mod == nil) {
diagnostic(ctxt, sys->sprint("load: cannot load %s: %r", path));
return "bad module";
}
s := mod->initbuiltin(ctxt, myself);
ctxt.env.bmods = (name, mod->getself()) :: ctxt.env.bmods;
if (s != nil) {
unloadmodule(ctxt, name);
diagnostic(ctxt, "load: module init failed: " + s);
}
return s;
}
unloadmodule(ctxt: ref Context, name: string): string
{
bl: list of (string, Shellbuiltin);
mod: Shellbuiltin;
for (cl := ctxt.env.bmods; cl != nil; cl = tl cl) {
(bname, bmod) := hd cl;
if (bname == name)
mod = bmod;
else
bl = hd cl :: bl;
}
if (mod == nil) {
diagnostic(ctxt, sys->sprint("module %s not found", name));
return "not found";
}
for (ctxt.env.bmods = nil; bl != nil; bl = tl bl)
ctxt.env.bmods = hd bl :: ctxt.env.bmods;
removebuiltinmod(ctxt.env.builtins, mod);
removebuiltinmod(ctxt.env.sbuiltins, mod);
return nil;
}
executable(s: (int, Sys->Dir), mode: int): int
{
(ok, info) := s;
return ok != -1 && (info.mode & Sys->DMDIR) == 0
&& (info.mode & mode) != 0;
}
quoted(val: list of ref Listnode, quoteblocks: int): string
{
s := "";
for (; val != nil; val = tl val) {
el := hd val;
if (el.cmd == nil || (quoteblocks && el.word != nil))
s += quote(el.word, 0);
else {
cmd := cmd2string(el.cmd);
if (quoteblocks)
cmd = quote(cmd, 0);
s += cmd;
}
if (tl val != nil)
s[len s] = ' ';
}
return s;
}
setstatus(ctxt: ref Context, val: string): string
{
ctxt.setlocal("status", ref Listnode(nil, val) :: nil);
return val;
}
#
# beginning of parser routines
#
doparse(l: ref YYLEX, prompt: string, showline: int): (ref Node, string)
{
l.prompt = prompt;
l.err = nil;
l.lval.node = nil;
yyparse(l);
l.lastnl = 0; # don't print secondary prompt next time
if (l.err != nil) {
s: string;
if (l.err == nil)
l.err = "unknown error";
if (l.errline > 0 && showline)
s = sys->sprint("%s:%d: %s", l.path, l.errline, l.err);
else
s = l.path + ": parse error: " + l.err;
return (nil, s);
}
return (l.lval.node, nil);
}
blanklex: YYLEX; # for hassle free zero initialisation
YYLEX.initstring(s: string): ref YYLEX
{
ret := ref blanklex;
ret.s = s;
ret.path="internal";
ret.strpos = 0;
return ret;
}
YYLEX.initfile(fd: ref Sys->FD, path: string): ref YYLEX
{
lex := ref blanklex;
lex.f = bufio->fopen(fd, bufio->OREAD);
lex.path = path;
lex.cbuf = array[2] of int; # number of characters of pushback
lex.linenum = 1;
lex.prompt = "";
return lex;
}
YYLEX.error(l: self ref YYLEX, s: string)
{
if (l.err == nil) {
l.err = s;
l.errline = l.linenum;
}
}
NOTOKEN: con -1;
YYLEX.lex(l: self ref YYLEX): int
{
# the following are allowed a free caret:
# $, word and quoted word;
# also, allowed chrs in unquoted word following dollar are [a-zA-Z0-9*_]
endword := 0;
wasdollar := 0;
tok := NOTOKEN;
while (tok == NOTOKEN) {
case c := l.getc() {
l.EOF =>
tok = END;
'\n' =>
tok = '\n';
'\r' or '\t' or ' ' =>
;
'#' =>
while ((c = l.getc()) != '\n' && c != l.EOF)
;
l.ungetc();
';' => tok = ';';
'&' =>
c = l.getc();
if(c == '&')
tok = ANDAND;
else{
l.ungetc();
tok = '&';
}
'^' => tok = '^';
'{' => tok = '{';
'}' => tok = '}';
')' => tok = ')';
'(' => tok = '(';
'=' => (tok, l.lval.optype) = ('=', n_ASSIGN);
'$' =>
if (l.atendword) {
l.ungetc();
tok = '^';
break;
}
case (c = l.getc()) {
'#' =>
l.lval.optype = n_COUNT;
'"' =>
l.lval.optype = n_SQUASH;
* =>
l.ungetc();
l.lval.optype = n_VAR;
}
tok = OP;
wasdollar = 1;
'"' or '`'=>
if (l.atendword) {
tok = '^';
l.ungetc();
break;
}
tok = OP;
if (c == '"')
l.lval.optype = n_BQ2;
else
l.lval.optype = n_BQ;
'>' or '<' =>
rtype: int;
nc := l.getc();
if (nc == '>') {
if (c == '>')
rtype = Sys->OWRITE | OAPPEND;
else
rtype = Sys->ORDWR;
nc = l.getc();
} else if (c == '>')
rtype = Sys->OWRITE;
else
rtype = Sys->OREAD;
tok = REDIR;
if (nc == '[') {
(tok, l.lval.redir) = readfdassign(l);
if (tok == ERROR)
(l.err, l.errline) = ("syntax error in redirection", l.linenum);
} else {
l.ungetc();
l.lval.redir = ref Redir(-1, -1, -1);
}
if (l.lval.redir != nil)
l.lval.redir.rtype = rtype;
'|' =>
tok = '|';
l.lval.redir = nil;
if ((c = l.getc()) == '[') {
(tok, l.lval.redir) = readfdassign(l);
if (tok == ERROR) {
(l.err, l.errline) = ("syntax error in pipe redirection", l.linenum);
return tok;
}
tok = '|';
} else if(c == '|')
tok = OROR;
else
l.ungetc();
'\'' =>
if (l.atendword) {
l.ungetc();
tok = '^';
break;
}
startline := l.linenum;
s := "";
for(;;) {
while ((nc := l.getc()) != '\'' && nc != l.EOF)
s[len s] = nc;
if (nc == l.EOF) {
(l.err, l.errline) = ("unterminated string literal", startline);
return ERROR;
}
if (l.getc() != '\'') {
l.ungetc();
break;
}
s[len s] = '\''; # 'xxx''yyy' becomes WORD(xxx'yyy)
}
l.lval.word = s;
tok = WORD;
endword = 1;
* =>
if (c == ':') {
if (l.getc() == '=') {
tok = '=';
l.lval.optype = n_LOCAL;
break;
}
l.ungetc();
}
if (l.atendword) {
l.ungetc();
tok = '^';
break;
}
allowed: string;
if (l.wasdollar)
allowed = "a-zA-Z0-9*_";
else
allowed = "^\n \t\r|$'#<>;^(){}`&=\"";
word := "";
loop: do {
case c {
'*' or '?' or '[' or GLOB =>
word[len word] = GLOB;
':' =>
nc := l.getc();
l.ungetc();
if (nc == '=')
break loop;
}
word[len word] = c;
} while ((c = l.getc()) != l.EOF && str->in(c, allowed));
l.ungetc();
l.lval.word = word;
tok = WORD;
endword = 1;
}
l.atendword = endword;
l.wasdollar = wasdollar;
}
# sys->print("token %s\n", tokstr(tok));
return tok;
}
tokstr(t: int): string
{
s: string;
case t {
'\n' => s = "'\\n'";
33 to 127 => s = sprint("'%c'", t);
DUP=> s = "DUP";
REDIR =>s = "REDIR";
WORD => s = "WORD";
OP => s = "OP";
END => s = "END";
ERROR=> s = "ERROR";
* =>
s = "<unknowntok"+ string t + ">";
}
return s;
}
YYLEX.ungetc(lex: self ref YYLEX)
{
lex.strpos--;
if (lex.f != nil) {
lex.ncbuf++;
if (lex.strpos < 0)
lex.strpos = len lex.cbuf - 1;
}
}
YYLEX.getc(lex: self ref YYLEX): int
{
if (lex.eof) # EOF sticks
return lex.EOF;
c: int;
if (lex.f != nil) {
if (lex.ncbuf > 0) {
c = lex.cbuf[lex.strpos++];
if (lex.strpos >= len lex.cbuf)
lex.strpos = 0;
lex.ncbuf--;
} else {
if (lex.lastnl && lex.prompt != nil)
sys->fprint(stderr(), "%s", lex.prompt);
c = bufio->lex.f.getc();
if (c == bufio->ERROR || c == bufio->EOF) {
lex.eof = 1;
c = lex.EOF;
} else if (c == '\n')
lex.linenum++;
lex.lastnl = (c == '\n');
lex.cbuf[lex.strpos++] = c;
if (lex.strpos >= len lex.cbuf)
lex.strpos = 0;
}
} else {
if (lex.strpos >= len lex.s) {
lex.eof = 1;
c = lex.EOF;
} else
c = lex.s[lex.strpos++];
}
return c;
}
# read positive decimal number; return -1 if no number found.
readnum(lex: ref YYLEX): int
{
sum := nc := 0;
while ((c := lex.getc()) >= '0' && c <= '9') {
sum = (sum * 10) + (c - '0');
nc++;
}
lex.ungetc();
if (nc == 0)
return -1;
return sum;
}
# return tuple (toktype, lhs, rhs).
# -1 signifies no number present.
# '[' char has already been read.
readfdassign(lex: ref YYLEX): (int, ref Redir)
{
n1 := readnum(lex);
if ((c := lex.getc()) != '=') {
if (c == ']')
return (REDIR, ref Redir(-1, n1, -1));
return (ERROR, nil);
}
n2 := readnum(lex);
if (lex.getc() != ']')
return (ERROR, nil);
return (DUP, ref Redir(-1, n1, n2));
}
mkseq(left, right: ref Node): ref Node
{
if (left != nil && right != nil)
return mk(n_SEQ, left, right);
else if (left == nil)
return right;
return left;
}
mk(ntype: int, left, right: ref Node): ref Node
{
return ref Node(ntype, left, right, nil, nil);
}
stderr(): ref Sys->FD
{
return sys->fildes(2);
}