shithub: purgatorio

ref: 3866717cbb020199d58171c1c0cdd7382a74ee82
dir: /appl/lib/plumbing.b/

View raw version
implement Plumbing;

include "sys.m";
	sys: Sys;

include "regex.m";
	regex: Regex;

include "plumbing.m";

init(regexmod: Regex, args: list of string): (list of ref Rule, string)
{
	sys = load Sys Sys->PATH;
	regex = regexmod;

	if(args == nil){
		user := readfile("/dev/user");
		if(user == nil)
			return (nil, sys->sprint("can't read /dev/user: %r"));
		filename := "/usr/"+user+"/plumbing";
		(rc, nil) := sys->stat(filename);
		if(rc < 0)
			filename = "/usr/"+user+"/lib/plumbing";
		args = filename :: nil;
	}
	r, rules: list of ref Rule;
	err: string;
	while(args != nil){
		filename := hd args;
		args = tl args;
		file := readfile(filename);
		if(file == nil)
			return (nil, sys->sprint("can't read %s: %r", filename));
		(r, err) = parse(filename, file);
		if(err != nil)
			return (nil, err);
		while(r != nil){
			rules = hd r :: rules;
			r = tl r;
		}
	}
	# reverse the rules
	r = nil;
	while(rules != nil){
		r = hd rules :: r;
		rules = tl rules;
	}
	return (r, nil);
}

readfile(filename: string): string
{
	fd := sys->open(filename, Sys->OREAD);
	if(fd == nil)
		return nil;
	(ok, dir) := sys->fstat(fd);
	if(ok < 0)
		return nil;
	size := int dir.length;
	if(size == 0)	# devices have length 0 sometimes
		size = 1000;
	b := array[size] of byte;
	n := sys->read(fd, b, len b);
	if(n <= 0)
		return nil;
	return string b[0:n];
}

parse(filename, file: string): (list of ref Rule, string)
{
	line: string;
	lineno := 0;
	i := 0;
	pats: list of ref Pattern;
	rules: list of ref Rule;
	while(i < len file){
		lineno++;
		(line, i) = nextline(file, i);
		(pat, err) := pattern(line);
		if(err != nil)
			return (nil, sys->sprint("%s:%d: %s", filename, lineno, err));
		if(pat == nil){
			if(pats==nil || !blank(line))	# comment line
				continue;
			(rul, err1) := rule(pats);
			if(err1 != nil)
				return (nil, sys->sprint("%s:%d: %s", filename, lineno-1, err1));
			rules = rul :: rules;
			pats = nil;
		}else
			pats = pat :: pats;
	}
	if(pats != nil){
		(rul, err1) := rule(pats);
		if(err1 != nil)
			return (nil, sys->sprint("%s:%d: %s", filename, lineno-1, err1));
		rules = rul :: rules;
	}
	# reverse the rules
	r: list of ref Rule;
	while(rules != nil){
		r = hd rules :: r;
		rules = tl rules;
	}
	return (r, nil);
}

nextline(file: string, i: int): (string, int)
{
	for(j:=i; j<len file; j++)
		if(file[j] == '\n')
			return (file[i:j], j+1);
	return (file[i:], len file);
}

blank(line: string): int
{
	for(i:=0; i<len line; i++)
		if(line[i]!=' ' && line[i]!='\t')
			return 0;
	return 1;
}

pattern(line: string): (ref Pattern, string)
{
	expand := 0;
	for(i:=0; i<len line; i++)
		if(line[i] == '$'){
			expand = 1;
			break;
		}
	(w, err) := words(line);
	if(err != nil)
		return (nil, err);
	if(w == nil)
		return (nil, nil);
	if(len w < 3)
		return (nil, "syntax error: too few words on line");
	pat := ref Pattern;
	pat.field = hd w;
	pat.pred = hd tl w;
	pat.arg = hd tl tl w;
	pat.extra = tl tl tl w;
	pat.expand = expand;
	return (pat, nil);
}

rule(pats: list of ref Pattern): (ref Rule, string)
{
	# pats is in reverse order on arrival
	actionpred := list of {"alwaysstart", "start", "to"};
	patternpred := list of {"is", "isdir", "isfile", "matches", "set"};
	npats := 0;
	nacts := 0;
	haveto := 0;
	for(l:=pats; l!=nil; l=tl l){
		pat := hd l;
		pred := pat.pred;
		noextra := 1;
		case pat.field {
		"plumb" =>
			nacts++;
			if(!oneof(pred, actionpred))
				return (nil, "illegal predicate "+pred+" in action");
			case pred {
			"to" or "alwaysstart" =>
				if(len pat.arg == 0)
					return (nil, "\"plumb "+pred+"\" must have non-empty target");
				haveto = 1;
			"start" =>
				noextra = 0;
			}
			if(npats != 0)
				return (nil, "actions must follow patterns in rule");
		"src" or "dst" or "dir" or "kind" or "attr" or "data" =>
			if(!oneof(pred, patternpred))
				return (nil, "illegal predicate "+pred+" in pattern");
			if(pred == "matches"){
				(pat.regex, nil) = regex->compile(pat.arg, 1);
				if(pat.regex == nil)
					return (nil, sys->sprint("error in regular expression '%s'", pat.arg));
			}
			npats++;
		}
		if(noextra && pat.extra != nil)
			return (nil, sys->sprint("too many words in '%s' pattern", pat.field));
	}
	if(haveto == 0)
		return (nil, "rule must have \"plumb to\" action");
	rule := ref Rule;
	rule.action = array[nacts] of ref Pattern;
	for(i:=nacts; --i>=0; ){
		rule.action[i] = hd pats;
		pats = tl pats;
	}
	rule.pattern = array[npats] of ref Pattern;
	for(i=npats; --i>=0; ){
		rule.pattern[i] = hd pats;
		pats = tl pats;
	}
	return (rule, nil);
}

oneof(word: string, words: list of string): int
{
	while(words != nil){
		if(word == hd words)
			return 1;
		words = tl words;
	}
	return 0;
}

words(line: string): (list of string, string)
{
	ws: list of string;
	i := 0;
	for(;;){
		# not in word; find beginning of word
		while(i<len line && (line[i]==' ' || line[i]=='\t'))
			i++;
		if(i==len line || line[i]=='#')
			break;
		# i is first character of word; is it quoted?
		if(line[i] == '\''){
			word := "";
			i++;
			while(i < len line){
				c := line[i++];
				if(c=='\''){
					if(i==len line || line[i]!='\'')
						break;
					# else it's a literal quote
					if(i < len line)
						i++;
				}
				word[len word] = c;
			}
			ws = word :: ws;
			continue;
		}
		# regular word; continue until white space or end
		start := i;
		while(i<len line && (line[i]!=' ' && line[i]!='\t'))
			i++;
		ws = line[start:i] :: ws;
	}
	r: list of string;
	while(ws != nil){
		r = hd ws :: r;
		ws = tl ws;
	}
	return (r, nil);
}