Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: better shebang support #256

Merged
merged 1 commit into from
Mar 27, 2024
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
19 changes: 19 additions & 0 deletions mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,25 @@ Deno.test("shebang support", async (t) => {
assertEquals(output, "Hello");
});

step("relative sub dir", async () => {
dir.join("echo_stdin2.ts").writeTextSync(
[
"#!/usr/bin/env -S deno run --allow-run",
"await new Deno.Command('deno', { args: ['run', ...Deno.args] }).spawn();",
].join("\n"),
);
dir.join("sub/sub.ts").writeTextSync(
[
"#!/usr/bin/env ../echo_stdin2.ts",
"console.log('Hello')",
].join("\n"),
);
const output = await $`./sub/sub.ts`
.cwd(dir)
.text();
assertEquals(output, "Hello");
});

await Promise.all(steps);
});
});
Expand Down
74 changes: 53 additions & 21 deletions src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,19 +906,40 @@ function checkMapCwdNotExistsError(cwd: string, err: unknown) {
}
}

async function executeCommandArgs(commandArgs: string[], context: Context): Promise<ExecuteResult> {
function executeCommandArgs(commandArgs: string[], context: Context): Promise<ExecuteResult> {
// look for a registered command first
const command = context.getCommand(commandArgs[0]);
const commandName = commandArgs.shift()!;
const command = context.getCommand(commandName);
if (command != null) {
return command(context.asCommandContext(commandArgs.slice(1)));
return Promise.resolve(command(context.asCommandContext(commandArgs)));
}

// fall back to trying to resolve the command on the fs
const resolvedCommand = await resolveCommand(commandArgs[0], context);
const unresolvedCommand: UnresolvedCommand = {
name: commandName,
baseDir: context.getCwd(),
};
return executeUnresolvedCommand(unresolvedCommand, commandArgs, context);
}

async function executeUnresolvedCommand(
unresolvedCommand: UnresolvedCommand,
commandArgs: string[],
context: Context,
): Promise<ExecuteResult> {
const resolvedCommand = await resolveCommand(unresolvedCommand, context);
if (resolvedCommand.kind === "shebang") {
return executeCommandArgs([...resolvedCommand.args, resolvedCommand.path, ...commandArgs.slice(1)], context);
return executeUnresolvedCommand(resolvedCommand.command, [...resolvedCommand.args, ...commandArgs], context);
}
const _assertIsPath: "path" = resolvedCommand.kind;
return executeCommandAtPath(resolvedCommand.path, commandArgs, context);
}

async function executeCommandAtPath(
commandPath: string,
commandArgs: string[],
context: Context,
): Promise<ExecuteResult> {
const pipeStringVals = {
stdin: getStdioStringValue(context.stdin),
stdout: getStdioStringValue(context.stdout.kind),
Expand All @@ -927,8 +948,8 @@ async function executeCommandArgs(commandArgs: string[], context: Context): Prom
let p: SpawnedChildProcess;
const cwd = context.getCwd();
try {
p = spawnCommand(resolvedCommand.path, {
args: commandArgs.slice(1),
p = spawnCommand(commandPath, {
args: commandArgs,
cwd,
env: context.getEnvVars(),
clearEnv: true,
Expand Down Expand Up @@ -1101,54 +1122,65 @@ interface ResolvedPathCommand {

interface ResolvedShebangCommand {
kind: "shebang";
path: string;
command: UnresolvedCommand;
args: string[];
}

async function resolveCommand(commandName: string, context: Context): Promise<ResolvedCommand> {
if (commandName.includes("/") || commandName.includes("\\")) {
if (!path.isAbsolute(commandName)) {
commandName = path.resolve(context.getCwd(), commandName);
}
interface UnresolvedCommand {
name: string;
baseDir: string;
}

async function resolveCommand(unresolvedCommand: UnresolvedCommand, context: Context): Promise<ResolvedCommand> {
if (unresolvedCommand.name.includes("/")) {
const commandPath = path.isAbsolute(unresolvedCommand.name)
? unresolvedCommand.name
: path.resolve(unresolvedCommand.baseDir, unresolvedCommand.name);
// only bother checking for a shebang when the path has a slash
// in it because for global commands someone on Windows likely
// won't have a script with a shebang in it on Windows
const result = await getExecutableShebangFromPath(commandName);
const result = await getExecutableShebangFromPath(commandPath);
if (result === false) {
throw new Error(`Command not found: ${commandName}`);
throw new Error(`Command not found: ${unresolvedCommand.name}`);
} else if (result != null) {
const args = await parseShebangArgs(result, context);
const name = args.shift()!;
args.push(commandPath);
return {
kind: "shebang",
path: commandName,
args: await parseShebangArgs(result, context),
command: {
name,
baseDir: path.dirname(commandPath),
},
args,
};
} else {
const _assertUndefined: undefined = result;
return {
kind: "path",
path: commandName,
path: commandPath,
};
}
}

// always use the current executable for "deno"
if (commandName.toUpperCase() === "DENO") {
if (unresolvedCommand.name.toUpperCase() === "DENO") {
return {
kind: "path",
path: Deno.execPath(),
};
}

const realEnvironment = new DenoWhichRealEnvironment();
const commandPath = await which(commandName, {
const commandPath = await which(unresolvedCommand.name, {
os: Deno.build.os,
stat: realEnvironment.stat,
env(key) {
return context.getVar(key);
},
});
if (commandPath == null) {
throw new Error(`Command not found: ${commandName}`);
throw new Error(`Command not found: ${unresolvedCommand.name}`);
}
return {
kind: "path",
Expand Down