forked from tools/josh
1
0
Fork 0
This is at the cost of removing support for `**` because that's just really difficult to implement.
This commit is contained in:
Florine W. Dekker 2019-11-08 21:19:38 +01:00
parent 5a028f9611
commit f5713fdf31
Signed by: FWDekker
GPG Key ID: B1B567AF58D6EE0F
3 changed files with 233 additions and 84 deletions

View File

@ -1,4 +1,4 @@
import {emptyFunction, getFileExtension, IllegalArgumentError, IllegalStateError} from "./Shared";
import {emptyFunction, getFileExtension, IllegalArgumentError} from "./Shared";
/**

View File

@ -1,6 +1,6 @@
import {Environment} from "./Environment";
import {Directory, FileSystem, Path} from "./FileSystem";
import {IllegalArgumentError, IllegalStateError} from "./Shared";
import {Directory, File, FileSystem, Path} from "./FileSystem";
import {IllegalArgumentError} from "./Shared";
import {InputArgs} from "./Shell";
import {EscapeCharacters} from "./Terminal";
@ -312,34 +312,65 @@ export class Globber {
* @param tokens the tokens to glob
*/
glob(tokens: string[]): string[] {
const cwdNode = this.fileSystem.get(this.cwd);
if (cwdNode === undefined)
return tokens;
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[] = [];
let results: 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);
if (token.startsWith("/"))
results = results.concat(this.glob2("/", token.slice(1), new Path("/")));
else
results = results.concat(this.glob2("", token, this.cwd));
});
return newTokens;
return results;
}
/**
* Recursively traverses the given path according to the glob pattern provided, keeping track of file system
* location with the given path, and returns all paths that match the glob pattern.
*
* @param history the "de-globbed" pattern until now; must end with a slash in between recursive calls
* @param glob the glob pattern that is still to be traversed
* @param path the current location in the file system
*/
private glob2(history: string, glob: string, path: Path): string[] {
if (!glob.includes(EscapeCharacters.Escape + "?") && !glob.includes(EscapeCharacters.Escape + "*"))
return [history + glob];
const dir = this.fileSystem.get(path);
if (!(dir instanceof Directory))
return [history + glob];
const nextPart = glob.includes("/") ? glob.substring(0, glob.indexOf("/")) : glob; // excluding /
const remainder = glob.includes("/") ? glob.substring(glob.indexOf("/") + 1) : ""; // excluding /
if (nextPart === ".")
return this.glob2(history + nextPart + "/", remainder, path);
if (nextPart === "..")
return this.glob2(history + nextPart + "/", remainder, path.parent);
const pattern = nextPart
.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&") // Escape regex from user input
.replaceAll(new RegExp(`${EscapeCharacters.Escape}\\\\\\?`), ".")
.replaceAll(new RegExp(`${EscapeCharacters.Escape}\\\\\\*`), "[^/]*");
return Object.keys(dir.nodes)
.filter(fileName => fileName.match(new RegExp(`^${pattern}$`)))
.map(fileName => {
if (dir.nodes[fileName] instanceof File) {
// Only match files if there are no more /s to match
if (!glob.includes("/"))
return [history + fileName];
return <string[]>[];
}
// Only recurse if there is still recurring to do
if (remainder !== "")
return this.glob2(`${history}${fileName}/`, remainder, path.getChild(fileName));
// Add / depending on user input
if (glob.includes("/"))
return [history + fileName + "/"];
else
return [history + fileName];
})
.reduce((acc, it) => acc.concat(it));
}
}

View File

@ -4,7 +4,7 @@ import {expect} from "chai";
import "../main/js/Extensions"
import {Environment} from "../main/js/Environment";
import {Globber, InputParser, Tokenizer} from "../main/js/InputParser";
import {Directory, File, FileSystem} from "../main/js/FileSystem";
import {Directory, File, FileSystem, Node, Path} from "../main/js/FileSystem";
import {EscapeCharacters} from "../main/js/Terminal";
@ -393,7 +393,6 @@ describe("tokenizer", () => {
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", () => {
@ -405,81 +404,200 @@ describe("tokenizer", () => {
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()
})),
"/"
);
});
const createGlobber = function(nodes: { [path: string]: Node } = {}, cwd: string = "/"): Globber {
const fs = new FileSystem(new Directory());
for (const path of Object.getOwnPropertyNames(nodes))
fs.add(new Path(path), nodes[path], true);
return new Globber(fs, cwd);
};
describe("?", () => {
it("throws an error if no matches are found", () => {
expect(() => globber.glob([`x${escape}?`])).to.throw;
});
it("does not expand unescaped ?s", () => {
const globber = createGlobber({"/ab": new File()});
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?"]);
});
it("expands a single instance", () => {
const globber = createGlobber({"/a1": new File(), "/a2": new File()});
expect(globber.glob([`a${escape}?`])).to.have.members(["a1", "a2"]);
});
it("expand multiple consecutive instances", () => {
const globber = createGlobber({"/a11": new File(), "/a12": new File(), "/a21": new File()});
expect(globber.glob([`a${escape}?${escape}?`])).to.have.members(["a11", "a12", "a21"]);
});
it("expand multiple non-consecutive instances", () => {
const globber = createGlobber({"/1a1": new File(), "/1a2": new File(), "/2a1": new File()});
expect(globber.glob([`${escape}?a${escape}?`])).to.have.members(["1a1", "1a2", "2a1"]);
});
it("does not expand to an empty character", () => {
const globber = createGlobber({"/a": new File(), "/aa": new File()});
expect(globber.glob([`a${escape}?`])).to.have.members(["aa"]);
});
it("does not expand to multiple characters", () => {
const globber = createGlobber({"/aa": new File(), "/aaa": new File()});
expect(globber.glob([`a${escape}?`])).to.have.members(["aa"]);
});
it("includes directories when not using a trailing slash", () => {
const globber = createGlobber({"/a1": new File(), "/a2": new Directory()});
expect(globber.glob([`a${escape}?`])).to.have.members(["a1", "a2"]);
});
it("excludes files when using a trailing slash", () => {
const globber = createGlobber({"/a1": new File(), "/a2": new Directory()});
expect(globber.glob([`a${escape}?/`])).to.have.members(["a2/"]);
});
it("expands in a subdirectory", () => {
const globber = createGlobber({"/a1": new File(), "/dir/a1": new File(), "/dir/a2": new File()});
expect(globber.glob([`/dir/a${escape}?`])).to.have.members(["/dir/a1", "/dir/a2"]);
});
it("expands in the parent directory", () => {
const globber = createGlobber({"/dir/a1": new File(), "/a2": new File(), "/a3": new File()}, "/dir");
expect(globber.glob([`../a${escape}?`])).to.have.members(["../a2", "../a3"]);
});
it("expands in the reflexive directory", () => {
const globber = createGlobber({"/dir/a1": new File(), "/a2": new File(), "/a3": new File()}, "/dir");
expect(globber.glob([`./a${escape}?`])).to.have.members(["./a1"]);
});
it("expands in an absolute path to the root", () => {
const globber = createGlobber({"/dir/a1": new File(), "/a2": new File(), "/a3": new File()}, "/dir");
expect(globber.glob([`/a${escape}?`])).to.have.members(["/a2", "/a3"]);
});
it("expands in an absolute path to a sibling", () => {
const globber = createGlobber({"/d1/a1": new File(), "/d2/a2": new File(), "/d2/a3": new File()}, "/d1");
expect(globber.glob([`/d2/a${escape}?`])).to.have.members(["/d2/a2", "/d2/a3"]);
});
});
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", () => {
const globber = createGlobber({"/ab": new File()});
expect(globber.glob(["a*"])).to.have.members(["a*"]);
});
it("expands a single instance", () => {
const globber = createGlobber({"/a1": new File(), "/a2": new File()});
expect(globber.glob([`a${escape}*`])).to.have.members(["a1", "a2"]);
});
it("expands multiple non-consecutive instances", () => {
const globber = createGlobber({"/1a1": new File(), "/2a2": new File()});
expect(globber.glob([`${escape}*a${escape}*`])).to.have.members(["1a1", "2a2"]);
});
it("expands to match all files in a directory", () => {
const globber = createGlobber({"/a": new File(), "/b": new File()});
expect(globber.glob([`${escape}*`])).to.have.members(["a", "b"]);
});
it("expands to an empty character", () => {
const globber = createGlobber({"/a": new File(), "/aa": new File()});
expect(globber.glob([`a${escape}*`])).to.have.members(["a", "aa"]);
});
it("expands to multiple characters", () => {
const globber = createGlobber({"/aa": new File(), "/aaa": new File()});
expect(globber.glob([`a${escape}*`])).to.have.members(["aa", "aaa"]);
});
it("does not expand to a slash", () => {
const globber = createGlobber({"/a1/file": new File(), "/a2": new File()});
expect(globber.glob([`a${escape}*`])).to.have.members(["a1", "a2"]);
});
it("includes directories when not using a trailing slash", () => {
const globber = createGlobber({"/a1": new File(), "/a2": new Directory()});
expect(globber.glob([`a${escape}*`])).to.have.members(["a1", "a2"]);
});
it("excludes files when using a trailing slash", () => {
const globber = createGlobber({"/a1": new File(), "/a2": new Directory()});
expect(globber.glob([`a${escape}*/`])).to.have.members(["a2/"]);
});
it("expands in a subdirectory", () => {
const globber = createGlobber({"/a1": new File(), "/dir/a1": new File(), "/dir/a2": new File()});
expect(globber.glob([`/dir/a${escape}*`])).to.have.members(["/dir/a1", "/dir/a2"]);
});
it("expands in the parent directory", () => {
const globber = createGlobber({"/dir/a1": new File(), "/a2": new File(), "/a3": new File()}, "/dir");
expect(globber.glob([`../a${escape}*`])).to.have.members(["../a2", "../a3"]);
});
it("expands in the reflexive directory", () => {
const globber = createGlobber({"/dir/a1": new File(), "/a2": new File(), "/a3": new File()}, "/dir");
expect(globber.glob([`./a${escape}*`])).to.have.members(["./a1"]);
});
it("expands in an absolute path to the root", () => {
const globber = createGlobber({"/dir/a1": new File(), "/a2": new File(), "/a3": new File()}, "/dir");
expect(globber.glob([`/a${escape}*`])).to.have.members(["/a2", "/a3"]);
});
it("expands in an absolute path to a sibling", () => {
const globber = createGlobber({"/d1/a1": new File(), "/d2/a2": new File(), "/d2/a3": new File()}, "/d1");
expect(globber.glob([`/d2/a${escape}*`])).to.have.members(["/d2/a2", "/d2/a3"]);
});
});
describe("**", () => {
describe("shared edge cases", () => {
it("throws an error if no matches are found", () => {
expect(() => globber.glob([`x${escape}**`])).to.throw;
expect(() => createGlobber().glob([`x${escape}?`])).to.throw;
});
it("globs **", () => {
expect(globber.glob([`${escape}*${escape}*b1`])).to.have.members(["ab1", "aa/ab1"]);
it("returns an empty token without change", () => {
expect(createGlobber().glob([""])).to.have.members([""]);
});
it("does not match the directory itself", () => {
expect(globber.glob([`${escape}*${escape}*`]).map(it => it.trim())).to.not.contain("");
it("returns a glob-less token without change", () => {
expect(createGlobber().glob(["abc"])).to.have.members(["abc"]);
});
it("does not process unescaped **s", () => {
expect(globber.glob(["a**"])).to.have.members(["a**"]);
it("returns any token without change if the cwd does not exist", () => {
const globber = createGlobber({"/a1": new File()}, "/dir");
expect(globber.glob([`a${EscapeCharacters.Escape}?`])).to.have.members([`a${EscapeCharacters.Escape}?`]);
});
});
it("does not use an embedded `.*` in regex matching", () => {
expect(globber.glob([`.${escape}*`])).to.have.members([".a"]);
})
});