Skip to content

Commit

Permalink
feat: implement
Browse files Browse the repository at this point in the history
  • Loading branch information
hugojosefson committed Jul 15, 2022
1 parent 2bd2a6f commit 490cff8
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 5 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Simple run function to execute shell commands in Deno.

Returns a Promise of what the command printed.

The promise rejects with status if the command fails.

## Usage

```typescript
Expand All @@ -22,13 +26,13 @@ console.log(idLine);
const remoteHost = "10.20.30.40";
const remoteCommand = "ps -ef --forest";
const remoteProcessTree: string = await run(["ssh", remoteHost, remoteCommand]);
console.log(remoteProcessTree);

// Supply STDIN to the command
const contents = `# Remote file
This will be the contents of the remote file.
`;
// In this case, ignore response string
await run(
["ssh", remoteHost, "bash", "-c", "cat > remote_file.md"],
{ stdin: contents },
Expand Down
7 changes: 7 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { mapKeys } from "https://deno.land/[email protected]/collections/map_keys.ts";
export { mapValues } from "https://deno.land/[email protected]/collections/map_values.ts";

export { camelCase } from "https://deno.land/x/[email protected]/mod.ts";
export { Select } from "https://deno.land/x/[email protected]/prompt/select.ts";
export { fetch as fetchFile } from "https://deno.land/x/[email protected]/mod.ts";
export { default as _parseColumns } from "https://cdn.skypack.dev/pin/[email protected]/mode=imports/optimized/parse-columns.js";
2 changes: 1 addition & 1 deletion example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ console.log(idLine);
const remoteHost = "10.20.30.40";
const remoteCommand = "ps -ef --forest";
const remoteProcessTree: string = await run(["ssh", remoteHost, remoteCommand]);
console.log(remoteProcessTree);

// Supply STDIN to the command
const contents = `# Remote file
This will be the contents of the remote file.
`;
// In this case, ignore response string
await run(
["ssh", remoteHost, "bash", "-c", "cat > remote_file.md"],
{ stdin: contents },
Expand Down
3 changes: 2 additions & 1 deletion mod.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { run } from "./src/run.ts";
export type { CommandFailure, RunOptions, SimpleValue } from "./src/run.ts";
export { jsonRun, run } from "./src/run.ts";
25 changes: 25 additions & 0 deletions src/fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export function isString(s?: string | unknown): s is string {
return typeof s === "string";
}

const decoder = new TextDecoder();
export function asString(buf: Uint8Array | null | undefined): string {
if (!buf) {
return "";
}
return decoder.decode(buf).trim();
}
export function j(obj: unknown, indentation = 2): string {
return JSON.stringify(obj, null, indentation);
}

export function parseJsonSafe(input: string | unknown): string | unknown {
if (isString(input)) {
try {
return JSON.parse(input);
} catch (_ignore) {
/* intentional fall-through, and return the input as-is */
}
}
return input;
}
8 changes: 8 additions & 0 deletions src/os.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export async function weakEnvGet(
variable: string,
): Promise<false | string | undefined> {
const query = { name: "env" as const, variable };
const permissionResponse = await Deno.permissions.query(query);
const isGranted = permissionResponse.state === "granted";
return isGranted && Deno.env.get(variable);
}
119 changes: 117 additions & 2 deletions src/run.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,118 @@
export async function run() {
throw new Error("Work In Progress; not implemented yet.");
import { asString, isString, j, parseJsonSafe } from "./fn.ts";
import { weakEnvGet } from "./os.ts";

/** Item in the command array. */
export type SimpleValue = string | number | boolean;

/**
* What the promise is rejected with, if the command exits with a non-zero exit code.
*/
export interface CommandFailure {
/** {@link Deno.ProcessStatus} from the underlying {@link Deno.Process}. */
status: Deno.ProcessStatus;
/** What the command printed to STDERR. */
stderr: string;
/** What the command printed to STDOUT (as far as it got). */
stdout: string;
}

export interface RunOptions {
/** If specified, will be supplied as STDIN to the command. */
stdin?: string;
/** Print extra details to STDERR; default to whether env variable `"VERBOSE"` has a truthy value, and `--allow-env` is enabled. */
verbose?: boolean;
}

const defaultRunOptions: RunOptions = {
verbose: !!await weakEnvGet("VERBOSE"),
};

async function tryRun(
cmd: string[],
options: RunOptions = defaultRunOptions,
): Promise<string> {
options = { ...defaultRunOptions, ...options };

const pipeStdIn = isString(options.stdin);

if (options.verbose) {
console.error(`
===============================================================================
${j({ cmd, stdin: options.stdin })}
-------------------------------------------------------------------------------`);
}
const process = Deno.run({
cmd,
stdin: pipeStdIn ? "piped" : "null",
stdout: "piped",
stderr: "piped",
});

if (pipeStdIn) {
const stdinBuf = new TextEncoder().encode(options.stdin);
try {
await process.stdin?.write(stdinBuf);
} finally {
process.stdin?.close();
}
}

const [
status,
stdout,
stderr,
] = await Promise.all([
process.status(),
process.output(),
process.stderrOutput(),
]);
process.close();

if (status.success) {
const stdoutString = asString(stdout);
if (options.verbose) {
console.error(stdoutString);
}
return stdoutString;
}
const reason: CommandFailure = {
status,
stderr: asString(stderr),
stdout: asString(stdout),
};
return Promise.reject(reason);
}

/**
* Runs command, returning a Promise for what the command prints to STDOUT. If the command exits with non-zero exit code, the promise rejects with a {@link CommandFailure}.
* @param command The command to run, as an array of strings, or as a single string. You do not need to quote arguments that contain spaces, when supplying the command as an array. If using the single string format, be careful with spaces.
* @param options Options for the execution.
*/
export async function run(
command: string | SimpleValue[],
options: RunOptions = defaultRunOptions,
): Promise<string> {
const cmd: string[] = isString(command)
? command.split(" ")
: command.map((segment) => `${segment}`);
try {
return await tryRun(cmd, options);
} catch (error) {
if (options.verbose) {
console.error(j({ cmd, error }));
}
throw error;
}
}

/**
* Runs a command, just like {@link run}, but parses the response as JSON if possible. Otherwise, returns it as-is.
* @param command The command to run, as an array of strings, or as a single string. You do not need to quote arguments that contain spaces, when supplying the command as an array. If using the single string format, be careful with spaces.
* @param options {@link RunOptions} for the execution.
*/
export async function jsonRun<T>(
command: string | SimpleValue[],
options: RunOptions = defaultRunOptions,
): Promise<T> {
return parseJsonSafe(await run(command, options)) as T;
}

0 comments on commit 490cff8

Please sign in to comment.