Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/executable/DprintExecutable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ export class DprintExecutable {
static async resolveCommand(options: DprintExecutableOptions): Promise<DprintCommand> {
const command = await getCommand(options);
return process.platform === "win32"
? await resolveWindowsCommand(command, commandPath => which(commandPath, whichEnvironment))
? await resolveWindowsCommand(
command,
commandPath => which(commandPath, whichEnvironment),
process.env.PATHEXT,
)
: command;
}

Expand Down
53 changes: 51 additions & 2 deletions src/executable/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ describe("getCommandLaunchInfo", () => {
getCommandLaunchInfo({ kind: "setting", path: "./bin/dprint.bat", cwd: "C:\\project" }, ["-v"], "win32").command,
"\"C:\\project\\bin\\dprint.bat\" \"-v\"",
);
assert.strictEqual(
getCommandLaunchInfo({ kind: "setting", path: ".vscode/dprint.BAT", cwd: "C:\\project" }, ["-v"], "win32")
.command,
"\"C:\\project\\.vscode\\dprint.BAT\" \"-v\"",
);
});
});

Expand All @@ -89,7 +94,7 @@ describe("resolveWindowsCommand", () => {
const searched: string[] = [];
const which = (command: string) => {
searched.push(command);
return Promise.resolve(command === "dprint" ? "C:\\npm prefix\\dprint.cmd" : `${command}.exe`);
return Promise.resolve(command === "dprint" ? "C:\\npm prefix\\dprint.cmd" : command.replace(/\.EXE$/, ".exe"));
};
assert.deepStrictEqual(
await resolveWindowsCommand({ kind: "path", path: "dprint" }, which),
Expand All @@ -99,7 +104,51 @@ describe("resolveWindowsCommand", () => {
await resolveWindowsCommand({ kind: "setting", path: "./bin/dprint", cwd: "C:\\project" }, which),
{ kind: "setting", path: "C:\\project\\bin\\dprint.exe", cwd: "C:\\project" },
);
assert.deepStrictEqual(searched, ["dprint", "C:\\project\\bin\\dprint"]);
assert.deepStrictEqual(searched, ["dprint", "C:\\project\\bin\\dprint.EXE"]);
});

it("prefers a file with an executable extension over the file at the path", async () => {
// ex. a `dprint` shell script for other platforms beside a `dprint.bat`
const files = ["C:\\project\\.vscode\\dprint", "C:\\project\\.vscode\\dprint.bat"];
const which = (command: string) =>
Promise.resolve(files.find(file => file.toLowerCase() === command.toLowerCase()));
for (const settingPath of [".vscode/dprint", ".vscode\\dprint", "./.vscode/dprint"]) {
assert.deepStrictEqual(
await resolveWindowsCommand({ kind: "setting", path: settingPath, cwd: "C:\\project" }, which, ".EXE;.BAT"),
{ kind: "setting", path: "C:\\project\\.vscode\\dprint.bat", cwd: "C:\\project" },
);
}
});

it("searches the launchable extensions in order and only once for a path with an extension", async () => {
const searched: string[] = [];
const which = (command: string) => {
searched.push(command);
return Promise.resolve(undefined);
};
const pathExt = ".COM;.EXE;.BAT;.CMD;.VBS;.JS";
await resolveWindowsCommand({ kind: "path", path: "C:/bin/dprint" }, which, pathExt);
await resolveWindowsCommand({ kind: "setting", path: "bin/dprint.Cmd", cwd: "C:\\project" }, which, pathExt);
await resolveWindowsCommand({ kind: "setting", path: "\\\\srv\\share\\dprint.exe", cwd: "C:\\project" }, which);
await resolveWindowsCommand({ kind: "setting", path: "C:bin\\dprint.exe", cwd: "C:\\project" }, which);
assert.deepStrictEqual(searched, [
"C:/bin/dprint.COM",
"C:/bin/dprint.EXE",
"C:/bin/dprint.BAT",
"C:/bin/dprint.CMD",
"C:/bin/dprint",
"C:\\project\\bin\\dprint.Cmd",
"\\\\srv\\share\\dprint.exe",
"C:\\project\\bin\\dprint.exe",
]);
});

it("uses the file at the path when there's none with an executable extension", async () => {
const which = (command: string) => Promise.resolve(command === "C:\\bin\\dprint" ? command : undefined);
assert.deepStrictEqual(
await resolveWindowsCommand({ kind: "setting", path: "C:\\bin\\dprint", cwd: "C:\\project" }, which),
{ kind: "setting", path: "C:\\bin\\dprint", cwd: "C:\\project" },
);
});

it("keeps the command when the file isn't found", async () => {
Expand Down
39 changes: 35 additions & 4 deletions src/executable/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,24 @@ export function expandWindowsEnvVars(text: string, env: { [name: string]: string
* Resolves the executable file of a command on Windows (ex. `dprint` to
* `C:\bin\dprint.exe`) so that it can be launched without cmd.exe when possible.
* The command is kept as-is when the file isn't found.
*
* Like cmd.exe, a file with an executable extension is preferred over the file at
* a path without one (ex. `bin/dprint.bat` over a `bin/dprint` shell script that's
* there for other platforms).
*/
export async function resolveWindowsCommand(
command: DprintCommand,
which: (command: string) => Promise<string | undefined>,
pathExt?: string,
): Promise<DprintCommand> {
const filePath = await which(getWindowsCommandPath(command));
return filePath == null ? command : { ...command, path: filePath };
const commandPath = getWindowsCommandPath(command);
for (const candidate of [...getWindowsExecutableCandidates(commandPath, pathExt), commandPath]) {
const filePath = await which(candidate);
if (filePath != null) {
return { ...command, path: filePath };
}
}
return command;
}

/** Gets the text to display for the command. */
Expand Down Expand Up @@ -102,11 +113,31 @@ export function getCommandLaunchInfo(
}

function getWindowsCommandPath(command: DprintCommand) {
return command.kind === "setting" && command.cwd != null && isRelativePath(command.path)
? path.win32.join(command.cwd, command.path)
return command.kind === "setting" && command.cwd != null && isWindowsRelativePath(command.path)
// resolve instead of join for a drive-relative path (ex. `C:bin\dprint`)
? path.win32.resolve(command.cwd, command.path)
: command.path;
}

/** Gets the path with each executable extension when it's a file path without one. */
function getWindowsExecutableCandidates(commandPath: string, pathExt: string | undefined) {
if (!/[\\/]/.test(commandPath)) {
return []; // a command name, which is searched for on the path with the extensions
}
const extensions = (pathExt ?? ".EXE;.CMD;.BAT;.COM").split(";").map(ext => ext.trim()).filter(ext => ext.length > 0);
const lowerCasePath = commandPath.toLowerCase();
if (extensions.some(ext => lowerCasePath.endsWith(ext.toLowerCase()))) {
return [];
}
// only the extensions that can be launched (ex. not a `dprint.js` beside the file)
return extensions.filter(ext => /^\.(exe|com|cmd|bat)$/i.test(ext)).map(ext => commandPath + ext);
}

/** If it's a file path relative to the folder (ex. `bin/dprint`) rather than a command name. */
function isWindowsRelativePath(commandPath: string) {
return /[\\/]/.test(commandPath) && !path.win32.isAbsolute(commandPath);
}

function getPosixSettingCommand(command: DprintCommand & { kind: "setting" }) {
// double quotes so the shell expands the setting (ex. `$HOME`)
const settingText = `"${command.path.replace(/"/g, "\\\"")}"`;
Expand Down
Loading