@bearz/exec
Version:
The exec module makes it easy to spawn child_processes across different runtimes and different operating systems.
1,505 lines • 50.7 kB
JavaScript
/**
* The `commands` module provides a set of classes and functions for executing
* commands in a shell environment. It includes the `Command` class for
* executing commands, the `ShellCommand` class for executing shell scripts,
* and the `Pipe` class for chaining commands together. The module also
* provides utility functions for creating commands, executing
* commands, and handling output.
*
* @module
*/
// @ts-nocheck TS2455
import { CommandError, NotFoundOnPathError } from "./errors.js";
import { globals, loadChildProcess, WIN } from "./globals.js";
import { pathFinder } from "./path_finder.js";
import { getLogger } from "./set_logger.js";
import { splat } from "./splat.js";
import { splitArguments } from "./split_arguments.js";
import { remove, removeSync } from "@bearz/fs/remove";
/**
* Converts the command arguments to an array of strings.
* @param args Converts the command arguments to an array of strings.
* @returns The array of strings.
*/
export function convertCommandArgs(args) {
if (typeof args === "string") {
return splitArguments(args);
}
if (Array.isArray(args)) {
return args;
}
return splat(args);
}
/**
* Represents a command that can be executed.
*/
export class Command {
file;
args;
options;
static #pipeFactory;
/**
* Creates a new instance of the Command class.
* @param file The executable command.
* @param args The arguments for the command.
* @param options The options for the command.
*/
constructor(file, args, options) {
this.file = file;
this.args = args;
this.options = options ?? {};
this.options.log ??= getLogger();
}
toArgs() {
const args = convertCommandArgs(this.args ?? []);
return [this.file, ...args];
}
toOptions() {
return this.options ?? {};
}
/**
* Sets the current working directory for the command.
* @param value The current working directory.
* @returns The Command instance.
*/
withCwd(value) {
this.options ??= {};
this.options.cwd = value;
return this;
}
/**
* Sets the environment variables for the command.
* @param value The environment variables.
* @returns The Command instance.
*/
withEnv(value) {
this.options ??= {};
this.options.env = value;
return this;
}
/**
* Sets the user ID for the command.
* @param value The user ID.
* @returns The Command instance.
*/
withUid(value) {
this.options ??= {};
this.options.uid = value;
return this;
}
/**
* Sets the group ID for the command.
* @param value The group ID.
* @returns The Command instance.
*/
withGid(value) {
this.options ??= {};
this.options.gid = value;
return this;
}
/**
* Sets the abort signal for the command.
* @param value The abort signal.
* @returns The Command instance.
*/
withSignal(value) {
this.options ??= {};
this.options.signal = value;
return this;
}
/**
* Sets the arguments for the command.
* @param value The arguments.
* @returns The Command instance.
*/
withArgs(value) {
this.args = value;
return this;
}
/**
* Sets the stdin behavior for the command.
* @param value The stdin behavior.
* @returns The Command instance.
*/
withStdin(value) {
this.options ??= {};
this.options.stdin = value;
return this;
}
/**
* Sets the stdout behavior for the command.
* @param value The stdout behavior.
* @returns The Command instance.
*/
withStdout(value) {
this.options ??= {};
this.options.stdout = value;
return this;
}
/**
* Sets the stderr behavior for the command.
* @param value The stderr behavior.
* @returns The Command instance.
*/
withStderr(value) {
this.options ??= {};
this.options.stderr = value;
return this;
}
/**
* Thenable method that allows the Command object to be used as a promise which calls the `output` method.
* It is not recommended to use this method directly. Instead, use the `output` method.
* @param onfulfilled A function called when the promise is fulfilled.
* @param onrejected A function called when the promise is rejected.
* @returns A promise that resolves to the output of the command.
* @example
* ```ts
* var cmd = new Command("echo", ["hello world"], { stdout: 'piped' });
* const result = await cmd;
* console.log(result.code);
* console.log(result.stdout);
* console.log(result.text());
* ```
*/
then(
onfulfilled,
// deno-lint-ignore no-explicit-any
onrejected,
) {
return this.output().then(onfulfilled, onrejected);
}
/**
* Runs the command asynchronously and returns a promise that resolves to the output of the command.
* The stdout and stderr are set to `inherit`.
* @returns A promise that resolves to the output of the command.
*/
async run() {
this.options ??= {};
const { stdout, stderr } = this.options;
try {
this.options.stdout = "inherit";
this.options.stderr = "inherit";
return await this.output();
} finally {
this.options.stdout = stdout;
this.options.stderr = stderr;
}
}
/**
* Runs the command synchronously and returns the output of the command.
* The stdout and stderr are set to `inherit`.
* @returns The output of the command.
*/
runSync() {
this.options ??= {};
const { stdout, stderr } = this.options;
try {
this.options.stdout = "inherit";
this.options.stderr = "inherit";
return this.outputSync();
} finally {
this.options.stdout = stdout;
this.options.stderr = stderr;
}
}
pipe() {
this.options ??= {};
this.options.stdout = "piped";
this.options.stderr = "inherit";
if (arguments.length === 0) {
throw new Error("Invalid arguments");
}
let next;
if (typeof arguments[0] === "string") {
const args = arguments[1];
const options = arguments[2];
const ctor = Object.getPrototypeOf(this).constructor;
next = new ctor(arguments[0], args, options);
next.withStdout("piped");
next.withStdin("piped");
} else {
next = arguments[0];
}
Command.#pipeFactory ??= new PipeFactory((file, args, options) => {
const ctor = Object.getPrototypeOf(this).constructor;
next = new ctor(file, args, options);
next.withStdin("piped");
next.withStdout("piped");
return next;
});
return Command.#pipeFactory.create(this.spawn()).pipe(next);
}
/**
* Gets the output of the command as text.
* @returns A promise that resolves to the output of the command as text.
*/
async text() {
this.options ??= {};
const { stdout } = this.options;
try {
this.options.stdout = "piped";
const output = await this.output();
return output.text();
} finally {
this.options.stdout = stdout;
}
}
/**
* Gets the output of the command as an array of lines.
* @returns A promise that resolves to the output of the command as an array of lines.
*/
async lines() {
this.options ??= {};
const { stdout } = this.options;
try {
this.options.stdout = "piped";
const output = await this.output();
return output.lines();
} finally {
this.options.stdout = stdout;
}
}
/**
* Gets the output of the command as JSON.
* @returns A promise that resolves to the output of the command as JSON.
*/
async json() {
this.options ??= {};
const { stdout } = this.options;
try {
this.options.stdout = "piped";
const output = await this.output();
return output.json();
} finally {
this.options.stdout = stdout;
}
}
/**
* Gets the output of the command.
* @returns A promise that resolves to the output of the command.
*/
output() {
throw new Error("Not implemented");
}
/**
* Gets the output of the command synchronously.
* @returns The output of the command.
*/
outputSync() {
throw new Error("Not implemented");
}
/**
* Spawns a child process for the command.
* @returns The spawned child process.
*/
spawn() {
throw new Error("Not implemented");
}
}
/**
* Represents a shell command.
*/
export class ShellCommand extends Command {
shellArgs;
script;
isFile;
/**
* Creates a new instance of the ShellCommand class.
* @param exe The executable command.
* @param script The shell script or command to execute.
* @param options The options for the shell command.
*/
constructor(exe, script, options) {
super(exe, options?.args, options);
this.shellArgs = options?.shellArgs;
this.script = script;
this.isFile = options?.isFile;
}
/**
* Gets the file extension for the shell script.
* @returns The file extension.
*/
get ext() {
return "";
}
toArgs() {
const { file, generated } = this.getScriptFile();
const args = this.getShellArgs(file ?? this.script, generated || (this.isFile ?? false));
return [this.file, ...args];
}
/**
* Gets the shell arguments for the given script and file type.
* @param script The shell script or command.
* @param isFile Indicates whether the script is a file.
* @returns An array of shell arguments.
*/
// deno-lint-ignore no-unused-vars
getShellArgs(script, isFile) {
const args = this.shellArgs ?? [];
args.push(script);
return args;
}
/**
* Gets the script file information. The `file` property is undefined if the script is not a file.
* @returns An object containing the script file path and whether it was generated.
*/
getScriptFile() {
if (
this.isFile ||
(!this.script.match(/\n/) && this.ext.length &&
this.script.trimEnd().endsWith(this.ext))
) {
return { file: this.script, generated: false };
}
return { file: undefined, generated: false };
}
}
/**
* Creates a new command instance. This is a shorthand for creating a new
* {@linkcode Command} instance and defaults the stdin to `inherit`, stderr
* to `piped`, and stdout to `piped` if the options are not set.
*
* @param exe - The executable to run.
* @param args - The arguments to pass to the executable.
* @param options - The options for the command.
* @returns A new `CommandType` instance.
*/
export function cmd(exe, args, options) {
options ??= {};
options.stdin ??= "inherit";
options.stderr ??= "piped";
options.stdout ??= "piped";
return new Command(exe, args, options);
}
/**
* Converts a string representing invoking an executable into a command instance.
* @param command The command to execute.
* @param options The options for the command.
* @returns The command instance.
*/
export function exec(command, options) {
const args = splitArguments(command);
if (args.length === 0) {
throw new Error("Invalid command");
}
const exe = args.shift();
return cmd(exe, args, options);
}
/**
* Represents a pipe for executing commands and chaining them together.
*/
class Pipe {
process;
cmdFactory;
#promise;
/**
* Creates a new instance of the Pipe class.
* @param process The initial ChildProcess to start the pipe with.
* @param cmdFactory The factory function for creating Command instances.
*/
constructor(process, cmdFactory) {
this.process = process;
this.cmdFactory = cmdFactory;
this.#promise = Promise.resolve(process);
}
pipe() {
if (arguments.length === 0) {
throw new Error("Invalid arguments");
}
if (typeof arguments[0] === "string") {
const args = arguments[1];
const options = arguments[2];
const next = this.cmdFactory(arguments[0], args, options);
return this.pipe(next);
}
const next = arguments[0];
this.#promise = this.#promise.then(async (process) => {
let child = next;
if (typeof next === "object" && "spawn" in next) {
if ("stdin" in next) {
next.stdin("piped");
}
child = next.spawn();
}
try {
// force stdin to close
await process.stdout.pipeTo(child.stdin, { preventClose: false });
if (!process.stdout.locked) {
await process.stdout.cancel();
}
// if (process.stderr.)
//await process.stderr.cancel();
} catch (ex) {
if (ex instanceof Error) {
throw new Error(`Pipe failed for ${process}: ${ex.message} ${ex.stack}`);
} else {
throw new Error(`Pipe failed for ${process}: ${ex}`);
}
}
return child;
});
return this;
}
/**
* Retrieves the output of the pipe as an Output instance.
* @returns A Promise that resolves to the Output instance.
*/
async output() {
const process = await this.#promise;
return process.output();
}
}
/**
* Represents a factory for creating Pipe instances.
*/
class PipeFactory {
cmdFactory;
constructor(cmdFactory) {
this.cmdFactory = cmdFactory;
}
/**
* Creates a new Pipe instance.
* @param process The ChildProcess object to associate with the Pipe.
* @returns A new Pipe instance.
*/
create(process) {
return new Pipe(process, this.cmdFactory);
}
}
if (globals.Deno) {
const EMPTY = "";
class DenoChildProcess {
#childProcess;
#options;
#file;
constructor(childProcess, options, file) {
this.#childProcess = childProcess;
this.#options = options;
this.#file = file;
}
get stdin() {
return this.#childProcess.stdin;
}
get stdout() {
return this.#childProcess.stdout;
}
get stderr() {
return this.#childProcess.stderr;
}
get pid() {
return this.#childProcess.pid;
}
get status() {
return this.#childProcess.status;
}
ref() {
return this.#childProcess.ref();
}
unref() {
return this.#childProcess.unref();
}
async output() {
const out = await this.#childProcess.output();
return new DenoOutput({
stdout: this.#options.stdout === "piped" ? out.stdout : new Uint8Array(0),
stderr: this.#options.stderr === "piped" ? out.stderr : new Uint8Array(0),
code: out.code,
signal: out.signal,
success: out.success,
}, this.#file);
}
kill(signo) {
return this.#childProcess.kill(signo);
}
onDispose;
[Symbol.asyncDispose]() {
if (this.onDispose) {
this.onDispose();
}
return this.#childProcess[Symbol.asyncDispose]();
}
}
class DenoOutput {
#text;
#lines;
#json;
#errorText;
#errorLines;
#errorJson;
#file;
stdout;
stderr;
code;
signal;
success;
constructor(output, file) {
this.stdout = output.stdout;
this.stderr = output.stderr;
this.code = output.code;
this.signal = output.signal;
this.success = output.success;
this.#file = file;
}
validate(fn, failOnStderr) {
fn ??= (code) => code === 0;
if (!fn(this.code)) {
throw new CommandError({ fileName: this.#file, code: this.code });
}
if (failOnStderr && this.stderr.length > 0) {
throw new CommandError({
fileName: this.#file,
code: this.code,
message: `${this.#file} failed with stderr: ${this.errorText()}`,
});
}
return this;
}
text() {
if (this.#text) {
return this.#text;
}
if (this.stdout.length === 0) {
this.#text = EMPTY;
return this.#text;
}
this.#text = new TextDecoder().decode(this.stdout);
return this.#text;
}
lines() {
if (this.#lines) {
return this.#lines;
}
this.#lines = this.text().split(/\r?\n/);
return this.#lines;
}
json() {
if (this.#json) {
return this.#json;
}
this.#json = JSON.parse(this.text());
return this.#json;
}
errorText() {
if (this.#errorText) {
return this.#errorText;
}
if (this.stderr.length === 0) {
this.#errorText = EMPTY;
return this.#errorText;
}
this.#errorText = new TextDecoder().decode(this.stderr);
return this.#errorText;
}
errorLines() {
if (this.#errorLines) {
return this.#errorLines;
}
this.#errorLines = this.errorText().split(/\r?\n/);
return this.#errorLines;
}
errorJson() {
if (this.#errorJson) {
return this.#errorJson;
}
this.#errorJson = JSON.parse(this.errorText());
return this.#errorJson;
}
toString() {
return this.text();
}
}
Command.prototype.spawn = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const args = this.args ? convertCommandArgs(this.args) : undefined;
const options = {
...this.options,
args: args,
};
if (this.options?.log) {
this.options?.log(exe, args);
}
const process = new Deno.Command(exe, options);
return new DenoChildProcess(process.spawn(), options, this.file);
};
Command.prototype.outputSync = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const args = this.args ? convertCommandArgs(this.args) : undefined;
const options = {
...this.options,
args: args,
};
options.stdout ??= "piped";
options.stderr ??= "piped";
options.stdin ??= "inherit";
if (this.options?.log) {
this.options?.log(exe, args);
}
const process = new Deno.Command(exe, options);
const out = process.outputSync();
return new DenoOutput({
stdout: options.stdout === "piped" ? out.stdout : new Uint8Array(0),
stderr: options.stderr === "piped" ? out.stderr : new Uint8Array(0),
code: out.code,
signal: out.signal,
success: out.success,
}, this.file);
};
Command.prototype.output = async function () {
const exe = await pathFinder.findExe(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const args = this.args ? convertCommandArgs(this.args) : undefined;
const options = {
...this.options,
args: args,
// deno-lint-ignore no-explicit-any
};
options.stdout ??= "piped";
options.stderr ??= "piped";
options.stdin ??= "inherit";
if (this.options?.log) {
this.options?.log(exe, args);
}
const process = new Deno.Command(exe, options);
const out = await process.output();
return new DenoOutput({
stdout: options.stdout === "piped" ? out.stdout : new Uint8Array(0),
stderr: options.stderr === "piped" ? out.stderr : new Uint8Array(0),
code: out.code,
signal: out.signal,
success: out.success,
}, this.file);
};
ShellCommand.prototype.output = async function () {
const exe = await pathFinder.findExe(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const { file, generated } = this.getScriptFile();
const isFile = file !== undefined;
try {
const args = this.getShellArgs(isFile ? file : this.script, isFile);
if (isFile && this.args) {
const splat = convertCommandArgs(this.args);
args.push(...splat);
}
const options = {
...this.options,
args: args,
};
options.stdout ??= "piped";
options.stderr ??= "piped";
options.stdin ??= "inherit";
if (this.options?.log) {
this.options?.log(exe, args);
}
const process = new Deno.Command(exe, options);
const out = await process.output();
return new DenoOutput({
stdout: options.stdout === "piped" ? out.stdout : new Uint8Array(0),
stderr: options.stderr === "piped" ? out.stderr : new Uint8Array(0),
code: out.code,
signal: out.signal,
success: out.success,
}, this.file);
} finally {
if (isFile && generated) {
await remove(file);
}
}
};
ShellCommand.prototype.outputSync = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const { file, generated } = this.getScriptFile();
const isFile = file !== undefined;
try {
const args = this.getShellArgs(isFile ? file : this.script, isFile);
if (isFile && this.args) {
const splat = convertCommandArgs(this.args);
args.push(...splat);
}
const options = {
...this.options,
args: args,
};
options.stdout ??= "piped";
options.stderr ??= "piped";
options.stdin ??= "inherit";
if (this.options?.log) {
this.options?.log(exe, args);
}
const process = new Deno.Command(exe, options);
const out = process.outputSync();
return new DenoOutput({
stdout: options.stdout === "piped" ? out.stdout : new Uint8Array(0),
stderr: options.stderr === "piped" ? out.stderr : new Uint8Array(0),
code: out.code,
signal: out.signal,
success: out.success,
}, this.file);
} finally {
if (isFile && generated) {
removeSync(file);
}
}
};
ShellCommand.prototype.spawn = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const { file, generated } = this.getScriptFile();
const isFile = file !== undefined;
const args = this.getShellArgs(isFile ? file : this.script, isFile);
if (isFile && this.args) {
const splat = convertCommandArgs(this.args);
args.push(...splat);
}
const options = {
...this.options,
args: args,
};
if (this.options?.log) {
this.options?.log(exe, args);
}
const process = new Deno.Command(exe, options);
const proc = new DenoChildProcess(process.spawn(), options, this.file);
proc.onDispose = () => {
if (isFile && generated) {
removeSync(file);
}
};
return proc;
};
} else if (globals.process) {
const child_process = loadChildProcess();
const { spawn, spawnSync } = child_process;
class NodeOutput {
#text;
#lines;
#json;
#errorText;
#errorLines;
#errorJson;
#file;
#args;
stdout;
stderr;
code;
signal;
success;
constructor(output) {
this.stdout = output.stdout;
this.stderr = output.stderr;
this.code = output.code;
this.signal = output.signal;
this.success = output.success;
this.#file = output.file;
this.#args = output.args;
}
validate(fn, failOnStderr) {
fn ??= (code) => code === 0;
if (!fn(this.code)) {
throw new CommandError({
fileName: this.#file,
code: this.code,
});
}
if (failOnStderr && this.stderr.length > 0) {
throw new CommandError({
fileName: this.#file,
code: this.code,
message: `Command failed with stderr: ${this.errorText()}`,
});
}
return this;
}
text() {
if (this.#text) {
return this.#text;
}
if (this.stdout.length === 0) {
this.#text = "";
return this.#text;
}
this.#text = new TextDecoder().decode(this.stdout);
return this.#text;
}
lines() {
if (this.#lines) {
return this.#lines;
}
this.#lines = this.text().split(/\r?\n/);
return this.#lines;
}
json() {
if (this.#json) {
return this.#json;
}
this.#json = JSON.parse(this.text());
return this.#json;
}
errorText() {
if (this.#errorText) {
return this.#errorText;
}
if (this.stderr.length === 0) {
this.#errorText = "";
return this.#errorText;
}
this.#errorText = new TextDecoder().decode(this.stderr);
return this.#errorText;
}
errorLines() {
if (this.#errorLines) {
return this.#errorLines;
}
this.#errorLines = this.errorText().split(/\r?\n/);
return this.#errorLines;
}
errorJson() {
if (this.#errorJson) {
return this.#errorJson;
}
this.#errorJson = JSON.parse(this.errorText());
return this.#errorJson;
}
toString() {
return this.text();
}
}
class NodeChildProcess {
#child;
#stdout;
#stderr;
#stdin;
#status;
#file;
constructor(child, file, signal) {
this.#child = child;
this.#file = file;
if (signal !== undefined) {
signal.addEventListener("abort", () => {
child.kill("SIGTERM");
});
}
this.#status = new Promise((resolve, reject) => {
child.on("error", (_err) => {
reject(_err);
});
child.on("exit", (code, signal) => {
resolve({
success: code === 0,
code: code || 1,
signal: signal,
});
});
});
}
get pid() {
return this.#child.pid ? this.#child.pid : -1;
}
get status() {
return this.#status;
}
get stdout() {
if (this.#stdout) {
return this.#stdout;
}
const child = this.#child;
if (child.stdout === null) {
throw new Error("stdout is not available");
}
const stream = new ReadableStream({
start(controller) {
if (child.stdout === null) {
controller.close();
return;
}
child.stdout.on("data", (data) => {
if (data instanceof Uint8Array) {
controller.enqueue(data);
}
});
child.stdout.on("end", () => {
controller.close();
});
},
});
this.#stdout = stream;
return stream;
}
get stderr() {
const child = this.#child;
if (child.stderr === null) {
throw new Error("stderr is not available");
}
const stream = new ReadableStream({
start(controller) {
if (child.stderr === null) {
controller.close();
return;
}
child.stderr.on("data", (data) => {
controller.enqueue(data);
});
child.stderr.on("end", () => {
controller.close();
});
},
});
return stream;
}
get stdin() {
if (this.#stdin) {
return this.#stdin;
}
const child = this.#child;
if (child.stdin === null) {
throw new Error("stdin is not available");
}
const stream = new WritableStream({
write(chunk) {
child.stdin?.write(chunk);
},
close() {
child.stdin?.end();
},
abort() {
child.stdin?.end();
},
});
this.#stdin = stream;
return stream;
}
kill(signo) {
if (signo === "SIGEMT") {
signo = "SIGTERM";
}
this.#child.kill(signo);
}
ref() {
this.#child.ref();
}
unref() {
this.#child.unref();
}
output() {
let stdout = new Uint8Array(0);
let stderr = new Uint8Array(0);
this.#child.stdout?.on("data", (data) => {
stdout = new Uint8Array([...stdout, ...data]);
});
this.#child.stderr?.on("data", (data) => {
stderr = new Uint8Array([...stderr, ...data]);
});
return new Promise((resolve, reject) => {
this.#child.on("error", (err) => {
reject(err);
});
this.#child.on("exit", (code, signal) => {
const o = {
file: this.#file,
stdout: stdout,
stderr: stderr,
code: code,
signal: signal,
success: code === 0,
};
resolve(new NodeOutput(o));
});
});
}
onDispose;
async [Symbol.asyncDispose]() {
if (this.onDispose) {
this.onDispose();
}
await this.status;
}
}
// deno-lint-ignore no-inner-declarations
function mapPipe(pipe) {
if (pipe === undefined) {
return undefined;
}
if (pipe === "inherit") {
return "inherit";
}
if (pipe === "null") {
return "ignore";
}
if (pipe === "piped") {
return "pipe";
}
return undefined;
}
Command.prototype.output = async function () {
const exe = await pathFinder.findExe(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const args = this.args ? convertCommandArgs(this.args) : [];
let signal;
if (this.options?.signal) {
signal = this.options.signal;
}
const o = this.options ?? {};
o.stdin ??= "inherit";
o.stdout ??= "piped";
o.stderr ??= "piped";
// handle shell options for windows and node's limitation for spawn
// where it will no longer execute the command if shell is not set
// for cmd and bat files.
let shell = o.shell ?? false;
const detached = o.detached ?? undefined;
const windowsHide = o.windowsHide ?? undefined;
if (WIN && exe.endsWith(".bat") || exe.endsWith(".cmd")) {
shell = true;
}
const child = spawn(exe, args, {
cwd: o.cwd,
env: o.env,
gid: o.gid,
uid: o.uid,
shell: shell,
detached: detached,
windowsHide: windowsHide,
stdio: [mapPipe(o.stdin), mapPipe(o.stdout), mapPipe(o.stderr)],
windowsVerbatimArguments: o.windowsRawArguments,
// deno-lint-ignore no-explicit-any
signal: signal,
});
if (this.options?.log) {
this.options.log(exe, args);
}
let stdout = new Uint8Array(0);
let stderr = new Uint8Array(0);
let code = 1;
let sig;
const promises = [];
if (child.stdout !== null) {
child.stdout.on("data", (data) => {
stdout = new Uint8Array([...stdout, ...data]);
});
}
if (child.stderr !== null) {
child.stderr.on("data", (data) => {
stderr = new Uint8Array([...stderr, ...data]);
});
}
promises.push(
new Promise((resolve) => {
if (child.stdout === null) {
resolve();
return;
}
child.stdout.on("end", () => {
resolve();
});
}),
);
promises.push(
new Promise((resolve) => {
if (child.stderr === null) {
resolve();
return;
}
child.stderr.on("end", () => {
resolve();
});
}),
);
promises.push(
new Promise((resolve) => {
child.on("exit", (c, s) => {
code = c !== null ? c : 1;
sig = s === null ? undefined : s;
resolve();
});
}),
);
await Promise.all(promises);
return new NodeOutput({
file: this.file,
stdout: stdout,
stderr: stderr,
code: code,
signal: sig,
success: code === 0,
});
};
Command.prototype.outputSync = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const args = this.args ? convertCommandArgs(this.args) : [];
const o = {
...this.options,
};
o.stdin ??= "inherit";
o.stdout ??= "piped";
o.stderr ??= "piped";
if (this.options?.log) {
this.options.log(exe, args);
}
// handle shell options for windows and node's limitation for spawn
// where it will no longer execute the command if shell is not set
// for cmd and bat files.
let shell = o.shell ?? false;
const detached = o.detached ?? undefined;
const windowsHide = o.windowsHide ?? undefined;
if (WIN && exe.endsWith(".bat") || exe.endsWith(".cmd")) {
shell = true;
}
const child = spawnSync(exe, args, {
cwd: o.cwd,
env: o.env,
gid: o.gid,
uid: o.uid,
shell: shell,
detached: detached,
windowsHide: windowsHide,
stdio: [mapPipe(o.stdin), mapPipe(o.stdout), mapPipe(o.stderr)],
windowsVerbatimArguments: o.windowsRawArguments,
});
const code = child.status ? child.status : 1;
return new NodeOutput({
file: this.file,
stdout: new Uint8Array(0),
stderr: new Uint8Array(0),
code: child.status ? child.status : 1,
signal: child.signal,
success: code === 0,
});
};
Command.prototype.spawn = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const args = this.args ? convertCommandArgs(this.args) : [];
const o = {
...this.options,
};
o.stdout ??= "inherit";
o.stderr ??= "inherit";
o.stdin ??= "inherit";
const stdin = mapPipe(o.stdin);
const stdout = mapPipe(o.stdout);
const stderr = mapPipe(o.stderr);
if (this.options?.log) {
this.options.log(exe, args);
}
// handle shell options for windows and node's limitation for spawn
// where it will no longer execute the command if shell is not set
// for cmd and bat files.
let shell = o.shell ?? false;
const detached = o.detached ?? undefined;
const windowsHide = o.windowsHide ?? undefined;
if (WIN && exe.endsWith(".bat") || exe.endsWith(".cmd")) {
shell = true;
}
const child = spawn(exe, args, {
cwd: o.cwd,
env: o.env,
gid: o.gid,
uid: o.uid,
shell: shell,
detached: detached,
windowsHide: windowsHide,
stdio: [stdin, stdout, stderr],
windowsVerbatimArguments: o.windowsRawArguments,
});
return new NodeChildProcess(child, this.file, o.signal);
};
ShellCommand.prototype.output = async function () {
const exe = await pathFinder.findExe(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const { file, generated } = this.getScriptFile();
const isFile = file !== undefined;
try {
const args = this.getShellArgs(isFile ? file : this.script, isFile);
if (isFile && this.args) {
const splat = convertCommandArgs(this.args);
args.push(...splat);
}
let signal;
if (this.options?.signal) {
signal = this.options.signal;
}
const o = this.options ?? {};
o.stdin ??= "inherit";
o.stdout ??= "piped";
o.stderr ??= "piped";
if (this.options?.log) {
this.options.log(exe, args);
}
// handle shell options for windows and node's limitation for spawn
// where it will no longer execute the command if shell is not set
// for cmd and bat files.
let shell = o.shell ?? false;
const detached = o.detached ?? undefined;
const windowsHide = o.windowsHide ?? undefined;
if (WIN && exe.endsWith(".bat") || exe.endsWith(".cmd")) {
shell = true;
}
const child = spawn(exe, args, {
cwd: o.cwd,
env: o.env,
gid: o.gid,
uid: o.uid,
shell: shell,
detached: detached,
windowsHide: windowsHide,
stdio: [mapPipe(o.stdin), mapPipe(o.stdout), mapPipe(o.stderr)],
windowsVerbatimArguments: o.windowsRawArguments,
// deno-lint-ignore no-explicit-any
signal: signal,
});
let stdout = new Uint8Array(0);
let stderr = new Uint8Array(0);
let code = 1;
let sig;
const promises = [];
if (child.stdout !== null) {
child.stdout.on("data", (data) => {
stdout = new Uint8Array([...stdout, ...data]);
});
}
if (child.stderr !== null) {
child.stderr.on("data", (data) => {
stderr = new Uint8Array([...stderr, ...data]);
});
}
promises.push(
new Promise((resolve) => {
if (child.stdout === null) {
resolve();
return;
}
child.stdout.on("end", () => {
resolve();
});
}),
);
promises.push(
new Promise((resolve) => {
if (child.stderr === null) {
resolve();
return;
}
child.stderr.on("end", () => {
resolve();
});
}),
);
promises.push(
new Promise((resolve) => {
child.on("exit", (c, s) => {
code = c !== null ? c : 1;
sig = s === null ? undefined : s;
resolve();
});
}),
);
await Promise.all(promises);
return new NodeOutput({
file: exe,
stdout: stdout,
stderr: stderr,
code: code,
signal: sig,
success: code === 0,
});
} finally {
if (isFile && generated) {
await remove(file);
}
}
};
ShellCommand.prototype.outputSync = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const { file, generated } = this.getScriptFile();
const isFile = file !== undefined;
try {
const args = this.getShellArgs(isFile ? file : this.script, isFile);
if (isFile && this.args) {
const splat = convertCommandArgs(this.args);
args.push(...splat);
}
const o = {
...this.options,
};
o.stdin ??= "inherit";
o.stdout ??= "piped";
o.stderr ??= "piped";
if (this.options?.log) {
this.options.log(exe, args);
}
// handle shell options for windows and node's limitation for spawn
// where it will no longer execute the command if shell is not set
// for cmd and bat files.
let shell = o.shell ?? false;
const detached = o.detached ?? undefined;
const windowsHide = o.windowsHide ?? undefined;
if (WIN && this.file.endsWith(".bat") || this.file.endsWith(".cmd")) {
shell = true;
}
const child = spawnSync(this.file, args, {
cwd: o.cwd,
env: o.env,
gid: o.gid,
uid: o.uid,
shell: shell,
detached: detached,
windowsHide: windowsHide,
stdio: [mapPipe(o.stdin), mapPipe(o.stdout), mapPipe(o.stderr)],
windowsVerbatimArguments: o.windowsRawArguments,
});
if (this.options?.log) {
this.options.log(this.file, args);
}
const code = child.status ? child.status : 1;
return new NodeOutput({
file: this.file,
stdout: new Uint8Array(0),
stderr: new Uint8Array(0),
code: child.status ? child.status : 1,
signal: child.signal,
success: code === 0,
});
} finally {
if (isFile && generated) {
removeSync(file);
}
}
};
ShellCommand.prototype.spawn = function () {
const exe = pathFinder.findExeSync(this.file);
if (exe === undefined) {
throw new NotFoundOnPathError(this.file);
}
const { file, generated } = this.getScriptFile();
const isFile = file !== undefined;
const args = this.getShellArgs(isFile ? file : this.script, isFile);
if (isFile && this.args) {
const splat = convertCommandArgs(this.args);
args.push(...splat);
}
const o = {
...this.options,
};
o.stdout ??= "inherit";
o.stderr ??= "inherit";
o.stdin ??= "inherit";
const stdin = mapPipe(o.stdin);
const stdout = mapPipe(o.stdout);
const stderr = mapPipe(o.stderr);
if (this.options?.log) {
this.options.log(exe, args);
}
// handle shell options for windows and node's limitation for spawn
// where it will no longer execute the command if shell is not set
// for cmd and bat files.
let shell = o.shell ?? false;
const detached = o.detached ?? undefined;
const windowsHide = o.windowsHide ?? undefined;
if (WIN && this.file.endsWith(".bat") || this.file.endsWith(".cmd")) {
shell = true;
}
const child = spawn(exe, args, {
cwd: o.cwd,
env: o.env,
gid: o.gid,
uid: o.uid,
shell: shell,
detached: detached,
windowsHide: windowsHide,
stdio: [stdin, stdout, stderr],
windowsVerbatimArguments: o.windowsRawArguments,
});
const proc = new NodeChildProcess(child, this.file, o.signal);
proc.onDispose = () => {
if (isFile && generated) {
removeSync(file);
}
};
return proc;
};
}
/**
* Run a command and return the output. This is a shorthand for creating a new
* {@linkcode Command} and calling {@linkcode Command.output} with stdout and
* stderr set to `inherit`.
* @param exe The executable to run.
* @param args The arguments to pass to the executable.
* @param options The options to run the command with.
* @returns The output of the command.
*/
export function run(exe, args, options) {
const o = options || {};
o.stderr = "inherit";
o.stdout = "inherit";
return new Command(exe, args, o).output();
}
/**
* Run a command and return the output synchronously. This is a shorthand for
* creating a new {@linkcode Command} and calling {@linkcode Command.outputSync}
* with stdout and stderr set to `inherit`.
* @param exe The executable to run.
* @param args The arguments to pass to the executable.
* @param options The options to run the command with.
* @returns The output of the command.
*/
export function runSync(exe, args, options) {
const o = options || {};
o.stderr = "inherit";
o.stdout = "inherit";
return new Command(exe, args, options).outputSync();
}
/**
* Run a command and return the output. This is a shorthand for creating a new
* {@linkcode Command} and calling {@linkcode Command.output} with stderr and
* stdout defaulting to `piped` if not set in the options.
* @param exe The executable to run.
* @param args The arguments to pass to the executable.
* @param options The options to run the command with.
* @returns The output of the command.
*/
export function output(exe, args, options) {
options ??= {};
options.stdin ??= "inherit";
options.stderr ??= "piped";
options.stdout ??= "piped";
return new Command(exe, args, options).output();
}
/**
* Run a command and return the output synchronously. This is a shorthand for
* creating a new {@linkcode Command} and calling {@linkcode Command.outputSync}
* with stderr and stdout defaulting to `piped` if not set in the options.
* @param exe The executable to run.
* @param args The arguments to pass to the executable.
* @param options The options to run the command with.
* @returns The output of the command.
*/
export function outputSync(exe, args, options) {
options ??= {};
options.stderr = "piped";
options.stdout = "piped";