Fix #20
This commit is contained in:
parent
b605436ab4
commit
9523c31afe
|
@ -270,11 +270,11 @@ export class Commands {
|
|||
.map(arg => Path.interpret(this.environment.get("cwd"), arg))
|
||||
.map(path => {
|
||||
if (!this.fileSystem.has(path))
|
||||
return `cat: ${it}: No such file`;
|
||||
return `cat: ${path}: No such file`;
|
||||
|
||||
const node = this.fileSystem.get(path);
|
||||
if (!(node instanceof File))
|
||||
return `cat: ${it}: No such file`;
|
||||
return `cat: ${path}: No such file`;
|
||||
|
||||
return node.contents;
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {getFileExtension, IllegalArgumentError, IllegalStateError} from "./Shared";
|
||||
import {emptyFunction, getFileExtension, IllegalArgumentError, IllegalStateError} from "./Shared";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -312,6 +312,19 @@ export abstract class Node {
|
|||
*/
|
||||
abstract nameString(name: string, path: Path): string;
|
||||
|
||||
/**
|
||||
* Recursively visits all nodes contained within this node.
|
||||
*
|
||||
* @param path the path to the current node
|
||||
* @param fun the function to apply to each node, including this node
|
||||
* @param pre the function to apply to the current node before applying the first `fun`
|
||||
* @param post the function to apply to the current node after applying the last `fun`
|
||||
*/
|
||||
abstract visit(path: string,
|
||||
fun: (node: Node, path: string) => void,
|
||||
pre: (node: Node, path: string) => void,
|
||||
post: (node: Node, path: string) => void): void;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the JSON deserialization of the given string as a node.
|
||||
|
@ -451,6 +464,18 @@ export class Directory extends Node {
|
|||
return `<a href="#" class="dirLink" onclick="execute('cd ${path.toString(true)}');execute('ls')">${name}</a>`;
|
||||
}
|
||||
|
||||
visit(path: string,
|
||||
fun: (node: Node, path: string) => void,
|
||||
pre: (node: Node, path: string) => void = emptyFunction,
|
||||
post: (node: Node, path: string) => void = emptyFunction) {
|
||||
pre(this, path);
|
||||
|
||||
fun(this, path);
|
||||
Object.keys(this._nodes).forEach(name => this._nodes[name].visit(`${path}/${name}`, fun, pre, post));
|
||||
|
||||
post(this, path);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given object into a directory.
|
||||
|
@ -509,6 +534,15 @@ export class File extends Node {
|
|||
}
|
||||
}
|
||||
|
||||
visit(path: string,
|
||||
fun: (node: Node, path: string) => void,
|
||||
pre: (node: Node, path: string) => void = emptyFunction,
|
||||
post: (node: Node, path: string) => void = emptyFunction) {
|
||||
pre(this, path);
|
||||
fun(this, path);
|
||||
post(this, path);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given object into a file.
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
import {Environment} from "./Environment";
|
||||
import {Directory, FileSystem, Path} from "./FileSystem";
|
||||
import {IllegalArgumentError, IllegalStateError} from "./Shared";
|
||||
import {InputArgs} from "./Shell";
|
||||
import {EscapeCharacters} from "./Terminal";
|
||||
|
||||
|
||||
/**
|
||||
* A parser for input strings.
|
||||
*/
|
||||
export class InputParser {
|
||||
/**
|
||||
* The tokenizer turn the input into tokens with.
|
||||
*/
|
||||
private readonly tokenizer: Tokenizer;
|
||||
/**
|
||||
* The globber to glob file paths with.
|
||||
*/
|
||||
private readonly globber: Globber;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new input parser.
|
||||
*
|
||||
* Usually, you'll want to use the static `InputParser#create` method instead.
|
||||
*
|
||||
* @param tokenizer the tokenizer turn the input into tokens with
|
||||
* @param globber the globber to glob file paths with
|
||||
*/
|
||||
constructor(tokenizer: Tokenizer, globber: Globber) {
|
||||
this.tokenizer = tokenizer;
|
||||
this.globber = globber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new input parser.
|
||||
*
|
||||
* @param environment the environment containing the variables to substitute
|
||||
* @param fileSystem the file system describing the valid paths to glob
|
||||
*/
|
||||
static create(environment: Environment, fileSystem: FileSystem): InputParser {
|
||||
return new InputParser(new Tokenizer(environment), new Globber(fileSystem, environment.get("cwd")));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given input string to a set of command-line arguments.
|
||||
*
|
||||
* @param input the string to parse
|
||||
*/
|
||||
parse(input: string): InputArgs {
|
||||
const tokens = this.globber.glob(this.tokenizer.tokenize(input));
|
||||
const command = tokens[0] ?? "";
|
||||
const [options, args] = this.parseOpts(tokens.slice(1).filter(it => !it.startsWith(EscapeCharacters.Escape)));
|
||||
const redirectTarget = this.getRedirectTarget(tokens.slice(1));
|
||||
|
||||
return new InputArgs(command, options, args, redirectTarget);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the redirect target described by the last token that describes a redirect target, or the default redirect
|
||||
* target if no token describes a redirect target.
|
||||
*
|
||||
* @param tokens an array of tokens of which some tokens may describe a redirect target
|
||||
*/
|
||||
private getRedirectTarget(tokens: string[]): InputArgs.RedirectTarget {
|
||||
let redirectTarget: InputArgs.RedirectTarget = ["default"];
|
||||
|
||||
tokens.forEach(token => {
|
||||
if (token.startsWith(`${EscapeCharacters.Escape}>${EscapeCharacters.Escape}>`))
|
||||
redirectTarget = ["append", token.slice(4)];
|
||||
else if (token.startsWith(`${EscapeCharacters.Escape}>`))
|
||||
redirectTarget = ["write", token.slice(2)];
|
||||
});
|
||||
|
||||
return redirectTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses options and arguments.
|
||||
*
|
||||
* @param tokens the tokens that form the options and arguments
|
||||
*/
|
||||
private parseOpts(tokens: string[]): [InputArgs.Options, string[]] {
|
||||
const options: { [key: string]: string | null } = {};
|
||||
|
||||
let i;
|
||||
for (i = 0; i < tokens.length; i++) {
|
||||
const arg = tokens[i];
|
||||
|
||||
if (!arg.startsWith("-") || arg === "--")
|
||||
break;
|
||||
|
||||
const argsParts = arg.split(/=(.*)/, 2);
|
||||
if (argsParts.length === 0 || argsParts.length > 2)
|
||||
throw new IllegalArgumentError("Unexpected number of parts.");
|
||||
if (argsParts[0].indexOf(' ') >= 0)
|
||||
break;
|
||||
|
||||
const value = argsParts.length === 1 ? null : argsParts[1];
|
||||
|
||||
if (argsParts[0].startsWith("--")) {
|
||||
const key = argsParts[0].substr(2);
|
||||
if (key === "")
|
||||
break;
|
||||
|
||||
options[key] = value;
|
||||
} else {
|
||||
const keys = argsParts[0].substr(1);
|
||||
if (keys === "")
|
||||
break;
|
||||
|
||||
if (keys.length === 1) {
|
||||
options[keys] = value;
|
||||
} else {
|
||||
if (value !== null)
|
||||
throw new IllegalArgumentError("Cannot assign value to multiple short options.");
|
||||
|
||||
for (const key of keys)
|
||||
options[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [options, tokens.slice(i)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns an input string into a series of expanded tokens.
|
||||
*/
|
||||
export class Tokenizer {
|
||||
/**
|
||||
* The environment containing the variables to substitute.
|
||||
*/
|
||||
private readonly environment: Environment;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new tokenizer.
|
||||
*
|
||||
* @param environment the environment containing the variables to substitute
|
||||
*/
|
||||
constructor(environment: Environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tokenizes the input string and expands the tokens.
|
||||
*
|
||||
* @param input the string to tokenize
|
||||
*/
|
||||
tokenize(input: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
|
||||
let token = "";
|
||||
let isInSingleQuotes = false;
|
||||
let isInDoubleQuotes = false;
|
||||
let isInCurlyBraces = 0;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const char = input[i];
|
||||
switch (char) {
|
||||
case "\\":
|
||||
if (i === input.length - 1)
|
||||
throw new IllegalArgumentError(
|
||||
"Unexpected end of input. '\\' was used but there was nothing to escape.");
|
||||
|
||||
const nextChar = input[i + 1];
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
token += "\\" + nextChar;
|
||||
else if (nextChar === "n")
|
||||
token += "\n";
|
||||
else
|
||||
token += nextChar;
|
||||
i++;
|
||||
break;
|
||||
case "'":
|
||||
if (isInDoubleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInSingleQuotes = !isInSingleQuotes;
|
||||
break;
|
||||
case "\"":
|
||||
if (isInSingleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInDoubleQuotes = !isInDoubleQuotes;
|
||||
break;
|
||||
case "{":
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInCurlyBraces++;
|
||||
break;
|
||||
case "}":
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInCurlyBraces--;
|
||||
|
||||
if (isInCurlyBraces < 0)
|
||||
throw new IllegalArgumentError("Unexpected closing '}' without corresponding '{'.");
|
||||
break;
|
||||
case " ":
|
||||
if (isInSingleQuotes || isInDoubleQuotes) {
|
||||
token += char;
|
||||
} else if (token !== "") {
|
||||
tokens.push(token);
|
||||
token = "";
|
||||
}
|
||||
break;
|
||||
case "$":
|
||||
if (isInSingleQuotes || isInDoubleQuotes) {
|
||||
token += char;
|
||||
break;
|
||||
}
|
||||
|
||||
let key = "";
|
||||
for (; i + 1 < input.length; i++) {
|
||||
const nextChar = input[i + 1];
|
||||
if (nextChar.match(/^[0-9a-z_]+$/i))
|
||||
key += nextChar;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (key === "")
|
||||
throw new IllegalArgumentError(`Missing variable name after '$'.`);
|
||||
|
||||
token += this.environment.getOrDefault(key, "");
|
||||
break;
|
||||
case ">":
|
||||
if (isInSingleQuotes || isInDoubleQuotes) {
|
||||
token += char;
|
||||
break;
|
||||
}
|
||||
|
||||
if (token !== "") {
|
||||
tokens.push(token);
|
||||
token = "";
|
||||
}
|
||||
|
||||
token += EscapeCharacters.Escape + ">";
|
||||
if (input[i + 1] === ">") {
|
||||
token += EscapeCharacters.Escape + ">";
|
||||
i++;
|
||||
}
|
||||
while (input[i + 1] === " ")
|
||||
i++;
|
||||
|
||||
break;
|
||||
case "*":
|
||||
case "?":
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
token += char;
|
||||
else
|
||||
token += EscapeCharacters.Escape + char;
|
||||
break;
|
||||
default:
|
||||
token += char;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (token !== "")
|
||||
tokens.push(token);
|
||||
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
throw new IllegalArgumentError("Unexpected end of input. Missing closing quotation mark.");
|
||||
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Globs file paths in tokens.
|
||||
*/
|
||||
export class Globber {
|
||||
/**
|
||||
* The file system describing the valid paths to glob.
|
||||
*/
|
||||
private readonly fileSystem: FileSystem;
|
||||
/**
|
||||
* The path to the current working directory to which globbing is relative.
|
||||
*/
|
||||
private readonly cwd: Path;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new globber.
|
||||
*
|
||||
* @param fileSystem the file system describing the valid paths to glob
|
||||
* @param cwd the path to the current working directory to which globbing is relative
|
||||
*/
|
||||
constructor(fileSystem: FileSystem, cwd: string) {
|
||||
this.fileSystem = fileSystem;
|
||||
this.cwd = new Path(cwd);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns globbed tokens.
|
||||
*
|
||||
* @param tokens the tokens to glob
|
||||
*/
|
||||
glob(tokens: string[]): string[] {
|
||||
const cwdNode = this.fileSystem.get(this.cwd);
|
||||
if (!(cwdNode instanceof Directory))
|
||||
throw new IllegalStateError("cwd is not a directory.");
|
||||
|
||||
const paths: string[] = [];
|
||||
cwdNode.visit("", (_, path) => paths.push(path.slice(1)));
|
||||
paths.shift();
|
||||
|
||||
let newTokens: string[] = [];
|
||||
tokens.forEach(token => {
|
||||
if (token.indexOf(EscapeCharacters.Escape + "?") < 0 && token.indexOf(EscapeCharacters.Escape + "*") < 0) {
|
||||
newTokens = newTokens.concat([token]);
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = token
|
||||
.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
|
||||
.replaceAll(new RegExp(`${EscapeCharacters.Escape}\\\\\\?`), ".")
|
||||
.replaceAll(new RegExp(`${EscapeCharacters.Escape}\\\\\\*${EscapeCharacters.Escape}\\\\\\*`), ".*")
|
||||
.replaceAll(new RegExp(`${EscapeCharacters.Escape}\\\\\\*`), "[^/]*");
|
||||
|
||||
const matches = paths.filter(path => path.match(new RegExp(`^${pattern}$`)));
|
||||
if (matches.length === 0)
|
||||
throw new IllegalArgumentError(`Glob pattern '${token}' has no matches.`);
|
||||
newTokens = newTokens.concat(matches);
|
||||
});
|
||||
return newTokens;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@ import * as Cookies from "js-cookie";
|
|||
import {Commands} from "./Commands";
|
||||
import {Environment} from "./Environment";
|
||||
import {Directory, File, FileSystem, Node, Path} from "./FileSystem";
|
||||
import {asciiHeaderHtml, IllegalArgumentError, IllegalStateError, stripHtmlTags} from "./Shared";
|
||||
import {InputParser} from "./InputParser";
|
||||
import {asciiHeaderHtml, IllegalStateError, stripHtmlTags} from "./Shared";
|
||||
import {EscapeCharacters, InputHistory} from "./Terminal";
|
||||
import {UserList} from "./UserList";
|
||||
|
||||
|
@ -124,8 +125,13 @@ export class Shell {
|
|||
|
||||
this.inputHistory.addEntry(inputString.trim());
|
||||
|
||||
const parser = new InputParser(this.environment);
|
||||
const input = parser.parse(stripHtmlTags(inputString));
|
||||
const parser = InputParser.create(this.environment, this.fileSystem);
|
||||
let input;
|
||||
try {
|
||||
input = parser.parse(stripHtmlTags(inputString));
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
if (input.redirectTarget[0] === "write") {
|
||||
try {
|
||||
const path = Path.interpret(this.environment.get("cwd"), input.redirectTarget[1]);
|
||||
|
@ -351,224 +357,3 @@ export class InputArgs {
|
|||
return this._args[index] !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser for input strings.
|
||||
*/
|
||||
export class InputParser {
|
||||
/**
|
||||
* The environment containing the variables to substitute.
|
||||
*/
|
||||
private readonly environment: Environment;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new input parser.
|
||||
*
|
||||
* @param environment the environment containing the variables to substitute
|
||||
*/
|
||||
constructor(environment: Environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given input string to a set of command-line arguments.
|
||||
*
|
||||
* @param input the string to parse
|
||||
*/
|
||||
parse(input: string): InputArgs {
|
||||
const tokens = this.tokenize(input);
|
||||
const command = tokens[0] ?? "";
|
||||
const [options, args] = this.parseOpts(tokens.slice(1).filter(it => !it.startsWith(EscapeCharacters.Escape)));
|
||||
const redirectTarget = this.getRedirectTarget(tokens.slice(1));
|
||||
|
||||
return new InputArgs(command, options, args, redirectTarget);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tokenizes the input string.
|
||||
*
|
||||
* @param input the string to tokenize
|
||||
*/
|
||||
private tokenize(input: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
|
||||
let token = "";
|
||||
let isInSingleQuotes = false;
|
||||
let isInDoubleQuotes = false;
|
||||
let isInCurlyBraces = 0;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const char = input[i];
|
||||
switch (char) {
|
||||
case "\\":
|
||||
if (i === input.length - 1)
|
||||
throw new IllegalArgumentError(
|
||||
"Unexpected end of input. '\\' was used but there was nothing to escape.");
|
||||
|
||||
const nextChar = input[i + 1];
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
token += "\\" + nextChar;
|
||||
else if (nextChar === "n")
|
||||
token += "\n";
|
||||
else
|
||||
token += nextChar;
|
||||
i++;
|
||||
break;
|
||||
case "'":
|
||||
if (isInDoubleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInSingleQuotes = !isInSingleQuotes;
|
||||
break;
|
||||
case "\"":
|
||||
if (isInSingleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInDoubleQuotes = !isInDoubleQuotes;
|
||||
break;
|
||||
case "{":
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInCurlyBraces++;
|
||||
break;
|
||||
case "}":
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
token += char;
|
||||
else
|
||||
isInCurlyBraces--;
|
||||
|
||||
if (isInCurlyBraces < 0)
|
||||
throw new IllegalArgumentError("Unexpected closing '}' without corresponding '{'.");
|
||||
break;
|
||||
case " ":
|
||||
if (isInSingleQuotes || isInDoubleQuotes) {
|
||||
token += char;
|
||||
} else if (token !== "") {
|
||||
tokens.push(token);
|
||||
token = "";
|
||||
}
|
||||
break;
|
||||
case "$":
|
||||
if (isInSingleQuotes || isInDoubleQuotes) {
|
||||
token += char;
|
||||
break;
|
||||
}
|
||||
|
||||
let key = "";
|
||||
for (; i + 1 < input.length; i++) {
|
||||
const nextChar = input[i + 1];
|
||||
if (nextChar.match(/^[0-9a-z_]+$/i))
|
||||
key += nextChar;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (key === "")
|
||||
throw new IllegalArgumentError(`Missing variable name after '$'.`);
|
||||
|
||||
token += this.environment.getOrDefault(key, "");
|
||||
break;
|
||||
case ">":
|
||||
if (isInSingleQuotes || isInDoubleQuotes) {
|
||||
token += char;
|
||||
break;
|
||||
}
|
||||
|
||||
if (token !== "") {
|
||||
tokens.push(token);
|
||||
token = "";
|
||||
}
|
||||
|
||||
token += EscapeCharacters.Escape + ">";
|
||||
if (input[i + 1] === ">") {
|
||||
token += `>`;
|
||||
i++;
|
||||
}
|
||||
while (input[i + 1] === " ")
|
||||
i++;
|
||||
|
||||
break;
|
||||
default:
|
||||
token += char;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (token !== "")
|
||||
tokens.push(token);
|
||||
|
||||
if (isInSingleQuotes || isInDoubleQuotes)
|
||||
throw new IllegalArgumentError("Unexpected end of input. Missing closing quotation mark.");
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the redirect target described by the last token that describes a redirect target, or the default redirect
|
||||
* target if no token describes a redirect target.
|
||||
*
|
||||
* @param tokens an array of tokens of which some tokens may describe a redirect target
|
||||
*/
|
||||
private getRedirectTarget(tokens: string[]): InputArgs.RedirectTarget {
|
||||
let redirectTarget: InputArgs.RedirectTarget = ["default"];
|
||||
|
||||
tokens.forEach(token => {
|
||||
if (token.startsWith(`${EscapeCharacters.Escape}>>`))
|
||||
redirectTarget = ["append", token.slice(3)];
|
||||
else if (token.startsWith(`${EscapeCharacters.Escape}>`))
|
||||
redirectTarget = ["write", token.slice(2)];
|
||||
});
|
||||
|
||||
return redirectTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses options and arguments.
|
||||
*
|
||||
* @param tokens the tokens that form the options and arguments
|
||||
*/
|
||||
private parseOpts(tokens: string[]): [InputArgs.Options, string[]] {
|
||||
const options: { [key: string]: string | null } = {};
|
||||
|
||||
let i;
|
||||
for (i = 0; i < tokens.length; i++) {
|
||||
const arg = tokens[i];
|
||||
|
||||
if (!arg.startsWith("-") || arg === "--")
|
||||
break;
|
||||
|
||||
const argsParts = arg.split(/=(.*)/, 2);
|
||||
if (argsParts.length === 0 || argsParts.length > 2)
|
||||
throw new IllegalArgumentError("Unexpected number of parts.");
|
||||
if (argsParts[0].indexOf(' ') >= 0)
|
||||
break;
|
||||
|
||||
const value = argsParts.length === 1 ? null : argsParts[1];
|
||||
|
||||
if (argsParts[0].startsWith("--")) {
|
||||
const key = argsParts[0].substr(2);
|
||||
if (key === "")
|
||||
break;
|
||||
|
||||
options[key] = value;
|
||||
} else {
|
||||
const keys = argsParts[0].substr(1);
|
||||
if (keys === "")
|
||||
break;
|
||||
|
||||
if (keys.length === 1) {
|
||||
options[keys] = value;
|
||||
} else {
|
||||
if (value !== null)
|
||||
throw new IllegalArgumentError("Cannot assign value to multiple short options.");
|
||||
|
||||
for (const key of keys)
|
||||
options[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [options, tokens.slice(i)];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,7 +219,7 @@ export class Terminal {
|
|||
this.isInputHidden = false;
|
||||
break;
|
||||
default:
|
||||
buffer += output.charAt(i) + output.charAt(i + 1);
|
||||
buffer += output.charAt(i + 1);
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
|
|
|
@ -10,8 +10,8 @@ describe("environment", () => {
|
|||
|
||||
|
||||
beforeEach(() => {
|
||||
environment = new Environment();
|
||||
roEnvironment = new Environment(["readonly"]);
|
||||
environment = new Environment([], {cwd: "/"});
|
||||
roEnvironment = new Environment(["readonly"], {cwd: "/"});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -3,108 +3,35 @@ import {expect} from "chai";
|
|||
|
||||
import "../main/js/Extensions"
|
||||
import {Environment} from "../main/js/Environment";
|
||||
import {InputParser} from "../main/js/Shell";
|
||||
import {Globber, InputParser, Tokenizer} from "../main/js/InputParser";
|
||||
import {Directory, File, FileSystem} from "../main/js/FileSystem";
|
||||
import {EscapeCharacters} from "../main/js/Terminal";
|
||||
|
||||
|
||||
describe("input args", () => {
|
||||
describe("input parser", () => {
|
||||
let parser: InputParser;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new InputParser(new Environment());
|
||||
const dummyGlobber = new class extends Globber {
|
||||
constructor() {
|
||||
super(new FileSystem(), "");
|
||||
}
|
||||
|
||||
|
||||
glob(tokens: string[]): string[] {
|
||||
return tokens;
|
||||
}
|
||||
};
|
||||
|
||||
parser = new InputParser(new Tokenizer(new Environment()), dummyGlobber);
|
||||
});
|
||||
|
||||
|
||||
describe("tokenization", () => {
|
||||
it("concatenates multiple strings into one token", () => {
|
||||
expect(parser.parse(`'co'm"m nd"`).command).to.equal("comm nd");
|
||||
});
|
||||
|
||||
it("includes escaped spaces into the token", () => {
|
||||
expect(parser.parse("com\\ mand").command).to.equal("com mand");
|
||||
});
|
||||
|
||||
it("includes escaped quotation marks into the token", () => {
|
||||
expect(parser.parse(`com\\'man\\"d`).command).to.equal(`com'man"d`);
|
||||
});
|
||||
|
||||
it("does not escape ordinary characters inside strings", () => {
|
||||
expect(parser.parse(`\\p`).command).to.equal("p");
|
||||
expect(parser.parse(`'\\p'`).command).to.equal("\\p");
|
||||
expect(parser.parse(`"\\p"`).command).to.equal("\\p");
|
||||
});
|
||||
|
||||
describe("grouping", () => {
|
||||
describe("quotes",() => {
|
||||
it("groups using single quotes", () => {
|
||||
expect(parser.parse("a'b'a").command).to.equal("aba");
|
||||
});
|
||||
|
||||
it("groups using double quotes", () => {
|
||||
expect(parser.parse(`a"b"a`).command).to.equal("aba");
|
||||
});
|
||||
|
||||
it("throws an error if single quotes are not closed", () => {
|
||||
expect(() => parser.parse("a'ba")).to.throw;
|
||||
});
|
||||
|
||||
it("throws an error if double quotes are not closed", () => {
|
||||
expect(() => parser.parse(`a"ba`)).to.throw;
|
||||
});
|
||||
|
||||
it("does not group double quotes within single quotes", () => {
|
||||
expect(parser.parse(`a'b"b'a`).command).to.equal(`ab"ba`);
|
||||
});
|
||||
|
||||
it("does not group single quotes within double quotes", () => {
|
||||
expect(parser.parse(`a"b'b"a`).command).to.equal("ab'ba");
|
||||
});
|
||||
});
|
||||
|
||||
describe("curly braces",() => {
|
||||
it("groups using curly braces", () => {
|
||||
expect(parser.parse("a{b}a").command).to.equal("aba");
|
||||
});
|
||||
|
||||
it("groups using nested curly braces", ()=> {
|
||||
expect(parser.parse("a{{b}{b}}a").command).to.equal("abba");
|
||||
});
|
||||
|
||||
it("throws an error if curly braces are not closed", () => {
|
||||
expect(() => parser.parse("a{ba")).to.throw;
|
||||
});
|
||||
|
||||
it("throws an error if curly braces are not opened", () => {
|
||||
expect(() => parser.parse("a}ba")).to.throw;
|
||||
});
|
||||
|
||||
it("throws an error if nested curly braces are not closed", () => {
|
||||
expect(() => parser.parse("a{{b}a")).to.throw;
|
||||
});
|
||||
|
||||
it("does not group curly braces within single quotes", () => {
|
||||
expect(parser.parse(`a'b{b'a`).command).to.equal("ab{ba");
|
||||
});
|
||||
|
||||
it("does not group curly braces within double quotes", () => {
|
||||
expect(parser.parse(`a"b{b"a`).command).to.equal("ab{ba");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("command", () => {
|
||||
it("returns the first token as the command", () => {
|
||||
expect(parser.parse("command arg1 arg2").command).to.equal("command");
|
||||
});
|
||||
|
||||
it("returns the first token as the command even if there are unnecessary spaces", () => {
|
||||
expect(parser.parse(" command arg1 arg2").command).to.equal("command");
|
||||
});
|
||||
|
||||
it("returns the first token as the command even if it contains special symbols", () => {
|
||||
expect(parser.parse("4com-mand3 arg1 arg2").command).to.equal("4com-mand3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("options", () => {
|
||||
|
@ -276,45 +203,252 @@ describe("input args", () => {
|
|||
expect(inputArgs.args).to.have.members([">", "file"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("tokenizer", () => {
|
||||
const escape = EscapeCharacters.Escape;
|
||||
let tokenizer: Tokenizer;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
tokenizer = new Tokenizer(new Environment());
|
||||
});
|
||||
|
||||
|
||||
describe("tokens", () => {
|
||||
describe("whitespace", () => {
|
||||
it("ignores unnecessary leading whitespace", () => {
|
||||
expect(tokenizer.tokenize(" token1 token2")).to.have.members(["token1", "token2"]);
|
||||
});
|
||||
|
||||
it("ignores unnecessary trailing whitespace", () => {
|
||||
expect(tokenizer.tokenize("token1 token2 ")).to.have.members(["token1", "token2"]);
|
||||
});
|
||||
|
||||
it("ignores unnecessary whitespace in between tokens", () => {
|
||||
expect(tokenizer.tokenize("token1 token2")).to.have.members(["token1", "token2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("escape characters", () => {
|
||||
it("includes escaped spaces into the token", () => {
|
||||
expect(tokenizer.tokenize("com\\ mand")).to.have.members(["com mand"]);
|
||||
});
|
||||
|
||||
it("includes escaped quotation marks in the token", () => {
|
||||
expect(tokenizer.tokenize(`com\\'man\\"d`)).to.have.members([`com'man"d`]);
|
||||
});
|
||||
|
||||
it("does not escape ordinary characters inside strings", () => {
|
||||
expect(tokenizer.tokenize(`\\p`)).to.have.members(["p"]);
|
||||
expect(tokenizer.tokenize(`'\\p'`)).to.have.members(["\\p"]);
|
||||
expect(tokenizer.tokenize(`"\\p"`)).to.have.members(["\\p"]);
|
||||
});
|
||||
|
||||
it("includes escaped spaces at the very end", () => {
|
||||
expect(tokenizer.tokenize("a b\\ ")).to.have.members(["a", "b "]);
|
||||
});
|
||||
|
||||
it("throws an error if an escape occurs but no character follows", () => {
|
||||
expect(() => tokenizer.tokenize("\\")).to.throw;
|
||||
});
|
||||
});
|
||||
|
||||
describe("grouping", () => {
|
||||
describe("quotes", () => {
|
||||
it("groups using single quotes", () => {
|
||||
expect(tokenizer.tokenize("a'b'a")).to.have.members(["aba"]);
|
||||
});
|
||||
|
||||
it("groups using double quotes", () => {
|
||||
expect(tokenizer.tokenize(`a"b"a`)).to.have.members(["aba"]);
|
||||
});
|
||||
|
||||
it("throws an error if single quotes are not closed", () => {
|
||||
expect(() => tokenizer.tokenize("a'ba")).to.throw;
|
||||
});
|
||||
|
||||
it("throws an error if double quotes are not closed", () => {
|
||||
expect(() => tokenizer.tokenize(`a"ba`)).to.throw;
|
||||
});
|
||||
|
||||
it("does not group double quotes within single quotes", () => {
|
||||
expect(tokenizer.tokenize(`a'b"b'a`)).to.have.members([`ab"ba`]);
|
||||
});
|
||||
|
||||
it("does not group single quotes within double quotes", () => {
|
||||
expect(tokenizer.tokenize(`a"b'b"a`)).to.have.members(["ab'ba"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("curly braces", () => {
|
||||
it("groups using curly braces", () => {
|
||||
expect(tokenizer.tokenize("a{b}a")).to.have.members(["aba"]);
|
||||
});
|
||||
|
||||
it("groups using nested curly braces", () => {
|
||||
expect(tokenizer.tokenize("a{{b}{b}}a")).to.have.members(["abba"]);
|
||||
});
|
||||
|
||||
it("throws an error if curly braces are not closed", () => {
|
||||
expect(() => tokenizer.tokenize("a{ba")).to.throw;
|
||||
});
|
||||
|
||||
it("throws an error if curly braces are not opened", () => {
|
||||
expect(() => tokenizer.tokenize("a}ba")).to.throw;
|
||||
});
|
||||
|
||||
it("throws an error if nested curly braces are not closed", () => {
|
||||
expect(() => tokenizer.tokenize("a{{b}a")).to.throw;
|
||||
});
|
||||
|
||||
it("does not group curly braces within single quotes", () => {
|
||||
expect(tokenizer.tokenize(`a'b{b'a`)).to.have.members(["ab{ba"]);
|
||||
});
|
||||
|
||||
it("does not group curly braces within double quotes", () => {
|
||||
expect(tokenizer.tokenize(`a"b{b"a`)).to.have.members(["ab{ba"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment", () => {
|
||||
beforeEach(() => {
|
||||
parser = new InputParser(new Environment([], {a: "b", aa: "c", r: ">"}));
|
||||
tokenizer = new Tokenizer(new Environment([], {a: "b", aa: "c", r: ">", cwd: "/"}));
|
||||
});
|
||||
|
||||
|
||||
it("substitutes a known environment variable with its value", () => {
|
||||
expect(parser.parse("$a").command).to.equal("b");
|
||||
expect(tokenizer.tokenize("$a")).to.have.members(["b"]);
|
||||
});
|
||||
|
||||
it("substitutes an unknown environment variable with nothing", () => {
|
||||
expect(parser.parse("$b").command).to.equal("");
|
||||
expect(tokenizer.tokenize("a$b")).to.have.members(["a"]);
|
||||
});
|
||||
|
||||
it("substitutes consecutive known environment variables with their value", () => {
|
||||
expect(parser.parse("$a$aa$a").command).to.equal("bcb");
|
||||
expect(tokenizer.tokenize("$a$aa$a")).to.have.members(["bcb"]);
|
||||
});
|
||||
|
||||
it("throws an error for nameless environment variables", () => {
|
||||
expect(() => parser.parse("$").command).to.throw;
|
||||
expect(() => tokenizer.tokenize("$")).to.throw;
|
||||
});
|
||||
|
||||
it("does not substitute environment variables in the middle of a single-quoted string", () => {
|
||||
expect(parser.parse("a'$a'c").command).to.equal("a$ac");
|
||||
expect(tokenizer.tokenize("a'$a'c")).to.have.members(["a$ac"]);
|
||||
});
|
||||
|
||||
it("does not substitute environment variables in the middle of a double-quoted string", () => {
|
||||
expect(parser.parse(`a"$a"c`).command).to.equal("a$ac");
|
||||
expect(tokenizer.tokenize(`a"$a"c`)).to.have.members(["a$ac"]);
|
||||
});
|
||||
|
||||
it("substitutes environment variables in the middle of curly braces", () => {
|
||||
expect(parser.parse("a{$a}c").command).to.equal("abc");
|
||||
expect(tokenizer.tokenize("a{$a}c")).to.have.members(["abc"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("escapes", () => {
|
||||
it("escapes output target characters", () => {
|
||||
expect(tokenizer.tokenize("a >b")).to.have.members(["a", `${escape}>b`]);
|
||||
expect(tokenizer.tokenize("a >>b")).to.have.members(["a", `${escape}>${escape}>b`]);
|
||||
});
|
||||
|
||||
it("substitutes special characters without interpreting them", () => {
|
||||
const inputArgs = parser.parse("command $r file");
|
||||
expect(inputArgs.args).to.have.members([">", "file"]);
|
||||
expect(inputArgs.redirectTarget).to.have.members(["default"]);
|
||||
it("does not escape escaped target characters", () => {
|
||||
expect(tokenizer.tokenize("a \\>b")).to.have.members(["a", ">b"]);
|
||||
expect(tokenizer.tokenize("a \\>>b")).to.have.members(["a", ">", `${escape}>b`]);
|
||||
});
|
||||
|
||||
it("escapes glob characters", () => {
|
||||
expect(tokenizer.tokenize("a b?")).to.have.members(["a", `b${escape}?`]);
|
||||
expect(tokenizer.tokenize("a b*")).to.have.members(["a", `b${escape}*`]);
|
||||
expect(tokenizer.tokenize("a b**")).to.have.members(["a", `b${escape}*${escape}*`]);
|
||||
});
|
||||
|
||||
it("does not escape escaped glob characters", () => {
|
||||
expect(tokenizer.tokenize("a b\\?")).to.have.members(["a", `b?`]);
|
||||
expect(tokenizer.tokenize("a b\\*")).to.have.members(["a", `b*`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("globber", () => {
|
||||
const escape = EscapeCharacters.Escape;
|
||||
let globber: Globber;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
globber = new Globber(
|
||||
new FileSystem(new Directory({
|
||||
"aa": new Directory({
|
||||
"ab1": new File()
|
||||
}),
|
||||
"ab1": new File(),
|
||||
"ab2": new File(),
|
||||
"aa3": new File(),
|
||||
"b?": new File(),
|
||||
".a": new File()
|
||||
})),
|
||||
"/"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
describe("?", () => {
|
||||
it("throws an error if no matches are found", () => {
|
||||
expect(() => globber.glob([`x${escape}?`])).to.throw;
|
||||
});
|
||||
|
||||
it("globs a single ?", () => {
|
||||
expect(globber.glob([`ab${escape}?`])).to.have.members(["ab1", "ab2"]);
|
||||
});
|
||||
|
||||
it("globs multiple ?s", () => {
|
||||
expect(globber.glob([`a${escape}?${escape}?`])).to.have.members(["ab1", "ab2", "aa3"]);
|
||||
});
|
||||
|
||||
it("does not process unescaped ?s", () => {
|
||||
expect(globber.glob(["a?"])).to.have.members(["a?"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("*", () => {
|
||||
it("throws an error if no matches are found", () => {
|
||||
expect(() => globber.glob([`x${escape}*`])).to.throw;
|
||||
});
|
||||
|
||||
it("globs a single *", () => {
|
||||
expect(globber.glob([`a${escape}*`])).to.have.members(["aa", "ab1", "ab2", "aa3"]);
|
||||
});
|
||||
|
||||
it("globs multiple *s", () => {
|
||||
expect(globber.glob([`a${escape}*/${escape}*`])).to.have.members(["aa/ab1"]);
|
||||
});
|
||||
|
||||
it("does not process unescaped *s", () => {
|
||||
expect(globber.glob(["a*"])).to.have.members(["a*"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("**", () => {
|
||||
it("throws an error if no matches are found", () => {
|
||||
expect(() => globber.glob([`x${escape}**`])).to.throw;
|
||||
});
|
||||
|
||||
it("globs **", () => {
|
||||
expect(globber.glob([`${escape}*${escape}*b1`])).to.have.members(["ab1", "aa/ab1"]);
|
||||
});
|
||||
|
||||
it("does not match the directory itself", () => {
|
||||
expect(globber.glob([`${escape}*${escape}*`]).map(it => it.trim())).to.not.contain("");
|
||||
});
|
||||
|
||||
it("does not process unescaped **s", () => {
|
||||
expect(globber.glob(["a**"])).to.have.members(["a**"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not use an embedded `.*` in regex matching", () => {
|
||||
expect(globber.glob([`.${escape}*`])).to.have.members([".a"]);
|
||||
})
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue