forked from tools/josh
Fix #46
This is at the cost of removing support for `**` because that's just really difficult to implement.
This commit is contained in:
parent
5a028f9611
commit
f5713fdf31
|
@ -1,4 +1,4 @@
|
||||||
import {emptyFunction, getFileExtension, IllegalArgumentError, IllegalStateError} from "./Shared";
|
import {emptyFunction, getFileExtension, IllegalArgumentError} from "./Shared";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {Environment} from "./Environment";
|
import {Environment} from "./Environment";
|
||||||
import {Directory, FileSystem, Path} from "./FileSystem";
|
import {Directory, File, FileSystem, Path} from "./FileSystem";
|
||||||
import {IllegalArgumentError, IllegalStateError} from "./Shared";
|
import {IllegalArgumentError} from "./Shared";
|
||||||
import {InputArgs} from "./Shell";
|
import {InputArgs} from "./Shell";
|
||||||
import {EscapeCharacters} from "./Terminal";
|
import {EscapeCharacters} from "./Terminal";
|
||||||
|
|
||||||
|
@ -312,34 +312,65 @@ export class Globber {
|
||||||
* @param tokens the tokens to glob
|
* @param tokens the tokens to glob
|
||||||
*/
|
*/
|
||||||
glob(tokens: string[]): string[] {
|
glob(tokens: string[]): string[] {
|
||||||
const cwdNode = this.fileSystem.get(this.cwd);
|
let results: string[] = [];
|
||||||
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[] = [];
|
|
||||||
tokens.forEach(token => {
|
tokens.forEach(token => {
|
||||||
if (token.indexOf(EscapeCharacters.Escape + "?") < 0 && token.indexOf(EscapeCharacters.Escape + "*") < 0) {
|
if (token.startsWith("/"))
|
||||||
newTokens = newTokens.concat([token]);
|
results = results.concat(this.glob2("/", token.slice(1), new Path("/")));
|
||||||
return;
|
else
|
||||||
}
|
results = results.concat(this.glob2("", token, this.cwd));
|
||||||
|
|
||||||
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;
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {expect} from "chai";
|
||||||
import "../main/js/Extensions"
|
import "../main/js/Extensions"
|
||||||
import {Environment} from "../main/js/Environment";
|
import {Environment} from "../main/js/Environment";
|
||||||
import {Globber, InputParser, Tokenizer} from "../main/js/InputParser";
|
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";
|
import {EscapeCharacters} from "../main/js/Terminal";
|
||||||
|
|
||||||
|
|
||||||
|
@ -393,7 +393,6 @@ describe("tokenizer", () => {
|
||||||
it("escapes glob characters", () => {
|
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}*`]);
|
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", () => {
|
it("does not escape escaped glob characters", () => {
|
||||||
|
@ -405,81 +404,200 @@ describe("tokenizer", () => {
|
||||||
|
|
||||||
describe("globber", () => {
|
describe("globber", () => {
|
||||||
const escape = EscapeCharacters.Escape;
|
const escape = EscapeCharacters.Escape;
|
||||||
let globber: Globber;
|
|
||||||
|
|
||||||
|
|
||||||
beforeEach(() => {
|
const createGlobber = function(nodes: { [path: string]: Node } = {}, cwd: string = "/"): Globber {
|
||||||
globber = new Globber(
|
const fs = new FileSystem(new Directory());
|
||||||
new FileSystem(new Directory({
|
for (const path of Object.getOwnPropertyNames(nodes))
|
||||||
"aa": new Directory({
|
fs.add(new Path(path), nodes[path], true);
|
||||||
"ab1": new File()
|
|
||||||
}),
|
return new Globber(fs, cwd);
|
||||||
"ab1": new File(),
|
};
|
||||||
"ab2": new File(),
|
|
||||||
"aa3": new File(),
|
|
||||||
"b?": new File(),
|
|
||||||
".a": new File()
|
|
||||||
})),
|
|
||||||
"/"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe("?", () => {
|
describe("?", () => {
|
||||||
it("throws an error if no matches are found", () => {
|
it("does not expand unescaped ?s", () => {
|
||||||
expect(() => globber.glob([`x${escape}?`])).to.throw;
|
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?"]);
|
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("*", () => {
|
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", () => {
|
it("does not process unescaped *s", () => {
|
||||||
|
const globber = createGlobber({"/ab": new File()});
|
||||||
|
|
||||||
expect(globber.glob(["a*"])).to.have.members(["a*"]);
|
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", () => {
|
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 **", () => {
|
it("returns an empty token without change", () => {
|
||||||
expect(globber.glob([`${escape}*${escape}*b1`])).to.have.members(["ab1", "aa/ab1"]);
|
expect(createGlobber().glob([""])).to.have.members([""]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not match the directory itself", () => {
|
it("returns a glob-less token without change", () => {
|
||||||
expect(globber.glob([`${escape}*${escape}*`]).map(it => it.trim())).to.not.contain("");
|
expect(createGlobber().glob(["abc"])).to.have.members(["abc"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not process unescaped **s", () => {
|
it("returns any token without change if the cwd does not exist", () => {
|
||||||
expect(globber.glob(["a**"])).to.have.members(["a**"]);
|
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"]);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue