2022-04-06 19:59:55 +00:00
|
|
|
"use strict";
|
|
|
|
(function(){
|
|
|
|
var variables = [];
|
|
|
|
var functions = [];
|
|
|
|
var file = "[anonymous]";
|
|
|
|
|
|
|
|
|
|
|
|
function executeCommand(args, std) {
|
2022-04-06 20:23:41 +00:00
|
|
|
console.log(`args: ${args}, std: ${std}`);
|
2022-04-06 19:59:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function astSequence() {
|
|
|
|
let commands = [];
|
|
|
|
return {
|
|
|
|
add: c => commands.push(c),
|
|
|
|
execute: (std) => commands.forEach(c => c.execute(std)),
|
2022-04-07 19:36:39 +00:00
|
|
|
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}",
|
2022-04-06 19:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function astCommand() {
|
|
|
|
let args = [];
|
|
|
|
return {
|
|
|
|
add: a => args.push(a),
|
2022-04-06 20:23:41 +00:00
|
|
|
size: a => args.length,
|
2022-04-06 19:59:55 +00:00
|
|
|
execute: (std) => {
|
|
|
|
let evaluatedArgs = args.map(a => a.evaluate());
|
|
|
|
executeCommand(evaluatedArgs, std);
|
|
|
|
},
|
2022-04-07 19:36:39 +00:00
|
|
|
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}",
|
2022-04-06 19:59:55 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-04-06 20:23:41 +00:00
|
|
|
function astAssignment(name) {
|
|
|
|
let value = astValueString("");
|
|
|
|
return {
|
|
|
|
setValue: v => { value = v; },
|
|
|
|
execute: (std) => {
|
|
|
|
variables[name] = value.evaluate();
|
2022-04-07 19:36:39 +00:00
|
|
|
},
|
|
|
|
toString: () => `assign '${name}'=\n${value.toString().split('\n').map(l => " " + l).join("\n")}`,
|
2022-04-06 20:23:41 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-04-06 19:59:55 +00:00
|
|
|
function astValueCompound() {
|
|
|
|
let components = [];
|
2022-04-07 19:36:39 +00:00
|
|
|
const self = {
|
2022-04-06 19:59:55 +00:00
|
|
|
add: c => components.push(c),
|
|
|
|
evaluate: () => components.map(c => c.evaluate()).join(""),
|
2022-04-07 19:36:39 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
},
|
2022-04-06 19:59:55 +00:00
|
|
|
};
|
2022-04-07 19:36:39 +00:00
|
|
|
return self;
|
2022-04-06 19:59:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function astValueString(str) {
|
|
|
|
return {
|
|
|
|
evaluate: () => str,
|
2022-04-07 19:36:39 +00:00
|
|
|
toString: () => "'" + str + "'",
|
2022-04-06 19:59:55 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function astValueVar(name) {
|
|
|
|
return {
|
|
|
|
// add support for multiple arguments in one variable
|
|
|
|
evaluate: () => variables[name],
|
2022-04-07 19:36:39 +00:00
|
|
|
toString: () => `var '${name}'`,
|
2022-04-06 19:59:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function astValueCommandSubstitution(ast) {
|
|
|
|
return {
|
|
|
|
evaluate: () => {
|
|
|
|
// TODO
|
|
|
|
},
|
2022-04-07 19:36:39 +00:00
|
|
|
toString: () => "not implemented",
|
2022-04-06 19:59:55 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function astValueProcessSubstitution(ast) {
|
|
|
|
return {
|
|
|
|
evaluate: () => {
|
|
|
|
// TODO
|
|
|
|
},
|
2022-04-07 19:36:39 +00:00
|
|
|
toString: () => "not implemented",
|
2022-04-06 19:59:55 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function panic(line, message) {
|
|
|
|
throw `${file}: line ${line}: panic: ${message}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function syntaxError(line, message) {
|
|
|
|
throw `${file}: line ${line}: syntax error: ${message}`;
|
|
|
|
}
|
|
|
|
|
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 = astValueCompound();
|
|
|
|
|
|
|
|
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(astValueString(buffer));
|
|
|
|
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(astValueVar(buffer));
|
|
|
|
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(astValueString(buffer));
|
|
|
|
break;
|
|
|
|
case QS_VARIABLE:
|
|
|
|
astRoot.add(astValueVar(buffer));
|
|
|
|
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 = astSequence();
|
|
|
|
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 = astCommand();
|
|
|
|
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(astValueVar(buffer.substring(1)));
|
|
|
|
} else {
|
2022-04-08 18:28:16 +00:00
|
|
|
value.add(astValueString(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(astValueVar(buffer.substring(1)));
|
|
|
|
} else {
|
2022-04-08 18:28:16 +00:00
|
|
|
current.add(astValueString(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(astValueVar(buffer.substring(1)));
|
|
|
|
} else {
|
2022-04-08 18:28:16 +00:00
|
|
|
value.add(astValueString(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(astValueVar(buffer.substring(1)));
|
|
|
|
} else {
|
2022-04-08 18:28:16 +00:00
|
|
|
current.add(astValueString(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 = astValueCompound();
|
|
|
|
}
|
|
|
|
if (buffer) {
|
|
|
|
value.add(astValueString(buffer));
|
|
|
|
buffer = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
const end = findSymbolInScope(content, line, '"', i + 1, length);
|
2022-04-07 19:39:48 +00:00
|
|
|
const [ast, _line] = doubleQuoteToAst(content.substring(i + 1, end), line);
|
|
|
|
line = _line;
|
2022-04-07 19:36:39 +00:00
|
|
|
|
|
|
|
value.add(ast);
|
|
|
|
|
|
|
|
i = end;
|
|
|
|
} else if (c == "'") {
|
|
|
|
const end = findSymbolInScope(content, line, "'", i + 1, length);
|
|
|
|
buffer += content.substring(i + 1, end);
|
2022-04-08 18:28:16 +00:00
|
|
|
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 = astAssignment(buffer);
|
|
|
|
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(astValueVar(buffer.substring(1)));
|
|
|
|
} else {
|
|
|
|
value.add(astValueString(buffer));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
current.setValue(value.reduce());
|
|
|
|
value = null;
|
|
|
|
} else if (buffer) {
|
|
|
|
if (buffer[0] == "$") {
|
|
|
|
current.setValue(astValueVar(buffer.substring(1)));
|
|
|
|
} else {
|
|
|
|
current.setValue(astValueString(buffer));
|
|
|
|
}
|
2022-04-06 20:23:41 +00:00
|
|
|
} else {
|
2022-04-07 19:36:39 +00:00
|
|
|
current.setValue(astValueString(""));
|
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 = astValueCompound();
|
|
|
|
}
|
|
|
|
if (buffer) {
|
|
|
|
value.add(astValueString(buffer));
|
|
|
|
buffer = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
const end = findSymbolInScope(content, line, '"', i + 1, length);
|
2022-04-07 19:39:48 +00:00
|
|
|
const [ast, _line] = doubleQuoteToAst(content.substring(i + 1, end), line);
|
|
|
|
line = _line;
|
2022-04-07 19:36:39 +00:00
|
|
|
|
|
|
|
value.add(ast);
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
})();
|