Skip to content

Commit

Permalink
Update utilities with Alhadis/Utils@fc8e057
Browse files Browse the repository at this point in the history
  • Loading branch information
Alhadis committed Jun 18, 2019
1 parent 2ff104e commit e9924f1
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 20 deletions.
49 changes: 29 additions & 20 deletions lib/utils/shell.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
/**
* Execute an external command.
*
* Resolves to an object with `stdin`, `stdout`, and `code` properties.
* Rejects with an error if the subprocess emitted an "error" event.
*
* @throws {Error} Rejects if the subprocess emits an `error` event.
* @example exec("sed", ["-e", "s/in/out/"], "input");
* @param {String} command - Name of the command to execute.
* @param {String[]} argList - Arguments/switches passed to command.
* @param {String} [input=null] - Data to pipe to standard input, if any.
* @param {ExecOptions} [options={}] - Additional options. See {@link ExecOptions}.
* @return {Object}
* @return {Promise<ExecResult>}
*/
export async function exec(command, argList = [], input = null, options = {}){
const defaultEncoding = "utf8";
Expand Down Expand Up @@ -49,14 +47,14 @@ export async function exec(command, argList = [], input = null, options = {}){
* @description
* Extra options used by {@link exec}. Strings are considered shorthand for `{encoding: "…"}`.
*
* @param {String} [cwd]
* @property {String} [cwd]
* Current working directory of executed command.
*
* @param {String[]|String} [encoding="utf8"]
* @property {String[]|String} [encoding="utf8"]
* Character encodings of stdin, stdout and stderr, respectively. Strings are
* treated as shorthand for setting the encodings of all three streams at once.
*
* @param {Object} [env={}]
* @property {Object} [env={}]
* Environment variables to include on top of the contents of {@link process.env}.
* Matching keys are overwritten; the existing environment is unaffected.
*
Expand All @@ -65,33 +63,44 @@ export async function exec(command, argList = [], input = null, options = {}){
* captured and returned as the resolved object's `stdout` property.
*/

/**
* @typedef {Object} ExecResult
* @description Object returned from a finished {@link exec} call.
* @property {Number} [code=0] - Exit status
* @property {String} [stdout=""] - Standard output
* @property {String} [stderr=""] - Standard error
*/


/**
* Execute a pipeline of chained commands.
*
* Resolves to an object with `stdin`, `stdout`, and `code` properties
* derived from the last command that finished executing.
* Execute a pipeline of chained {@link exec} calls.
*
* @uses {@link exec}
* @example execChain([["ps", "ax"], ["grep", "ssh"]], stdin);
* @param {Array[]} commands - List of commands/argv pairs
* @param {String} [inputData=null] - Data piped to stdin
* @param {String} [outputPath=""] - File to write stdout to
* @param {String} [encoding="utf8"] - Character encoding
* @return {Promise}
* @param {Array[]} commands - List of command/argv pairs.
* @param {String} [input=null] - Data piped to first command's standard input.
* @param {ExecOptions} [options={}] - Options passed to {@link exec}.
*
* @return {Promise<ExecResult>}
* Resolves with the standard output and exit code of the last command.
* Standard error is concatenated from each command's stderr stream.
*/
export async function execChain(commands, inputData = null, outputPath = ""){
export async function execChain(commands, input = null, options = {}){
commands = commands.slice();
let result = {};
let stderr = "";
while(commands.length){
let command = commands.shift();
if("string" === typeof command)
command = [command];
const [cmd, ...argv] = command;
const output = commands.length ? "" : outputPath;
result = await exec(cmd, argv, inputData, output);
inputData = result.stdout;
const opts = {...options};
if(commands.length) opts.outputPath = "";
result = await exec(cmd, argv, input, opts);
stderr += result.stderr;
input = result.stdout;
}
result.stderr = stderr;
return result;
}

Expand Down
59 changes: 59 additions & 0 deletions test/1.2-shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,65 @@ describe("Shell integration", () => {
});
});

describe("execChain()", function(){
const {execChain} = require("..");
this.slow(1000);

it("executes a pipeline of external commands", async () =>
expect(await execChain([
["printf", "%s\\n", "foo"],
["sed", "s/foo/bar/"],
["tr", "a-z", "A-Z"],
])).to.eql({code: 0, stdout: "BAR\n", stderr: ""}));

it("executes pipelines asynchronously", async () =>
expect(execChain([["true"]])).to.be.a("promise"));

it("avoids modifying the original command list", async () => {
const cmds = [["echo", "Foo"], ["grep", "Foo"]];
const copy = JSON.parse(JSON.stringify(cmds));
await execChain(cmds);
expect(cmds).to.eql(copy);
});

it("returns the exit status of the last command", async () => {
expect(await execChain([["true"], ["false"]])).to.eql({stdout: "", stderr: "", code: 1});
expect(await execChain([["false"], ["true"]])).to.eql({stdout: "", stderr: "", code: 0});
});

it("concatenates each command's stderr stream", async () =>
expect(await execChain([
["node", "-e", 'console.log("ABC"); console.warn("123")'],
["node", "-e", 'console.log("XYZ"); console.warn("456")'],
])).to.eql({
code: 0,
stderr: "123\n456\n",
stdout: "XYZ\n",
}));

it("can pipe input to the first command", async () =>
expect(await execChain([["sed", "s/foo/bar/"]], "<foo>")).to.eql({
code: 0,
stderr: "",
stdout: "<bar>\n",
}));

it("can write the last command's output to a file", async () => {
const tmp = require("path").join(__dirname, "fixtures", "temp.log");
fs.existsSync(tmp) && fs.unlinkSync(tmp);
expect(await execChain([
["node", "-e", "console.warn(123); console.log(456)"],
["sed", "s/456/bar/"],
], null, {outputPath: tmp})).to.eql({
code: 0,
stderr: "123\n",
stdout: "",
});
expect(fs.readFileSync(tmp, "utf8")).to.equal("bar\n");
fs.unlinkSync(tmp);
});
});

describe("which()", () => {
const {which} = require("..");
let firstNode = "";
Expand Down

0 comments on commit e9924f1

Please sign in to comment.