xref: /inferno-os/appl/tiny/sh.b (revision 37da2899f40661e3e9631e497da8dc59b971cbd0)
1implement Sh;
2
3include "sys.m";
4	sys: Sys;
5	FD: import Sys;
6
7include "draw.m";
8	Context: import Draw;
9
10include "filepat.m";
11	filepat: Filepat;
12	nofilepat := 0;			# true if load Filepat has failed.
13
14include "bufio.m";
15	bufio: Bufio;
16	Iobuf: import bufio;
17
18include "env.m";
19	env: Env;
20
21stdin: ref FD;
22stderr: ref FD;
23waitfd: ref FD;
24
25Quoted: con '\uFFF0';
26stringQuoted: con "\uFFF0";
27
28Sh: module
29{
30	init: fn(ctxt: ref Context, argv: list of string);
31};
32
33Command: adt
34{
35	args: list of string;
36	inf, outf: string;
37	append: int;
38};
39
40Async, Seq: con iota;
41
42Pipeline: adt
43{
44	cmds: list of ref Command;
45	term: int;
46};
47
48usage()
49{
50	sys->fprint(stderr, "Usage: sh [-n] [-c cmd] [file]\n");
51}
52
53init(ctxt: ref Context, argv: list of string)
54{
55	n: int;
56	arg: list of string;
57	buf := array[1024] of byte;
58
59	sys = load Sys Sys->PATH;
60	env = load Env Env->PATH;
61
62	stderr = sys->fildes(2);
63
64	waitfd = sys->open("#p/"+string sys->pctl(0, nil)+"/wait", sys->OREAD);
65	if(waitfd == nil){
66		sys->fprint(stderr, "sh: open wait: %r\n");
67		return;
68	}
69
70	eflag := nflag := lflag := 0;
71	cmd: string;
72	if(argv != nil)
73		argv = tl argv;
74	for(; argv != nil && len hd argv && (hd argv)[0]=='-'; argv = tl argv)
75		case hd argv {
76		"-e" =>
77			eflag = 1;
78		"-n" =>
79			nflag = 1;
80		"-l" =>
81			lflag = 1;
82		"-c" =>
83			argv = tl argv;
84			if(len argv != 1){
85				usage();
86				return;
87			}
88			cmd = hd argv;
89		* =>
90			usage();
91			return;
92		}
93
94	if (lflag)
95		startup(ctxt);
96
97	if(eflag == 0)
98		sys->pctl(sys->FORKENV, nil);
99	if(nflag == 0)
100		sys->pctl(sys->FORKNS, nil);
101	if(cmd != nil){
102		arg = tokenize(cmd+"\n");
103		if(arg != nil)
104			runit(ctxt, parseit(arg));
105		return;
106	}
107	if(argv != nil){
108		script(ctxt, hd argv);
109		return;
110	}
111
112	stdin = sys->fildes(0);
113
114	prompt := sysname() + "$ ";
115	for(;;){
116		sys->print("%s", prompt);
117		n = sys->read(stdin, buf, len buf);
118		if(n <= 0)
119			break;
120		arg = tokenize(string buf[0:n]);
121		if(arg != nil)
122			runit(ctxt, parseit(arg));
123	}
124}
125
126rev(arg: list of string): list of string
127{
128	ret: list of string;
129
130	while(arg != nil){
131		ret = hd arg :: ret;
132		arg = tl arg;
133	}
134	return ret;
135}
136
137waitfor(pid: int)
138{
139	if(pid <= 0)
140		return;
141	buf := array[sys->WAITLEN] of byte;
142	status := "";
143	for(;;){
144		n := sys->read(waitfd, buf, len buf);
145		if(n < 0){
146			sys->fprint(stderr, "sh: read wait: %r\n");
147			return;
148		}
149		status = string buf[0:n];
150		if(status[len status-1] != ':')
151			sys->fprint(stderr, "%s\n", status);
152		who := int status;
153		if(who != 0){
154			if(who == pid)
155				return;
156		}
157	}
158}
159
160mkprog(ctxt: ref Context, arg: list of string, infd, outfd: ref Sys->FD, waitpid: chan of int)
161{
162	fds := list of {0, 1, 2};
163	if(infd != nil)
164		fds = infd.fd :: fds;
165	if(outfd != nil)
166		fds = outfd.fd :: fds;
167	pid := sys->pctl(sys->NEWFD, fds);
168	console := sys->fildes(2);
169
170	if(infd != nil){
171		sys->dup(infd.fd, 0);
172		infd = nil;
173	}
174	if(outfd != nil){
175		sys->dup(outfd.fd, 1);
176		outfd = nil;
177	}
178
179	waitpid <-= pid;
180
181	if(pid < 0 || arg == nil)
182		return;
183
184	{
185		exec(ctxt, arg, console);
186	}exception{
187	"fail:*" =>
188		#sys->fprint(console, "%s:%s\n", hd arg, e.name[5:]);
189		exit;
190	"write on closed pipe" =>
191		#sys->fprint(console, "%s: %s\n", hd arg, e.name);
192		exit;
193	}
194}
195
196exec(ctxt: ref Context, args: list of string, console: ref Sys->FD)
197{
198	if (args == nil)
199		return;
200	cmd := hd args;
201	file := cmd;
202
203	if(len file<4 || file[len file-4:]!=".dis")
204		file += ".dis";
205
206	c := load Sh file;
207	if(c == nil) {
208		err := sys->sprint("%r");
209		if(err != "permission denied" && err != "access permission denied" && file[0]!='/' && file[0:2]!="./"){
210			c = load Sh "/dis/"+file;
211			if(c == nil)
212				err = sys->sprint("%r");
213		}
214		if(c == nil){
215			sys->fprint(console, "%s: %s\n", cmd, err);
216			return;
217		}
218	}
219
220	c->init(ctxt, args);
221}
222
223script(ctxt: ref Context, src: string)
224{
225	bufio = load Bufio Bufio->PATH;
226	if(bufio == nil){
227		sys->fprint(stderr, "sh: load bufio: %r\n");
228		return;
229	}
230
231	f := bufio->open(src, Bufio->OREAD);
232	if(f == nil){
233		sys->fprint(stderr, "sh: open %s: %r\n", src);
234		return;
235	}
236	for(;;){
237		s := f.gets('\n');
238		if(s == nil)
239			break;
240		arg := tokenize(s);
241		if(arg != nil)
242			runit(ctxt, parseit(arg));
243	}
244}
245
246sysname(): string
247{
248	fd := sys->open("#c/sysname", sys->OREAD);
249	if(fd == nil)
250		return "anon";
251	buf := array[128] of byte;
252	n := sys->read(fd, buf, len buf);
253	if(n < 0)
254		return "anon";
255	return string buf[0:n];
256}
257
258# Lexer.
259
260tokenize(s: string): list of string
261{
262	tok: list of string;
263	token := "";
264	instring := 0;
265
266loop:
267	for(i:=0; i<len s; i++) {
268		if(instring) {
269			if(s[i] != '\'')
270				token = addchar(token, s[i]);
271			else if(i == len s-1 || s[i+1] != '\'') {
272				if(i == len s-1 || s[i+1] == ' ' || s[i+1] == '\t' || s[i+1] == '\n'){
273					tok = token :: tok;
274					token = "";
275				}
276				instring = 0;
277			} else {
278				token[len token] = '\'';
279				i++;
280			}
281			continue;
282		}
283		case s[i] {
284		' ' or '\t' or '\n' or '#' or
285		'\'' or '|' or '&' or ';' or
286		'>' or '<' or '\r' =>
287			if(token != "" && s[i]!='\''){
288				tok = token :: tok;
289				token = "";
290			}
291			case s[i] {
292			'#' =>
293				break loop;
294			'\'' =>
295				instring = 1;
296			'>' =>
297				ss := "";
298				ss[0] = s[i];
299				if(i<len s-1 && s[i+1]==s[i])
300					ss[1] = s[i++];
301				tok = ss :: tok;
302			'|' or '&' or ';' or '<' =>
303				ss := "";
304				ss[0] = s[i];
305				tok = ss :: tok;
306			}
307		* =>
308			token[len token] = s[i];
309		}
310	}
311	if(instring){
312		sys->fprint(stderr, "sh: unmatched quote\n");
313		return nil;
314	}
315	return rev(tok);
316}
317
318ismeta(char: int): int
319{
320	case char {
321	'*'  or '[' or '?' or
322	'#'  or '\'' or '|' or
323	'&' or ';' or '>' or
324	'<'  =>
325		return 1;
326	}
327	return 0;
328}
329
330addchar(token: string, char: int): string
331{
332	if(ismeta(char) && (len token==0 || token[0]!=Quoted))
333		token = stringQuoted + token;
334	token[len token] = char;
335	return token;
336}
337
338# Parser.
339
340getcommand(words: list of string): (ref Command, list of string)
341{
342	args: list of string;
343	word: string;
344	si, so: string;
345	append := 0;
346
347gather:
348	do {
349		word = hd words;
350
351		case word {
352		">" or ">>" =>
353			if(so != nil)
354				return (nil, nil);
355
356			words = tl words;
357
358			if(words == nil)
359				return (nil, nil);
360
361			so = hd words;
362			if(len so>0 && so[0]==Quoted)
363				so = so[1:];
364			if(word == ">>")
365				append = 1;
366		"<" =>
367			if(si != nil)
368				return (nil, nil);
369
370			words = tl words;
371
372			if(words == nil)
373				return (nil, nil);
374
375			si = hd words;
376			if(len si>0 && si[0]==Quoted)
377				si = si[1:];
378		"|" or ";" or "&" =>
379			break gather;
380		* =>
381			files := doexpand(word);
382			while(files != nil){
383				args = hd files :: args;
384				files = tl files;
385			}
386		}
387
388		words = tl words;
389	} while (words != nil);
390
391	return (ref Command(rev(args), si, so, append), words);
392}
393
394doexpand(file: string): list of string
395{
396	if(file == nil)
397		return file :: nil;
398	if(len file>0 && file[0]==Quoted)
399		return file[1:] :: nil;
400	if (nofilepat)
401		return file :: nil;
402	for(i:=0; i<len file; i++)
403		if(file[i]=='*' || file[i]=='[' || file[i]=='?'){
404			if(filepat == nil) {
405				if ((filepat = load Filepat Filepat->PATH) == nil) {
406					sys->fprint(stderr, "sh: warning: cannot load %s: %r\n",
407							Filepat->PATH);
408					nofilepat = 1;
409					return file :: nil;
410				}
411			}
412			files := filepat->expand(file);
413			if(files != nil)
414				return files;
415			break;
416		}
417	return file :: nil;
418}
419
420revc(arg: list of ref Command): list of ref Command
421{
422	ret: list of ref Command;
423	while(arg != nil) {
424		ret = hd arg :: ret;
425		arg = tl arg;
426	}
427	return ret;
428}
429
430getpipe(words: list of string): (ref Pipeline, list of string)
431{
432	cmds: list of ref Command;
433	cur: ref Command;
434	word: string;
435
436	term := Seq;
437gather:
438	while(words != nil) {
439		word = hd words;
440
441		if(word == "|")
442			return (nil, nil);
443
444		(cur, words) = getcommand(words);
445
446		if(cur == nil)
447			return (nil, nil);
448
449		cmds = cur :: cmds;
450
451		if(words == nil)
452			break gather;
453
454		word = hd words;
455		words = tl words;
456
457		case word {
458		";" =>
459			break gather;
460		"&" =>
461			term = Async;
462			break gather;
463		"|" =>
464			continue gather;
465		}
466		return (nil, nil);
467	}
468
469	if(word == "|")
470		return (nil, nil);
471
472	return (ref Pipeline(revc(cmds), term), words);
473}
474
475revp(arg: list of ref Pipeline): list of ref Pipeline
476{
477	ret: list of ref Pipeline;
478
479	while(arg != nil) {
480		ret = hd arg :: ret;
481		arg = tl arg;
482	}
483	return ret;
484}
485
486parseit(words: list of string): list of ref Pipeline
487{
488	ret: list of ref Pipeline;
489	cur: ref Pipeline;
490
491	while(words != nil) {
492		(cur, words) = getpipe(words);
493		if(cur == nil){
494			sys->fprint(stderr, "sh: syntax error\n");
495			return nil;
496		}
497		ret = cur :: ret;
498	}
499	return revp(ret);
500}
501
502# Runner.
503
504runpipeline(ctx: ref Context, pipeline: ref Pipeline)
505{
506	if(pipeline.term == Async)
507		sys->pctl(sys->NEWPGRP, nil);
508	pid := startpipeline(ctx, pipeline);
509	if(pid < 0)
510		return;
511	if(pipeline.term == Seq)
512		waitfor(pid);
513}
514
515startpipeline(ctx: ref Context, pipeline: ref Pipeline): int
516{
517	pid := 0;
518	cmds := pipeline.cmds;
519	first := 1;
520	inpipe, outpipe: ref Sys->FD;
521	while(cmds != nil) {
522		last := tl cmds == nil;
523		cmd := hd cmds;
524
525		infd: ref Sys->FD;
526		if(!first)
527			infd = inpipe;
528		else if(cmd.inf != nil){
529			infd = sys->open(cmd.inf, Sys->OREAD);
530			if(infd == nil){
531				sys->fprint(stderr, "sh: can't open %s: %r\n", cmd.inf);
532				return -1;
533			}
534		}
535
536		outfd: ref Sys->FD;
537		if(!last){
538			fds := array[2] of ref Sys->FD;
539			if(sys->pipe(fds) < 0){
540				sys->fprint(stderr, "sh: can't make pipe: %r\n");
541				return -1;
542			}
543			outpipe = fds[0];
544			outfd = fds[1];
545			fds = nil;
546		}else if(cmd.outf != nil){
547			if(cmd.append){
548				outfd = sys->open(cmd.outf, Sys->OWRITE);
549				if(outfd != nil)
550					sys->seek(outfd, big 0, Sys->SEEKEND);
551			}
552			if(outfd == nil)
553				outfd = sys->create(cmd.outf, Sys->OWRITE, 8r666);
554			if(outfd == nil){
555				sys->fprint(stderr, "sh: can't open %s: %r\n", cmd.outf);
556				return -1;
557			}
558		}
559
560		rpid := chan of int;
561		spawn mkprog(ctx, cmd.args, infd, outfd, rpid);
562		pid = <-rpid;
563		infd = nil;
564		outfd = nil;
565
566		inpipe = outpipe;
567		outpipe = nil;
568
569		first = 0;
570		cmds = tl cmds;
571	}
572	return pid;
573}
574
575runit(ctx: ref Context, pipes: list of ref Pipeline)
576{
577	while(pipes != nil) {
578		pipeline := hd pipes;
579		pipes = tl pipes;
580		if(pipeline.term == Seq)
581			runpipeline(ctx, pipeline);
582		else
583			spawn runpipeline(ctx, pipeline);
584	}
585}
586
587strchr(s: string, c: int): int
588{
589	ln := len s;
590	for (i := 0; i < ln; i++)
591		if (s[i] == c)
592			return i;
593	return -1;
594}
595
596# PROFILE: con "/lib/profile";
597PROFILE: con "/lib/infernoinit";
598
599startup(ctxt: ref Context)
600{
601	if (env == nil)
602		return;
603	# if (env->getenv("home") != nil)
604	#	return;
605	home := gethome();
606	env->setenv("home", home);
607	escript(ctxt, PROFILE);
608	escript(ctxt, home + PROFILE);
609}
610
611escript(ctxt: ref Context, file: string)
612{
613	fd := sys->open(file, Sys->OREAD);
614	if (fd != nil)
615		script(ctxt, file);
616}
617
618gethome(): string
619{
620	fd := sys->open("/dev/user", sys->OREAD);
621  	if(fd == nil)
622    		return "/";
623  	buf := array[128] of byte;
624  	n := sys->read(fd, buf, len buf);
625  	if(n < 0)
626    		return "/";
627  	return "/usr/" + string buf[0:n];
628}
629