├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── README.md ├── examples ├── .eslintrc.json ├── 1.js ├── 2.js ├── 3.js ├── 4.js ├── 5.js ├── 6.js ├── 7.js ├── 8.js ├── 9.js └── package.json ├── lib └── index.js ├── package.json └── test ├── fixture ├── cat.js ├── env.js ├── env2.js ├── error.js └── json.js └── test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | "Promise": true 7 | }, 8 | "rules": { 9 | "indent": ["error", 2], 10 | "no-unused-vars": ["error", {"vars": "all", "args": "after-used"}], 11 | "no-undef": ["error"], 12 | "quotes": ["error", "double", {"avoidEscape": true}], 13 | "semi": ["error", "always"], 14 | "strict": ["error", "global"], 15 | "no-redeclare": ["error", {"builtinGlobals": true}] 16 | } 17 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | .eslintrc* 4 | .gitattributes 5 | node_modules 6 | examples 7 | test 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pshell 2 | 3 | Provides a simple Promise-based interface for running shell commands. 4 | 5 | # Basic usage 6 | 7 | ```js 8 | var shell = require("pshell"); 9 | 10 | shell("node --version").then(res => { 11 | console.log("exit code:", res.code); 12 | }); 13 | 14 | /****** console output ******* 15 | /bin/sh -c node --version 16 | v4.5.0 17 | exit code: 0 18 | ******************************/ 19 | ``` 20 | 21 | # Why? 22 | 23 | Writing a shell script in JavaScript is: 24 | 25 | - Easier than bash script to most developers. 26 | - More portable (don't leave Windows users behind). 27 | - More powerful in managing child processes in asynchronous way. 28 | 29 | # More details 30 | 31 | > Don't echo the commands. 32 | 33 | ```js 34 | var shell = require("pshell"); 35 | 36 | shell("node --version", {echoCommand: false}).then(res => { 37 | console.log("exit code:", res.code); 38 | }); 39 | 40 | /****** console output ******* 41 | v4.5.0 42 | exit code: 0 43 | ******************************/ 44 | ``` 45 | 46 | > Capture the output as a string. 47 | 48 | ```js 49 | var shell = require("pshell"); 50 | 51 | shell("node --version", {echoCommand: false, captureOutput: true}).then(res => { 52 | console.log("stdout:", JSON.stringify(res.stdout)); 53 | console.log("exit code:", res.code); 54 | }); 55 | 56 | /****** console output ******* 57 | stdout: "v4.5.0\n" 58 | exit code: 0 59 | ******************************/ 60 | ``` 61 | 62 | > Configure the global options so you don't need to specify the same options every time. 63 | 64 | ```js 65 | var shell = require("pshell"); 66 | 67 | shell.options.echoCommand = false; 68 | shell.options.captureOutput = true; 69 | 70 | Promise.all([ 71 | shell("node --version"), 72 | shell("npm --version") 73 | ]).then(res => { 74 | process.stdout.write("Node version: " + res[0].stdout); 75 | process.stdout.write("NPM version: " + res[1].stdout); 76 | }); 77 | 78 | /****** console output ******* 79 | Node version: v4.5.0 80 | NPM version: 3.10.6 81 | ******************************/ 82 | ``` 83 | 84 | > You can get a pre-configured version of `shell` function by calling the `context` API. This is a good way to avoid modifying the global options. Other `pshell` users in the same process won't be affected. 85 | 86 | ```js 87 | var shell = require("pshell").context({echoCommand: false, captureOutput: true}); 88 | 89 | Promise.all([ 90 | shell("node --version"), 91 | shell("npm --version") 92 | ]).then(res => { 93 | process.stdout.write("Node version: " + res[0].stdout); 94 | process.stdout.write("NPM version: " + res[1].stdout); 95 | }); 96 | 97 | /****** console output ******* 98 | Node version: v4.5.0 99 | NPM version: 3.10.6 100 | ******************************/ 101 | ``` 102 | 103 | > A non-zero exit code rejects the promise by default. 104 | 105 | ```js 106 | var shell = require("pshell").context({echoCommand: false}); 107 | 108 | // Using double quotes here is intentional. Windows shell supports double quotes only. 109 | shell('node -e "process.exit(1)"').then(res => { 110 | console.log("exit code:", res.code); 111 | }).catch(err => { 112 | console.error("error occurred:", err.message); 113 | }); 114 | 115 | /****** console output ******* 116 | error occurred: [Error: Process 26546 exited with code 1] 117 | ******************************/ 118 | ``` 119 | 120 | > Set `ignoreError` to `true` if you want to get the promise resolved instead. 121 | 122 | ```js 123 | var shell = require("pshell").context({echoCommand: false, ignoreError: true}); 124 | 125 | // Using double quotes here is intentional. Windows shell supports double quotes only. 126 | shell('node -e "process.exit(1)"').then(res => { 127 | console.log("exit code:", res.code); 128 | }).catch(err => { 129 | console.error("error occurred:", err.message); 130 | }); 131 | 132 | /****** console output ******* 133 | exit code: 1 134 | ******************************/ 135 | ``` 136 | 137 | # API 138 | 139 | ### shell(command[, options]) 140 | 141 | Executes the `command` string using the system's default shell (`"/bin/sh"` on Unix, `"cmd.exe"` on Windows). An optional `options` object can be given to override the base options for this session. 142 | 143 | Returns a promise that will be resolved with the [result object](#result-object) when the command execution completes. 144 | 145 | ### shell.options 146 | 147 | An object represents the base options to be applied to every session executed from this context. You can modify fields of this object to change the default behavior. 148 | 149 | ### shell.context([options]) 150 | 151 | Creates a new `shell()` function that is pre-configured with the specified options (combined with the current base options). 152 | 153 | ### shell.spawn(exec[, args[, options]]) 154 | 155 | Executes a child process specified in `exec` directly without the system shell layer. `args` is an array of string arguments to be given to the process. Returns an object `{childProcess, promise}`. See [`ChildProcess`](https://nodejs.org/api/child_process.html#child_process_class_childprocess) for more details about Node's underlying child process object. 156 | 157 | This is the base method for implementing `shell()` and `shell.exec()`. 158 | 159 | ```js 160 | var shell = require("pshell").context({echoCommand: false}); 161 | 162 | var ret = shell.spawn("node", ["--version"], {captureOutput: true}); 163 | 164 | console.log("Node process ID:", ret.childProcess.pid); 165 | 166 | ret.promise.then(function(res) { 167 | console.log("stdout:", JSON.stringify(res.stdout)); 168 | }); 169 | 170 | /****** console output ******* 171 | Node process ID: 16372 172 | stdout: "v4.5.0\n" 173 | ******************************/ 174 | ``` 175 | 176 | ### shell.exec(command[, options]) 177 | 178 | Same as `shell()` but returns an object `{childProcess, promise}` instead of a promise. Primary reason that you might want to use this function instead of `shell()` is probably to access the child process object for advanced use cases. However, keep in mind that the child process object returned by this method represents the system shell process, not the command process. 179 | 180 | Calling `shell(cmd, opts)` is same as calling `shell.exec(cmd, opts).promise`. 181 | 182 | ### shell.env(def) 183 | 184 | Gets an object containing key-value pairs of environment variables combined with `process.env`. This is a low level function that can be used to construct an object for `options.rawEnv`. 185 | 186 | In most cases, you will not need to use this function directly. Using `options.env` would be sufficient. See `options.env` and `options.rawEnv` for more details. 187 | 188 | # Options 189 | 190 | ### options.Promise (default: `null`) 191 | 192 | A `Promise` constructor to use instead of the system default one. 193 | 194 | ### options.echoCommand (default: `true`) 195 | 196 | If truthy, prints the command string to the console. If you specify a function, it gets called like `func(exec, args)` with `this` set to the options object. If you return `false` from the function, the command is not executed. `childProcess` and the result object of the promise are `null` in this case. 197 | 198 | ### options.ignoreError (default: `false`) 199 | 200 | If truthy, a non-zero exit code doesn't reject the promise so you can continue to the next steps. 201 | 202 | ### options.captureOutput (default: `false`) 203 | 204 | If truthy, `stdout` of the child process is captured as a string in `res.stdout`. If falsy, it is printed to the parent's `stdout`. If you specify a function, it gets called like `func(buf)` with `this` set to the options object. `buf` is an instance of `Buffer` containing the captured content. The return value from this function is set to `res.stdout`. You can use this feature for advanced use cases such as handling custom character encoding or parsing JSON. 205 | 206 | ```js 207 | var shell = require("pshell"); 208 | 209 | shell('node -e "console.log(JSON.stringify({a:1,b:2}))"', { 210 | echoCommand: false, 211 | captureOutput: function(buf) { 212 | return JSON.parse(buf.toString()); 213 | } 214 | }).then(function(res) { 215 | console.log("type:", typeof res.stdout); 216 | console.log("data:", res.stdout); 217 | }); 218 | 219 | /****** console output ******* 220 | type: object 221 | data: { a: 1, b: 2 } 222 | ******************************/ 223 | ``` 224 | 225 | ### options.captureError (default: `false`) 226 | 227 | If truthy, 'stderr' of the child process captured as a string in `res.stderr`. You can specify a function as in `captureOutput` options. 228 | 229 | ### options.normalizeText (default: `true`) 230 | 231 | If truthy, the end of line characters in a captured string are normalized to `"\n"`. Only used when capturing to a string is enabled (by `options.captureOutput` and/or `options.captureError`). 232 | 233 | ### options.env 234 | 235 | An object containing key-value pairs of environment variables to set, in addition to the default `process.env`. Each `env` object in context chain is merged together to form a combined object and that object is given to `shell.env()` function to construct a final raw object, combined with `process.env` for Node's underlying API. 236 | 237 | ```js 238 | var shell = require("pshell"); 239 | 240 | shell("tape **/*.js", { 241 | env: { 242 | PATH: ["node_modules/.bin", process.env.PATH] 243 | } 244 | }); 245 | ``` 246 | 247 | This example effectively inserts `"node_modules/.bin"` at the beginning of `PATH` while passing down other values of `process.env` intact to the child process. If you specify an array as a value of an environment variable, all elements are joined into a string, separated with a path delimiter (':' on Unix, ';' on Windows). 248 | 249 | Also, `options.env` does one more important job automatically for you. Because environment variable names are case insensitive on Windows, having multiple variables that are only different in case causes a problem. For example, simply cloning `process.env` and setting `PATH` will create a new key `PATH` in additon to an existing key `Path` on Windows because `Path` is the default key name. `options.env` prevents this from happening by removing matching keys regardless of case before setting a new value. 250 | 251 | ### options.rawEnv 252 | 253 | If you don't want the automatic features of `env`, you can specify a raw object in `options.rawEnv`. If `options.rawEnv` is specified, `options.env` is ignored and `options.rawEnv` is given to the child process as an entire set of environment variables. 254 | 255 | ### options.inputContent (default: `null`) 256 | 257 | You can specify a string or an instance of `Buffer` to supply the input data to `stdin` of the child process. 258 | 259 | ### Node's `child_process.spawn()` options 260 | 261 | In addition to the proprietary options above, the following options of [`child_process.spawn()`](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) also work. 262 | 263 | - cwd 264 | - arg0 265 | - stdio (if specified, `inputContent`, `captureOutput` and `captureError` options are ignored) 266 | - detached 267 | - uid 268 | - gid 269 | 270 | # Result object 271 | 272 | This objects is given as a value when the promise returned by one of execution functions is resolved. 273 | 274 | ### res.code (number) 275 | 276 | The exit code of child process. 277 | 278 | ### res.stdout (string or any) 279 | 280 | The captured `stdout` of child process. This field exists only when `options.captureOutput` is truthy. This is a string value by default but you can override its behavior with your own custom handler. 281 | 282 | ### res.stderr (string or any) 283 | 284 | The captured `stderr` of child process. This field exists only when `options.captureError` is truthy. This is a string value by default but you can override its behavior with your own custom handler. 285 | 286 | # Develop & contribute 287 | 288 | ## Setup 289 | 290 | ```sh 291 | git clone https://github.com/asyncmax/pshell 292 | cd pshell 293 | npm install 294 | npm run lint # run lint 295 | npm test # run test 296 | ``` 297 | 298 | ## Coding style 299 | 300 | - Use two spaces for indentation. 301 | - Use double quotes for strings. 302 | - Use ES5 syntax for lib & test. 303 | 304 | # License 305 | 306 | MIT 307 | -------------------------------------------------------------------------------- /examples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6 4 | }, 5 | "env": { 6 | "node": true 7 | }, 8 | "rules": { 9 | "indent": ["error", 2], 10 | "no-undef": ["error"], 11 | "quotes": ["error", "double", {"avoidEscape": true}], 12 | "semi": ["error", "always"], 13 | "strict": ["error", "never"] 14 | } 15 | } -------------------------------------------------------------------------------- /examples/1.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell"); 2 | 3 | shell("node --version").then(res => { 4 | console.log("exit code:", res.code); 5 | }); 6 | 7 | /****** console output ******* 8 | /bin/sh -c node --version 9 | v4.5.0 10 | exit code: 0 11 | ******************************/ -------------------------------------------------------------------------------- /examples/2.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell"); 2 | 3 | shell("node --version", {echoCommand: false}).then(res => { 4 | console.log("exit code:", res.code); 5 | }); 6 | 7 | /****** console output ******* 8 | v4.5.0 9 | exit code: 0 10 | ******************************/ 11 | -------------------------------------------------------------------------------- /examples/3.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell"); 2 | 3 | shell("node --version", {echoCommand: false, captureOutput: true}).then(res => { 4 | console.log("stdout:", JSON.stringify(res.stdout)); 5 | console.log("exit code:", res.code); 6 | }); 7 | 8 | /****** console output ******* 9 | stdout: "v4.5.0\n" 10 | exit code: 0 11 | ******************************/ 12 | -------------------------------------------------------------------------------- /examples/4.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell"); 2 | 3 | shell.options.echoCommand = false; 4 | shell.options.captureOutput = true; 5 | 6 | Promise.all([ 7 | shell("node --version"), 8 | shell("npm --version") 9 | ]).then(res => { 10 | process.stdout.write("Node version: " + res[0].stdout); 11 | process.stdout.write("NPM version: " + res[1].stdout); 12 | }); 13 | 14 | /****** console output ******* 15 | Node version: v4.5.0 16 | NPM version: 3.10.6 17 | ******************************/ 18 | -------------------------------------------------------------------------------- /examples/5.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell").context({echoCommand: false, captureOutput: true 2 | }); 3 | 4 | Promise.all([ 5 | shell("node --version"), 6 | shell("npm --version") 7 | ]).then(res => { 8 | process.stdout.write("Node version: " + res[0].stdout); 9 | process.stdout.write("NPM version: " + res[1].stdout); 10 | }); 11 | 12 | /****** console output ******* 13 | Node version: v4.5.0 14 | NPM version: 3.10.6 15 | ******************************/ 16 | -------------------------------------------------------------------------------- /examples/6.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell").context({echoCommand: false}); 2 | 3 | shell('node -e "process.exit(1)"').then(res => { 4 | console.log("exit code:", res.code); 5 | }).catch(err => { 6 | console.error("error occurred:", err.message); 7 | }); 8 | 9 | /****** console output ******* 10 | error occurred: [Error: Process 26546 exited with code 1] 11 | ******************************/ 12 | -------------------------------------------------------------------------------- /examples/7.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell").context({echoCommand: false, ignoreError: true}); 2 | 3 | shell('node -e "process.exit(1)"').then(res => { 4 | console.log("exit code:", res.code); 5 | }).catch(err => { 6 | console.error("error occurred:", err.message); 7 | }); 8 | 9 | /****** console output ******* 10 | exit code: 1 11 | ******************************/ 12 | -------------------------------------------------------------------------------- /examples/8.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell").context({echoCommand: false}); 2 | 3 | var ret = shell.spawn("node", ["--version"], {captureOutput: true}); 4 | 5 | console.log("Node process ID:", ret.childProcess.pid); 6 | 7 | ret.promise.then(function(res) { 8 | console.log("stdout:", JSON.stringify(res.stdout)); 9 | }); 10 | 11 | /****** console output ******* 12 | Node process ID: 16372 13 | stdout: "v4.5.0\n" 14 | ******************************/ 15 | -------------------------------------------------------------------------------- /examples/9.js: -------------------------------------------------------------------------------- 1 | var shell = require("pshell"); 2 | 3 | shell('node -e "console.log(JSON.stringify({a:1,b:2}))"', { 4 | echoCommand: false, 5 | captureOutput: function(buf) { 6 | return JSON.parse(buf.toString()); 7 | } 8 | }).then(function(res) { 9 | console.log("type:", typeof res.stdout); 10 | console.log("data:", res.stdout); 11 | }); 12 | 13 | /****** console output ******* 14 | type: object 15 | data: { a: 1, b: 2 } 16 | ******************************/ 17 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "pshell": "*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var npath = require("path"); 4 | var nspawn = require("child_process").spawn; 5 | 6 | function _spawn(exec, args, options) { 7 | var P = options.Promise || Promise; 8 | 9 | if (options.echoCommand) { 10 | if (typeof options.echoCommand === "function") { 11 | if (options.echoCommand(exec, args) === false) { 12 | return { 13 | childProcess: null, 14 | promise: P.resolve(null) 15 | }; 16 | } 17 | } else { 18 | console.log(exec, args.join(" ")); 19 | } 20 | } 21 | 22 | // Specifying `stdio` skips special stdio handling and handover the option 23 | // to underlying Node's `spawn` directly. 24 | var handleStdio; 25 | 26 | if (!options.stdio) { 27 | handleStdio = true; 28 | options.stdio = [ 29 | options.inputContent ? "pipe" : "inherit", 30 | options.captureOutput ? "pipe" : "inherit", 31 | options.captureError ? "pipe" : "inherit" 32 | ]; 33 | } 34 | 35 | if (options.rawEnv) 36 | options.env = options.rawEnv; 37 | else if (options.env) 38 | options.env = _env(options.env); 39 | 40 | var child = nspawn(exec, args, options); 41 | 42 | var promise = new P(function(resolve, reject) { 43 | function _stream(s, format) { 44 | return new P(function(resolve, reject) { 45 | var chunks = []; 46 | s.on("data", function(chunk) { 47 | chunks.push(chunk); 48 | }).on("end", function() { 49 | chunks = Buffer.concat(chunks); 50 | if (typeof format === "function") { 51 | chunks = format(chunks); 52 | } else { 53 | chunks = chunks.toString(); 54 | if (options.normalizeText) { 55 | if (typeof options.normalizeText === "function") 56 | chunks = options.normalizeText(chunks); 57 | else 58 | chunks = chunks.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); 59 | } 60 | } 61 | resolve(chunks); 62 | }).once("error", function(err) { 63 | reject(err); 64 | }); 65 | }); 66 | } 67 | 68 | function _capture() { 69 | var res = {}; 70 | var promises = []; 71 | if (options.captureOutput) { 72 | promises.push(_stream(child.stdout, options.captureOutput).then(function(data) { 73 | res.stdout = data; 74 | })); 75 | } 76 | if (options.captureError) { 77 | promises.push(_stream(child.stderr, options.captureError).then(function(data) { 78 | res.stderr = data; 79 | })); 80 | } 81 | res.promise = P.all(promises); 82 | return res; 83 | } 84 | 85 | var capture; 86 | 87 | if (handleStdio) { 88 | if (options.captureOutput || options.captureError) 89 | capture = _capture(); 90 | if (options.inputContent) 91 | child.stdin.end(options.inputContent); 92 | } 93 | 94 | child.once("exit", function(code, signal) { 95 | if (!options.ignoreError && (code || signal)) { 96 | var msg = code ? ("exited with code " + code) : ("was terminated by signal " + signal); 97 | var err = Error("Process " + child.pid + " " + msg); 98 | reject(err); 99 | } else { 100 | var res = { 101 | code: code, 102 | signal: signal, 103 | }; 104 | if (capture) { 105 | capture.promise.then(function() { 106 | res.stdout = capture.stdout; 107 | res.stderr = capture.stderr; 108 | resolve(res); 109 | }).catch(reject); 110 | } else { 111 | resolve(res); 112 | } 113 | } 114 | }).once("error", function(err) { 115 | reject(err); 116 | }); 117 | }); 118 | 119 | return { 120 | childProcess: child, 121 | promise: promise 122 | }; 123 | } 124 | 125 | function _shell(command, options) { 126 | var sh, sw, args; 127 | 128 | if (process.platform === "win32") { 129 | sh = options.shellName || process.env.comspec || "cmd.exe"; 130 | sw = options.shellSwitch || ["/s", "/c"]; 131 | args = sw.concat('"' + command + '"'); 132 | options.windowsVerbatimArguments = true; 133 | } else { 134 | sh = options.shellName || "/bin/sh"; 135 | sw = options.shellSwitch || ["-c"]; 136 | args = sw.concat(command); 137 | } 138 | 139 | return _spawn(sh, args, options); 140 | } 141 | 142 | function _assign(assigner, des) { 143 | var len = arguments.length; 144 | var idx, src, key, val; 145 | 146 | for (idx = 2; idx < len; idx++) { 147 | src = arguments[idx]; 148 | for (key in src) { 149 | if (!src.hasOwnProperty(key)) 150 | continue; 151 | val = src[key]; 152 | if (assigner) 153 | assigner(des, key, val); 154 | else 155 | des[key] = val; 156 | } 157 | } 158 | 159 | return des; 160 | } 161 | 162 | function _optionAssigner(des, key, val) { 163 | if (key === "env" && des[key]) 164 | _assign(null, des[key], val); 165 | else 166 | des[key] = val; 167 | } 168 | 169 | function _merge() { 170 | var args = Array.prototype.slice.call(arguments); 171 | return _assign.apply(null, [_optionAssigner].concat(args)); 172 | } 173 | 174 | function _envAssigner(des, key, val) { 175 | function _clean(key) { 176 | key = key.toLowerCase(); 177 | for (var name in des) { 178 | if (!des.hasOwnProperty(name)) 179 | continue; 180 | if (name.toLowerCase() === key) 181 | delete des[name]; 182 | } 183 | } 184 | 185 | if (Array.isArray(val)) 186 | val = val.join(npath.delimiter); 187 | 188 | if (process.platform === "win32") 189 | _clean(key); 190 | 191 | des[key] = val; 192 | } 193 | 194 | function _env(env) { 195 | return _assign(_envAssigner, {}, process.env, env); 196 | } 197 | 198 | function _context(baseOptions) { 199 | function shell(command, options) { 200 | options = _merge({}, baseOptions, options); 201 | return _shell(command, options).promise; 202 | } 203 | 204 | // `spawn(exec, options)` format is not supported 205 | function spawn(exec, args, options) { 206 | options = _merge({}, baseOptions, options); 207 | return _spawn(exec, args || [], options); 208 | } 209 | 210 | function exec(command, options) { 211 | options = _merge({}, baseOptions, options); 212 | return _shell(command, options); 213 | } 214 | 215 | shell.options = baseOptions = baseOptions || {}; 216 | shell.context = function(options) { 217 | return _context(_merge({}, baseOptions, options)); 218 | }; 219 | shell.spawn = spawn; 220 | shell.exec = exec; 221 | shell.env = _env; 222 | 223 | return shell; 224 | } 225 | 226 | module.exports = _context({ 227 | Promise: null, 228 | echoCommand: true, // boolean or function 229 | ignoreError: false, // boolean 230 | shellName: null, // string 231 | shellSwitch: null, // null or array of string 232 | inputContent: null, // null, Buffer or string 233 | captureOutput: false, // boolean or function 234 | captureError: false, // boolean or function 235 | normalizeText: true // boolean or function 236 | 237 | // In addition to the above, Node's `spawn` options are also supported: 238 | // cwd, env, arg0, stdio, detached, uid, gid 239 | }); 240 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pshell", 3 | "version": "1.1.0", 4 | "description": "Promise-based portable interface for running shell commands", 5 | "author": "Max Jung ", 6 | "license": "MIT", 7 | "repository": "https://github.com/asyncmax/pshell", 8 | "main": "lib/index.js", 9 | "scripts": { 10 | "lint": "eslint lib/*.js test/**/*.js examples/*.js", 11 | "tape": "tape test/*.js", 12 | "test": "npm run lint && npm run tape" 13 | }, 14 | "devDependencies": { 15 | "es6-promise": "^3.3.1", 16 | "eslint": "^3.5.0", 17 | "tape": "^4.6.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixture/cat.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | process.stdin.pipe(process.stdout); 4 | 5 | console.error("stderr test"); -------------------------------------------------------------------------------- /test/fixture/env.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | console.log(process.env.MY_ENV_VAR); 4 | -------------------------------------------------------------------------------- /test/fixture/env2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | console.log(process.env.MY_ENV_VAR1); 4 | console.log(process.env.MY_ENV_VAR2); 5 | console.log(process.env.MY_ENV_VAR3); 6 | 7 | if (Object.keys(process.env).length <= 3) 8 | process.exit(1); 9 | -------------------------------------------------------------------------------- /test/fixture/error.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | process.exit(33); 4 | -------------------------------------------------------------------------------- /test/fixture/json.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var DATA = { 4 | type: "message", 5 | message: [ 6 | "Hello", 7 | "world!" 8 | ] 9 | }; 10 | 11 | console.log(JSON.stringify(DATA)); -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var npath = require("path"); 4 | var test = require("tape"); 5 | var shell = require("../lib"); 6 | 7 | shell.options.echoCommand = false; 8 | 9 | var CAT = "node " + npath.join(__dirname, "fixture/cat.js"); 10 | var ERROR = "node " + npath.join(__dirname, "fixture/error.js"); 11 | var JSONJS = "node " + npath.join(__dirname, "fixture/json.js"); 12 | var ENV = "node " + npath.join(__dirname, "fixture/env.js"); 13 | var ENV2 = "node " + npath.join(__dirname, "fixture/env2.js"); 14 | 15 | test("basic echo test", function(t) { 16 | shell("echo test").then(function(res) { 17 | t.equal(res.code, 0); 18 | t.ok(!res.stdout); 19 | t.ok(!res.stderr); 20 | }).then(t.end, t.end); 21 | }); 22 | 23 | test("stdout capture", function(t) { 24 | shell("echo stdout test", {captureOutput: true}).then(function(res) { 25 | t.equal(res.code, 0); 26 | t.equal(res.stdout, "stdout test\n"); 27 | t.ok(!res.stderr); 28 | }).then(t.end, t.end); 29 | }); 30 | 31 | test("stdout as JSON", function(t) { 32 | shell(JSONJS, {captureOutput: function(buf) { 33 | t.ok(Buffer.isBuffer(buf)); 34 | return JSON.parse(buf.toString()); 35 | }}).then(function(res) { 36 | t.equal(typeof res.stdout, "object"); 37 | t.deepEqual(res.stdout, { 38 | type: "message", 39 | message: [ 40 | "Hello", 41 | "world!" 42 | ] 43 | }); 44 | }).then(t.end, t.end); 45 | }); 46 | 47 | test("supplying stdin", function(t) { 48 | shell(CAT, {inputContent: "Hello!", captureOutput: true}).then(function(res) { 49 | t.equal(res.code, 0); 50 | t.equal(res.stdout, "Hello!"); 51 | t.ok(!res.stderr); 52 | }).then(t.end, t.end); 53 | }); 54 | 55 | test("stderr capture", function(t) { 56 | shell(CAT, {inputContent: "World\n", captureOutput: true, captureError: true}).then(function(res) { 57 | t.equal(res.code, 0); 58 | t.equal(res.stdout, "World\n"); 59 | t.equal(res.stderr, "stderr test\n"); 60 | }).then(t.end, t.end); 61 | }); 62 | 63 | test("error exit rejection", function(t) { 64 | shell(ERROR).then(function() { 65 | t.fail("error exit didn't reject the promise"); 66 | }, function() { 67 | t.pass("error exit rejected the promise"); 68 | }).then(t.end, t.end); 69 | }); 70 | 71 | test("ignoreError option", function(t) { 72 | shell(ERROR, {ignoreError: true}).then(function(res) { 73 | t.equal(res.code, 33); 74 | }).then(t.end, t.end); 75 | }); 76 | 77 | test("shell.spawn", function(t) { 78 | var res = shell.spawn("node", ["--version"], {captureOutput: true}); 79 | t.ok(res.childProcess); 80 | t.ok(res.promise); 81 | res.promise.then(function(res) { 82 | t.equal(res.code, 0); 83 | t.ok(res.stdout); 84 | }).then(t.end, t.end); 85 | }); 86 | 87 | test("shell.exec", function(t) { 88 | var res = shell.exec("echo hello", {captureOutput: true}); 89 | t.ok(res.childProcess); 90 | t.ok(res.promise); 91 | res.promise.then(function(res) { 92 | t.equal(res.code, 0); 93 | t.equal(res.stdout, "hello\n"); 94 | }).then(t.end, t.end); 95 | }); 96 | 97 | test("shell.context", function(t) { 98 | var sh = shell.context({ 99 | captureOutput: true, 100 | ignoreError: true 101 | }); 102 | 103 | t.equal(sh.options.echoCommand, false, "should inherit parent's options"); 104 | 105 | sh("echo hi!").then(function(res) { 106 | t.equal(res.code, 0); 107 | t.equal(res.stdout, "hi!\n"); 108 | t.ok(!res.stderr); 109 | return sh(ERROR); 110 | }).then(function(res) { 111 | t.equal(res.code, 33); 112 | }).then(t.end, t.end); 113 | }); 114 | 115 | test("normalizeText option", function(t) { 116 | var options = {inputContent: "Hello\r\nWonderful\rWorld\n", captureOutput: true}; 117 | shell(CAT, options).then(function(res) { 118 | t.equal(res.stdout, "Hello\nWonderful\nWorld\n"); 119 | options.normalizeText = false; 120 | return shell(CAT, options); 121 | }).then(function(res) { 122 | t.equal(res.stdout, "Hello\r\nWonderful\rWorld\n"); 123 | options.normalizeText = function(text) { 124 | return "[" + text.replace(/\n|\r/g, "") + "]"; 125 | }; 126 | return shell(CAT, options); 127 | }).then(function(res) { 128 | t.equal(res.stdout, "[HelloWonderfulWorld]"); 129 | }).then(t.end, t.end); 130 | }); 131 | 132 | test("stdio option", function(t) { 133 | shell("echo hello", { 134 | stdio: ["ignore", "pipe", "inherit"], 135 | captureOutput: true, // should be ignored 136 | captureError: true // should be ignored 137 | }).then(function(res) { 138 | t.equal(res.code, 0); 139 | t.ok(!res.stdout); 140 | t.ok(!res.stderr); 141 | }).then(t.end, t.end); 142 | }); 143 | 144 | test("rawEnv option", function(t) { 145 | shell(ENV, {rawEnv: {MY_ENV_VAR: "haha"}, captureOutput: true}).then(function(res) { 146 | t.equal(res.stdout, "haha\n"); 147 | }).then(t.end, t.end); 148 | }); 149 | 150 | test("Promise option", function(t) { 151 | var P = require("es6-promise").Promise; 152 | 153 | var promise = shell("echo hello", {Promise: P, captureOutput: true}); 154 | 155 | t.equal(promise.constructor, P); 156 | 157 | promise.then(function(res) { 158 | t.equal(res.stdout, "hello\n"); 159 | }).then(t.end, t.end); 160 | }); 161 | 162 | test("echoCommand handler", function(t) { 163 | var history = []; 164 | var sh = shell.context({ 165 | echoCommand: function(exec, args) { 166 | history.push([exec, args]); 167 | return false; 168 | } 169 | }); 170 | 171 | var ret = sh.spawn("some-exec", ["--help"]); 172 | 173 | t.equal(ret.childProcess, null); 174 | 175 | t.deepEqual(history, [ 176 | ["some-exec", ["--help"]] 177 | ]); 178 | 179 | ret.promise.then(function(res) { 180 | t.equal(res, null); 181 | }).then(t.end, t.end); 182 | }); 183 | 184 | test("shell.env", function(t) { 185 | var count = Object.keys(process.env).length; 186 | 187 | if (process.env.PATH === undefined) 188 | count++; 189 | 190 | var env = shell.env({ 191 | PATH: ["MY_TEST_PATH", process.env.PATH] 192 | }); 193 | 194 | t.ok(count > 1); 195 | t.equal(Object.keys(env).length, count); 196 | t.equal(env.PATH.split(npath.delimiter)[0], "MY_TEST_PATH"); 197 | 198 | t.end(); 199 | }); 200 | 201 | test("env option", function(t) { 202 | var sh = shell.context({ 203 | env: {MY_ENV_VAR1: "hello", MY_ENV_VAR2: "hi"} 204 | }); 205 | 206 | sh(ENV2, {env: {MY_ENV_VAR2: "foo", MY_ENV_VAR3: "bar"}, captureOutput: true}).then(function(res) { 207 | t.equal(res.stdout, "hello\nfoo\nbar\n"); 208 | }).then(t.end, t.end); 209 | }); 210 | --------------------------------------------------------------------------------