sh.js/interpreter.js

485 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2022-04-06 19:59:55 +00:00
"use strict";
(function(){
var variables = [];
var functions = [];
2022-04-11 18:51:28 +00:00
var currentFile = "[anonymous]";
2022-04-06 19:59:55 +00:00
2022-04-11 18:51:28 +00:00
const buildins = {
echo: function(args, ctx) {
const length = args.length;
for (let i = 1; i < length; i++) {
ctx.stdout.write(args[i]);
if (i < length - 1) {
ctx.stdout.write(" ");
}
}
ctx.stdout.write("\n");
ctx.stdout.flush();
return 0;
},
};
function executeCommand(args, ctx) {
if (!ctx) {
ctx = defaultCtx;
}
let errorCode = 255;
if (buildins[args[0]]) {
errorCode = buildins[args[0]](args, ctx);
} else {
console.debug(`command not found; args: ${args}, ctx: ${ctx}`);
}
console.debug("exit status: " + errorCode);
2022-04-06 19:59:55 +00:00
}
2022-04-11 18:51:28 +00:00
const file = {
console: function() {
let buffer = [];
const self = {
read: () => "",
close: () => self.flush(),
flush: () => {
console.log(buffer);
buffer = [];
},
write: (str) => {
buffer += str;
},
};
return self;
}
};
function makeCtx(stdin, stdout, stderr) {
return {
stdin: stdin,
stdout: stdout,
stderr: stderr,
}
}
const defaultCtx = makeCtx(file.console(), file.console(), file.console());
const ast = {
sequence: function() {
let commands = [];
return {
add: c => commands.push(c),
2022-04-11 18:51:28 +00:00
execute: (ctx) => commands.forEach(c => c.execute(ctx)),
toString: () => "sequence {\n" + commands
.map(c => c.toString())
.map(s => s.split("\n"))
.map(a => a.map(s => " " + s))
.map(a => a.join("\n"))
.join(",\n") + "\n}",
}
},
command: function() {
let args = [];
return {
add: a => args.push(a),
size: a => args.length,
2022-04-11 18:51:28 +00:00
execute: (ctx) => {
let evaluatedArgs = args.map(a => a.evaluate());
2022-04-11 18:51:28 +00:00
executeCommand(evaluatedArgs, ctx);
},
toString: () => "command {\n" + args
.map(a => a.toString())
.map(s => s.split("\n"))
.map(a => a.map(s => " " + s))
.map(a => a.join("\n"))
.join(",\n") + "\n}",
};
},
assignment: function(name) {
let value = ast.value.string("");
return {
setValue: v => { value = v; },
2022-04-11 18:51:28 +00:00
execute: (ctx) => {
variables[name] = value.evaluate();
},
toString: () => `assign '${name}'=\n${value.toString().split('\n').map(l => " " + l).join("\n")}`,
};
},
value: {
compound: function() {
let components = [];
const self = {
add: c => components.push(c),
evaluate: () => components.map(c => c.evaluate()).join(""),
toString: () => "compound {\n" + components
.map(a => a.toString())
.map(s => s.split("\n"))
.map(a => a.map(s => " " + s))
.map(a => a.join("\n"))
.join(",\n") + "\n}",
reduce: () => {
if (components.length == 1) {
if (components[0].reduce) {
return components[0].reduce();
} else {
return components[0];
}
} else {
return self;
}
},
};
return self;
2022-04-06 19:59:55 +00:00
},
string: function(str) {
return {
evaluate: () => str,
toString: () => "'" + str + "'",
};
2022-04-07 19:36:39 +00:00
},
2022-04-06 20:23:41 +00:00
variable: function(name) {
return {
// add support for multiple arguments in one variable
evaluate: () => variables[name],
toString: () => `var '${name}'`,
2022-04-07 19:36:39 +00:00
}
},
2022-04-06 19:59:55 +00:00
commandSubstitution: function(ast) {
return {
evaluate: () => {
// TODO
},
toString: () => "not implemented",
};
2022-04-06 19:59:55 +00:00
},
processSubstitution: function(ast) {
return {
evaluate: () => {
// TODO
},
toString: () => "not implemented",
};
2022-04-06 19:59:55 +00:00
},
},
2022-04-06 19:59:55 +00:00
}
function panic(line, message) {
2022-04-11 18:51:28 +00:00
throw `${currentFile}: line ${line}: panic: ${message}`;
2022-04-06 19:59:55 +00:00
}
function syntaxError(line, message) {
2022-04-11 18:51:28 +00:00
throw `${currentFile}: line ${line}: syntax error: ${message}`;
2022-04-06 19:59:55 +00:00
}
2022-04-07 19:36:39 +00:00
function findSymbolInScope(content, line, symbol, startPosition, length) {
let scopeStack = [];
for (let i = startPosition; i < length; i++) {
const c = content[i];
if (c == '\n') {
line++;
}
if (scopeStack.length == 0) {
if (c == symbol) {
return i;
} else if (c == '"') {
scopeStack.push('"');
} else if (c == '$' && i < length - 1 && content[i + 1] == '(') {
i++;
scopeStack.push(')');
} else if (c == '`') {
scopeStack.push('`');
} else if (c == "'") {
scopeStack.push("'");
} else {
// continue
}
} else {
const top = scopeStack[scopeStack.length - 1];
if (c == top) {
scopeStack.pop();
} else if (top == '"' && c == '$' && i < length - 1 && content[i + 1] == '(') {
i++;
scopeStack.push(')');
} else if (top == '"' && c == '`') {
scopeStack.push('`');
} else if (top == ')' || top == '`') {
if (c == '"') {
scopeStack.push('"');
} else if (c == "'") {
scopeStack.push("'");
} else {
// continue
}
} else {
// continue
}
}
}
syntaxError(line, "unexpected end of file");
}
function doubleQuoteToAst(quoteContent, line) {
const length = quoteContent.length;
let astRoot = ast.value.compound();
2022-04-07 19:36:39 +00:00
let buffer = "";
const QS_INIT = 0;
const QS_VARIABLE = 1;
const QS_SUBSTITUTION = 2;
let state = QS_INIT;
for (let i = 0; i < length; i++) {
const c = quoteContent[i];
if (c == '\n') {
line++;
}
switch(state) {
case QS_INIT:
if (c == '$') {
astRoot.add(ast.value.string(buffer));
2022-04-07 19:36:39 +00:00
buffer = "";
if (i < length - 1 && quoteContent[i + 1] == '(') {
state = QS_SUBSTITUTION;
i += 1;
} else {
state = QS_VARIABLE;
}
} else {
buffer += c;
}
break;
case QS_VARIABLE:
if (!("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".includes(c))) {
astRoot.add(ast.value.variable(buffer));
2022-04-07 19:36:39 +00:00
buffer = "";
state = QS_INIT;
i--;
} else {
buffer += c;
}
break;
case QS_SUBSTITUTION:
throw "not implemented";
break;
default:
panic(line, "unknown parse state");
break;
}
}
if (buffer) {
switch(state) {
case QS_INIT:
astRoot.add(ast.value.string(buffer));
2022-04-07 19:36:39 +00:00
break;
case QS_VARIABLE:
astRoot.add(ast.value.variable(buffer));
2022-04-07 19:36:39 +00:00
break;
case QS_SUBSTITUTION:
throw "not implemented";
break;
default:
panic(line, "unknown parse state");
break;
}
}
2022-04-07 19:39:48 +00:00
return [astRoot, line];
2022-04-07 19:36:39 +00:00
}
2022-04-06 19:59:55 +00:00
function parseCommands(content) {
let line = 1;
2022-04-06 20:23:41 +00:00
const PS_INIT = 0;
const PS_COMMENT = -1;
const PS_COMMAND = 1;
const PS_ASSIGN = 2;
2022-04-06 19:59:55 +00:00
let astRoot = ast.sequence();
2022-04-06 19:59:55 +00:00
let current = null;
2022-04-07 19:36:39 +00:00
let value = null;
2022-04-06 19:59:55 +00:00
let state = PS_INIT;
let buffer = "";
const length = content.length;
for (let i = 0; i < length; i++) {
const c = content[i];
switch(state) {
case PS_INIT:
if (['\n', '\r', ' ', '\t', ';'].includes(c)) {
// continue
} else if (c == '#') {
state = PS_COMMENT;
} else {
current = ast.command();
2022-04-06 19:59:55 +00:00
buffer = c;
state = PS_COMMAND;
}
break;
case PS_COMMENT:
if (c == '\n') {
state = PS_INIT;
}
break;
case PS_COMMAND:
// check for escape and quotes
if (c == ';' || c == '\n') {
2022-04-07 19:36:39 +00:00
if (value) {
if (buffer) {
if (buffer[0] == '$') {
value.add(ast.value.variable(buffer.substring(1)));
2022-04-07 19:36:39 +00:00
} else {
value.add(ast.value.string(buffer.replaceAll('\\$', '$')));
2022-04-07 19:36:39 +00:00
}
buffer = "";
}
current.add(value.reduce());
value = null;
} else if (buffer) {
2022-04-06 20:23:41 +00:00
if (buffer[0] == '$') {
current.add(ast.value.variable(buffer.substring(1)));
2022-04-06 20:23:41 +00:00
} else {
current.add(ast.value.string(buffer.replaceAll('\\$', '$')));
2022-04-06 20:23:41 +00:00
}
2022-04-06 19:59:55 +00:00
buffer = "";
}
astRoot.add(current);
current = null;
state = PS_INIT;
} else if (c == ' ' || c == '\t') {
2022-04-07 19:36:39 +00:00
if (value) {
if (buffer) {
if (buffer[0] == '$') {
value.add(ast.value.variable(buffer.substring(1)));
2022-04-07 19:36:39 +00:00
} else {
value.add(ast.value.string(buffer.replaceAll('\\$', '$')));
2022-04-07 19:36:39 +00:00
}
buffer = "";
}
current.add(value.reduce());
value = null;
} else if (buffer) {
2022-04-06 20:23:41 +00:00
if (buffer[0] == '$') {
current.add(ast.value.variable(buffer.substring(1)));
2022-04-06 20:23:41 +00:00
} else {
current.add(ast.value.string(buffer.replaceAll('\\$', '$')));
2022-04-06 20:23:41 +00:00
}
2022-04-06 19:59:55 +00:00
buffer = "";
}
2022-04-07 19:36:39 +00:00
} else if (c == '"') {
if (!value) {
value = ast.value.compound();
2022-04-07 19:36:39 +00:00
}
if (buffer) {
value.add(ast.value.string(buffer));
2022-04-07 19:36:39 +00:00
buffer = "";
}
const end = findSymbolInScope(content, line, '"', i + 1, length);
const [_ast, _line] = doubleQuoteToAst(content.substring(i + 1, end), line);
2022-04-07 19:39:48 +00:00
line = _line;
2022-04-07 19:36:39 +00:00
value.add(_ast);
2022-04-07 19:36:39 +00:00
i = end;
} else if (c == "'") {
const end = findSymbolInScope(content, line, "'", i + 1, length);
buffer += content.substring(i + 1, end);
if (buffer && buffer[0] == '$') {
// mask dollar sign in buffer so the variable is not expanded
buffer = '\\' + buffer;
}
2022-04-07 19:36:39 +00:00
i = end;
2022-04-06 20:23:41 +00:00
} else if (c == '=' && current.size() == 0) {
current = ast.assignment(buffer);
2022-04-06 20:23:41 +00:00
state = PS_ASSIGN;
buffer = "";
} else {
buffer += c;
}
break;
case PS_ASSIGN:
// check for escape and quotes
if (c == ';' || c == '\n') {
2022-04-07 19:36:39 +00:00
if (value) {
if (buffer) {
if (buffer[0] == "$") {
value.add(ast.value.variable(buffer.substring(1)));
2022-04-07 19:36:39 +00:00
} else {
value.add(ast.value.string(buffer));
2022-04-07 19:36:39 +00:00
}
}
current.setValue(value.reduce());
value = null;
} else if (buffer) {
if (buffer[0] == "$") {
current.setValue(ast.value.variable(buffer.substring(1)));
2022-04-07 19:36:39 +00:00
} else {
current.setValue(ast.value.string(buffer));
2022-04-07 19:36:39 +00:00
}
2022-04-06 20:23:41 +00:00
} else {
current.setValue(ast.value.string(""));
2022-04-06 20:23:41 +00:00
}
buffer = "";
astRoot.add(current);
current = null;
state = PS_INIT;
2022-04-07 19:36:39 +00:00
} else if (c == '"') {
if (!value) {
value = ast.value.compound();
2022-04-07 19:36:39 +00:00
}
if (buffer) {
value.add(ast.value.string(buffer));
2022-04-07 19:36:39 +00:00
buffer = "";
}
const end = findSymbolInScope(content, line, '"', i + 1, length);
const [_ast, _line] = doubleQuoteToAst(content.substring(i + 1, end), line);
2022-04-07 19:39:48 +00:00
line = _line;
2022-04-07 19:36:39 +00:00
value.add(_ast);
2022-04-07 19:36:39 +00:00
i = end;
} else if (c == "'") {
const end = findSymbolInScope(content, line, "'", i + 1, length);
buffer += content.substring(i + 1, end);
i = end;
2022-04-06 20:23:41 +00:00
} else if (c == ' ' || c == '\t') {
// would normale set exported variable for command but we don't support that anyway
// current.setValue(astValueString(buffer));
buffer = "";
current = null;
state = PS_INIT;
2022-04-06 19:59:55 +00:00
} else {
buffer += c;
}
break;
default:
panic(line, "unknown parse state");
break;
}
if (c == '\n') {
line++;
}
}
return astRoot;
}
window.sh = {
parse: parseCommands,
};
})();