CLIte/clite/shell.js
2023-12-15 00:44:44 +10:00

901 lines
19 KiB
JavaScript

clite.commands.data = function() {
// insert commands below this line
clite.commands.load('sh',function(args,env,io) {
var stdio = io.include('stdio');
var stdlib = io.include('stdlib');
var term = io.include('term');
var clite = io.include('clite');
var has_exited = false;
var macro = {
clear:function(args,io) {
term.clear();
return 0;
},
cd:function(args,io) {
var dir = env.HOME;
if (args.length > 1)
dir = clite.resolvePath(args[1]);
var fd = stdio.open(dir,stdio.flags.O_SEARCH|stdio.flags.O_DIRECTORY);
if (!fd) {
stdio.fprintf(io.stderr,'invalid directory: %s\n',dir);
return 1;
}
stdio.close(fd);
env.PWD = dir;
return 0;
},
pwd:function(args,io) {
stdio.fprintf(io.stdout,'%s\n',env.PWD);
return 0;
},
echo:function(args,io) {
args.shift();
var txt = args.join(' ')+'\n';
stdio.write(io.stdout,txt);
return 0;
},
which:function(args,io) {
if (args.length <2)
return 1;
if (typeof macro[args[1]] != 'undefined') {
return 1;
}else{
var path = resolvePATH(args[1]);
if (!path)
return 1;
stdio.fprintf(io.stdout,'%s is %s\n',args[1],path);
}
return 0;
},
type:function(args,io) {
if (args.length <2)
return 1;
if (typeof macro[args[1]] != 'undefined') {
stdio.fprintf(io.stdout,'%s is a shell builtin\n',args[1]);
}else{
var path = resolvePATH(args[1]);
if (!path)
return 1;
stdio.fprintf(io.stdout,'%s is %s\n',args[1],path);
}
return 0;
},
whoami:function(args,io) {
stdio.fprintf(io.stdout,'%s\n',env.USER);
return 0;
},
alias:function(args,io) {
stdio.fprintf(io.stdout,'unimplemented\n');
return 0;
},
export:function(args,io) {
if (args.length == 1) {
Object.keys(env).forEach(function(key) {
stdio.fprintf(io.stdout,'%s=%s\n',key,env[key]);
});
return 0;
}
var parts = args[1].split('=');
if (parts.length != 2)
return 0;
env[parts[0]] = parts[1];
return 0;
},
exit:function(args,io) {
has_exited = true;
var ev = 0;
if (args.length > 1)
ev = parseInt(args[1]);
io.exit(ev);
return 0;
}
};
var history = {
data:[],
current:'',
index:0,
add:function(txt) {
if (txt.length < 1)
return;
if (history.data.length > 0 && history.data[history.data.length-1] == txt)
return;
history.data.push(txt);
history.index = history.data.length;
},
up:function() {
if (history.index < 1)
return null;
history.index--;
return history.getCurrent();
},
down:function() {
if (history.index >= history.data.length)
return null;
history.index++;
return history.getCurrent();
},
setCurrent:function(txt) {
history.current = txt;
},
getCurrent:function() {
if (history.index >= history.data.length)
return history.current;
return history.data[history.index];
},
resetCurrent:function() {
history.current = '';
history.index = history.data.length;
},
clear:function() {
history.data = [];
history.current = '';
history.index = 0;
}
};
var parser = {
files:[],
callback:null,
queue:function(file) {
parser.files.push(file);
},
internal:{
lines:[],
parseFile:function(f) {
var fn = clite.resolvePath(f);
if (fn == null) {
parser.run();
return;
}
var st = stdio.stat(fn);
if (st == null || st.type != stdio.types.FT_SCRIPT) {
parser.run();
return;
}
var fd = stdio.open(fn,stdio.flags.O_RDONLY,parser.internal.prepFile);
if (!fd)
parser.run();
},
prepFile:function(fd) {
var data = stdio.readAll(fd);
if (fd != io.stdin)
stdio.close(fd);
if (!data) {
parser.run();
return;
}
parser.internal.lines = data.split('\n');
if (parser.internal.lines.length < 1) {
parser.run();
return;
}
parser.internal.parseLine();
},
parseLine:function() {
var l = parser.internal.lines.shift();
if (l == null) {
parser.run();
return;
}
if (l[0] == '#') {
parser.internal.parseLine();
return;
}
exec.setCommand(l);
exec.run(parser.internal.parseLine);
}
},
exit:function() {
if (typeof parser.callback !== 'function')
return;
try{
parser.callback();
} catch(err) {
stdio.write(io.stderr,'Shell: internal error in parser\n');
io.exit(1);
return;
}
},
run:function(cb) {
if (typeof cb === 'function')
parser.callback = cb;
var f = parser.files.shift();
if (!f) {
parser.exit();
return;
}
parser.internal.parseFile(f);
},
stdin:function() {
parser.callback = function() {
io.exit(0);
}
parser.internal.prepFile(io.stdin);
}
};
var exec = {
callback:null,
cmds:[],
active:null,
bufferout:null,
bufferin:null,
cfg:{closout:false,closin:false,closerr:false},
last:0,
io:{},
internal:{
doCommand:function(cmd) {
exec.active = cmd;
if ((cmd.onlastsuccess && exec.last != 0) || (cmd.onlastfail && exec.last == 0)) {
exec.run();
return;
}
var usebi = false;
var usebo = false;
if (cmd.stdin != '&0') {
switch (cmd.stdin) {
case '|':
exec.stdin.setOut();
usebi = true;
break;
case null:
exec.stdin.setNull();
break;
default:
exec.stdin.setFile(cmd.stdin);
break;
}
}else{
if (exec.cfg.closin) {
stdio.close(exec.io.stdin);
exec.cfg.closin = false;
}
exec.io.stdin = io.stdin;
}
if (cmd.stdout != '&1') {
switch (cmd.stdout) {
case '|':
exec.stdout.setBuffered();
usebo = true;
break;
case null:
exec.stdout.setNull();
break;
default:
exec.stdout.setFile(cmd.stdout);
break;
}
}else{
if (exec.cfg.closout) {
stdio.close(exec.io.stdout);
exec.cfg.closout = false;
}
exec.io.stdout = io.stdout;
}
if (cmd.stderr != '&2') {
switch (cmd.stderr) {
case '&1':
exec.stderr.setOut();
break;
case null:
exec.stderr.setNull();
break;
default:
exec.stderr.setFile(cmd.stderr);
break;
}
}else{
if (exec.cfg.closerr) {
stdio.close(exec.io.stderr);
exec.cfg.closerr = false;
}
exec.io.stderr = io.stderr;
}
if (exec.io.stdin == null) {
stdio.write(io.stderr,'could not open standard input for reading\n');
exec.setLast(1);
exec.run();
return;
}
if (exec.io.stdout == null) {
stdio.write(io.stderr,'could not open standard output for writing\n');
exec.setLast(1);
exec.run();
return;
}
if (exec.io.stderr == null) {
stdio.write(io.stderr,'could not open standard error for writing\n');
exec.setLast(1);
exec.run();
return;
}
if (typeof macro[cmd.args[0]] != 'undefined') {
exec.io.exit = io.exit;
var r = macro[cmd.args[0]](cmd.args,exec.io);
exec.setLast(r);
exec.run();
return;
}
if (!cmd.path) {
stdio.fprintf(io.stderr,'Shell: unknown command: %s\n',cmd.args[0]);
exec.setLast(1);
exec.run();
return;
}
var pid = stdlib.fork(env,exec.io,exec.internal.doExec);
if (pid == 0) {
stdio.write(io.stderr,'Shell: internal error\n');
exec.setLast(1);
exec.run();
return;
}
stdlib.waitpid(pid,function(pid,ev) {
exec.setLast(ev);
exec.run();
});
},
doExec:function(env,io) {
var r = stdlib.exec(exec.active.path,exec.active.args,env,io);
if (r > 0)
return;
if (r == -1) {
stdlib.fprintf(io.stderr,'Shell: unknown command: %s\n',exec.active.args[0]);
io.exit(-1);
return;
}
if (r == -2) {
stdio.fprintf(io.stderr,'Shell: error in command: %s\n',exec.active.args[0]);
io.exit(-1);
return;
}
if (r == -3 || r == -4) {
stdio.fprintf(io.stderr,'Shell: not an executable file: %s\n',exec.active.args[0]);
io.exit(-3);
return;
}
if (r == -5) {
stdio.fprintf(io.stderr,'Shell: invalid parser for file: %s\n',exec.active.args[0]);
io.exit(-3);
return;
}
if (r == -6) {
stdio.fprintf(io.stderr,'Shell: no valid parser for file: %s\n',exec.active.args[0]);
io.exit(-3);
return;
}
if (r<0) {
stdio.fprintf(io.stderr,'Shell: could not exec file: %s\n',exec.active.args[0]);
io.exit(-3);
}
}
},
setLast:function(v) {
exec.last = parseInt(v);
env['?'] = exec.last.toString();
},
setCommand:function(txt) {
var args = clite.strToArgs(txt);
if (args.length < 1)
return;
args.forEach(function(val,i) {
args[i] = preprocess(val);
});
var cmd = {
args:[],
path:null,
stdin:'&0',
stdout:'&1',
stderr:'&2',
onlastsuccess:false,
onlastfail:false
};
var c = structuredClone(cmd);
exec.cmds = [];
for (var i=0; i<args.length; i++) {
if (args[i] == '|') {
c.stdout = '|';
exec.cmds.push(c);
c = structuredClone(cmd);
c.stdin = '|';
continue;
}
if (args[i] == '||') {
exec.cmds.push(c);
c = structuredClone(cmd);
c.onlastfail = true;
continue;
}
if (args[i] == '&&') {
exec.cmds.push(c);
c = structuredClone(cmd);
c.onlastsuccess = true;
continue;
}
if (args[i] == '>') {
i++;
c.stdout = clite.resolvePath(args[i]);
if (c.stdout == '/dev/null')
c.stdout = null;
continue;
}
if (args[i] == '2>&1') {
c.stderr = '&1';
continue;
}
if (args[i] == '>>') {
i++;
c.stdout = clite.resolvePath(args[i]);
if (c.stdout == '/dev/null') {
c.stdout = null;
}else{
c.stdout = '+'+c.stdout;
}
continue;
}
if (args[i] == '<') {
i++;
c.stdin = clite.resolvePath(args[i]);
if (c.stdin == '/dev/null')
c.stdin = null;
continue;
}
c.args.push(args[i]);
}
if (c.args.length > 0)
exec.cmds.push(c);
var fault = false;
exec.cmds.forEach(function(c) {
if (c.args.length < 1) {
fault = true;
return;
}
c.path = resolvePATH(c.args[0]);
});
if (fault) {
stdio.write(io.stderr,'Shell: unable to pass command\n');
exec.exit();
return;
}
exec.io.pid = io.pid;
exec.io.stdin = io.stdin;
exec.io.stdout = io.stdout;
exec.io.stderr = io.stderr;
exec.io.exit = null; // filled in by fork()
exec.io.include = null; // filled in by fork()
exec.last = 0;
},
stdin:{
setFile:function(path) {
if (exec.cfg.closin) {
stdio.close(exec.io.stdin);
exec.cfg.closin = false;
}
exec.io.stdin = stdio.open(path,stdio.flags.O_RDONLY);
exec.cfg.closin = true;
},
setNull:function() {
if (exec.cfg.closin) {
stdio.close(exec.io.stdin);
exec.cfg.closin = false;
}
exec.io.stdin = stdio.open('/dev/null',stdio.flags.O_RDONLY);
exec.cfg.closin = true;
},
setOut:function() {
if (exec.cfg.closin) {
stdio.close(exec.io.stdin);
exec.cfg.closin = false;
}
if (exec.bufferout) {
exec.io.stdout = null;
exec.cfg.closout = false;
exec.bufferin = exec.bufferout;
exec.bufferout = null;
exec.io.stdin = exec.bufferin;
stdio.seek(exec.io.stdin,0);
exec.cfg.closin = true;
}else{
exec.stdin.setNull();
}
}
},
stdout:{
setBuffered:function() {
if (exec.cfg.closout) {
stdio.close(exec.io.stdout);
exec.cfg.closout = false;
if (exec.bufferout)
exec.bufferout = null;
}
exec.bufferout = stdio.mkstemp('/tmp/shell-XXXXXX');
exec.io.stdout = exec.bufferout;
exec.cfg.closout = false;
},
setFile:function(path) {
if (exec.cfg.closout) {
stdio.close(exec.io.stdout);
exec.cfg.closout = false;
}
var flags = stdio.flags.O_WRONLY|stdio.flags.O_CREAT|stdio.flags.O_SYNC;
var p = path;
if (path[0] == '+') {
flags |= stdio.flags.O_APPEND;
p = path.substring(1);
}else{
flags |= stdio.flags.O_TRUNC;
}
exec.io.stdout = stdio.open(p,flags);
exec.cfg.closout = true;
},
setNull:function() {
if (exec.cfg.closout) {
stdio.close(exec.io.stdout);
exec.cfg.closout = false;
}
exec.io.stdout = stdio.open('/dev/null',stdio.flags.O_WRONLY);
exec.cfg.closin = true;
}
},
stderr:{
setFile:function(path) {
if (exec.cfg.closerr) {
stdio.close(exec.io.stderr);
exec.cfg.closerr = false;
}
var flags = stdio.flags.O_WRONLY|stdio.flags.O_CREAT|stdio.flags.O_SYNC;
var p = path;
if (path[0] == '+') {
flags |= stdio.flags.O_APPEND;
p = path.substring(1);
}else{
flags |= stdio.flags.O_TRUNC;
}
exec.io.stderr = stdio.open(p,flags);
exec.cfg.closerr = true;
},
setNull:function() {
if (exec.cfg.closerr) {
stdio.close(exec.io.stderr);
exec.cfg.closerr = false;
}
exec.io.stderr = stdio.open('/dev/null',stdio.flags.O_WRONLY);
exec.cfg.closerr = true;
},
setOut:function() {
if (exec.cfg.closerr) {
stdio.close(exec.io.stderr);
exec.cfg.closerr = false;
}
exec.io.stderr = exec.io.stdout;
exec.cfg.closerr = exec.cfg.closout;
}
},
exit:function() {
if (exec.cfg.closin) {
stdio.close(exec.io.stdin);
exec.cfg.closin = false;
}
var out = '';
if (exec.bufferout)
out = stdio.readAll(exec.bufferout);
if (exec.cfg.closout) {
stdio.close(exec.io.stdout);
exec.cfg.closout = false;
}
if (exec.cfg.closerr) {
stdio.close(exec.io.stderr);
exec.cfg.closerr = false;
}
exec.active = null;
exec.cmds = [];
if (typeof exec.callback !== 'function')
return;
try{
exec.callback(out);
} catch(err) {
console.log(err.fileName+':'+err.lineNumber+':'+err.columnNumber+':'+err.message);
stdio.write(io.stderr,'Shell: internal error in command execution\n');
io.exit(1);
return;
}
},
run:function(cb) {
if (typeof cb === 'function')
exec.callback = cb;
var c = exec.cmds.shift();
if (!c) {
exec.exit();
return;
}
exec.internal.doCommand(c);
}
};
function help() {
stdio.write(io.stdout,`
sh - command interpreter (shell)
Usage: sh [OPTION] [FILE]
Options:
-? Print this help information
`);
}
function resolvePATH(txt) {
if (typeof env.PATH === 'undefined')
env.PATH = '/bin';
var paths = env.PATH.split(':');
var path = null;
paths.forEach(function(p) {
if (path)
return;
var rp = clite.resolvePath(txt,p);
var st = stdio.stat(rp);
if (st) {
path = rp;
}
});
return path;
}
function preprocess(txt) {
var parts = txt.split('$');
var pptxt = '';
if (parts.length == 1)
return txt;
parts.forEach(function(p,i) {
if (i > 0) {
var b = p;
var e = '';
if (b[0] == '{') {
var pi = b.indexOf('}');
e = b.substring(pi+1);
b = b.substring(1,pi);
}else{
var pi = b.indexOf(' ');
if (pi > 0) {
e = b.substring(pi);
b = b.substring(0,pi);
}
}
if (typeof env[b] === 'string') {
b = env[b];
}else{
b = '';
}
pptxt += b+e;
return;
}
pptxt += p;
});
txt = pptxt;
parts = txt.split('`');
pptxt = '';
if (parts.length == 1)
return txt;
parts.forEach(function(p,i) {
if (i > 0) {
var b = p;
var e = '';
var pi = b.indexOf('`');
if (pi > 0) {
e = b.substring(pi);
b = b.substring(0,pi);
}
// TODO: replace b with the output of the command in b (command substitution)
pptxt = b+e;
return;
}
pptxt += p;
});
return pptxt;
}
function tabfill(c) {
var parts = clite.strToArgs(c);
var ai = parts.length-1;
if (ai < 0)
return null;
var a = parts[ai];
var add = '';
if (ai == 0) {
// test for shell macros/builtins
Object.keys(macro).forEach(function(key) {
if (add == '' && key.substring(0,a.length) == a) {
add = key.substring(a.length);
}
});
if (add == '') {
// test for commands
var pparts = env.PATH.split(':');
pparts.forEach(function(pp) {
if (add != '')
return;
var fd = stdio.open('/bin',stdio.flags.O_SEARCH|stdio.flags.O_DIRECTORY);
var f;
while (add == '' && (f = stdio.read(fd)) != null) {
if (f.substring(0,a.length) == a) {
add = f.substring(a.length)+' ';
}
}
stdio.close(fd);
});
}
}else{
// TODO: path fill
}
if (add == '')
return null;
stdio.write(io.stdout,'\b'+add);
return c+add;
}
function writePrompt() {
// generate the prompt
var p = env.PS1;
if (typeof p === 'undefined')
p = '${USER}:${PWD}'
var prompt = preprocess(p);
if (stdlib.getuid() == 0) {
prompt += '# ';
}else{
prompt += '$ ';
}
stdio.write(io.stdout,prompt);
}
function inputRead() {
if (has_exited)
return;
var c = '';
var cc = 0;
function inputProcess(str) {
switch (str) {
case '\t': // tab
case 9:
var ct = tabfill(c);
if (ct != null) {
c = ct;
cc = c.length;
}
break;
case '\n': // enter
term.ttyctrl('raw',false);
history.add(c);
history.resetCurrent();
exec.setCommand(c);
exec.run(inputRead);
c = '';
cc = 0;
return;
break;
case '\b':
case 8:
if (c.length > 0) {
cc--;
c = c.substring(0,cc);
stdio.write(io.stdout,'\b \b');
}
break;
case -3: // down arrow
var ct = history.down();
if (ct != null) {
for (var i=0; i<c.length; i++) {
stdio.write(io.stdout,'\b \b');
}
c = ct;
cc = c.length;
stdio.write(io.stdout,c);
}
break;
case -4: // up arrow
var ct = history.up();
if (ct != null) {
for (var i=0; i<c.length; i++) {
stdio.write(io.stdout,'\b \b');
}
c = ct;
cc = c.length;
stdio.write(io.stdout,c);
}
break;
default:
if (typeof str === 'string') {
c += str;
cc++;
history.setCurrent(c);
}
}
stdio.read(io.stdin,inputProcess);
}
// write the prompt
writePrompt();
// set tty to raw mode
term.ttyctrl('raw',true);
// then read from stdin
stdio.read(io.stdin,inputProcess);
}
function main(args) {
var f = null;
var s = false;
for (var i=1; i<args.length; i++) {
if (args[i][0] == '-') {
for (var j=1; j<args[i].length; j++) {
switch (args[i][j]) {
case '?':
help();
return 0;
break;
case 's':
s = true;
break;
default:
stdio.fprintf(io.stderr,'unknown argument -%c\n',args[i][j]);
}
}
}else if (f == null) {
f = clite.resolvePath(args[i]);
}else{
stdio.fprintf(io.stderr,'unknown argument - %s\n',args[i]);
}
}
// TODO: hacks r uglee
// if args[0] isn't the shell itself, then we're running a shell script
if (!f && args[0] != '/bin/sh' && args[0] != 'sh') {
f = clite.resolvePath(args[0]);
if (!f)
return 1;
// make the arguments readable by the shell script
env['#'] = args.length.toString();
for (var i=0; i<args.length; i++) {
env[i.toString()] = args[i];
}
}
// run a shell script if one is specified
if (f != null) {
parser.queue(f);
parser.run(function() {
io.exit(0);
});
// run a shell script from stdin
}else if (!stdio.isatty(io.stdin)) {
if (!s)
return 0;
parser.stdin();
// otherwise we have an interactive shell
// so run /etc/shrc and ~/.shrc
}else{
parser.queue('/etc/shrc');
parser.queue(env.HOME+'/.shrc');
parser.run(function() {
history.clear();
inputRead();
});
}
return null;
}
return main(args);
});
}