├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── slowScript.sh └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | # 7 | node_modules/ 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Charles Hulcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-run-cmd 2 | 3 | # This repository is not maintained. Try using other, more mature shell command libraries. 4 | 5 | - 6 | *Node.js commandline/terminal interface.* 7 | 8 | _This package was developed on Node 5.4.0. Earlier versions of Node may not have support for the Javascript syntax of this package._ 9 | 10 | Easily run simple or sophisticated console/terminal command(s) from Node. Supports sequential and parallel execution. Returns a promise that resolves to an array of exit codes for each command run. 11 | 12 | With node-run-cmd you can execute a single command or an array of commands quite simply. 13 | 14 | If you want, set in-depth options for commands being run, including callbacks for data, errors, and completion. Also set working directory, environment variables, run execution process in detached mode, set the uid and gid for the execution process(es), and set the shell to run the command in. 15 | 16 | The source that this package is based on has been in production since February, 2016. Note that the examples here are for illustrative purposes only; most of the time there is no real need to run commands from Node, and they should be avoided if there are cross-platform requirements. This package aims to help when there isn't an agreeable alternative. 17 | 18 | If most of your commands are filesystem related, I would instead look to [node-fs-extra](https://github.com/jprichardson/node-fs-extra) to accomplish what the commands would have. 19 | 20 | ##### NPM: 21 | ![node-cmd npm version](https://img.shields.io/npm/v/node-run-cmd.svg) ![supported node version for node-cmd](https://img.shields.io/node/v/node-run-cmd.svg) ![total npm downloads for node-cmd](https://img.shields.io/npm/dt/node-run-cmd.svg) ![monthly npm downloads for node-cmd](https://img.shields.io/npm/dm/node-run-cmd.svg) ![npm licence for node-cmd](https://img.shields.io/npm/l/node-run-cmd.svg) 22 | 23 | ##### GitHub: 24 | ![node-run-cmd GitHub Release](https://img.shields.io/github/release/c-h-/node-run-cmd.svg) ![GitHub license node-run-cmd license](https://img.shields.io/github/license/c-h-/node-run-cmd.svg) ![open issues for node-run-cmd on GitHub](https://img.shields.io/github/issues/c-h-/node-run-cmd.svg) 25 | 26 | Licensed via MIT License. 27 | 28 | - 29 | 30 | ## Quick Start 31 | The quickest way to get started is to run a single, simple command, then increase complexity as needed. 32 | 33 | Install the package: 34 | ```shell 35 | $ npm install --save node-run-cmd 36 | ``` 37 | 38 | Use the package: 39 | ```javascript 40 | var nrc = require('node-run-cmd'); 41 | nrc.run('mkdir foo'); 42 | ``` 43 | 44 | ## Examples 45 | These examples get increasingly complex to demonstrate the robustness of the package. Not all options are demonstrated; see the Options Object section for all possible options. 46 | 47 | ### Simple command 48 | ```javascript 49 | nrc.run('mkdir foo'); 50 | ``` 51 | 52 | ### Aysnchronous usage 53 | #### Promise style 54 | ```javascript 55 | nrc.run('mkdir foo').then(function(exitCodes) { 56 | doSomethingElse(); 57 | }, function(err) { 58 | console.log('Command failed to run with error: ', err); 59 | }); 60 | ``` 61 | #### Callback style 62 | ```javascript 63 | var callback = function (exitCodes) { 64 | doSomethingElse(); 65 | }; 66 | nrc.run('mkdir foo', { onDone: callback } ); 67 | ``` 68 | 69 | ### Use output (stdout) from command 70 | ```javascript 71 | var dataCallback = function(data) { 72 | useData(data); 73 | }; 74 | nrc.run('ls', { onData: dataCallback }); 75 | ``` 76 | 77 | ### Use error output (stderr) from command 78 | ```javascript 79 | var errorCallback = function(data) { 80 | useErrorData(data); 81 | }; 82 | nrc.run('ls ~/does/not/exist', { onError: dataCallback }); 83 | ``` 84 | 85 | ### Use exit code from command 86 | ```javascript 87 | var doneCallback = function(code) { 88 | useCode(code); 89 | }; 90 | nrc.run('ls foo', { onDone: doneCallback }); 91 | ``` 92 | *OR* 93 | ```javascript 94 | nrc.run('ls foo').then(function(codes){ useCode(codes[0]); }); 95 | ``` 96 | 97 | ### Run multiple commands 98 | ```javascript 99 | nrc.run([ 'mkdir foo', 'touch foo/bar.txt' ]); 100 | ``` 101 | 102 | ### Set working directory for commands 103 | ```javascript 104 | var commands = [ 105 | 'mkdir foo', 106 | 'touch foo/bar.txt' 107 | ]; 108 | var options = { cwd: 'path/to/my/dir' }; 109 | nrc.run(commands, options); 110 | ``` 111 | 112 | ### Set different working directory for each command 113 | ```javascript 114 | var commands = [ 115 | { command: 'mkdir foo', cwd: 'dir1' }, 116 | { command: 'mkdir foo', cwd: 'dir2' } 117 | ]; 118 | nrc.run(commands); 119 | ``` 120 | 121 | ### Set different working directory one command and default working directory for the others 122 | ```javascript 123 | var commands = [ 124 | 'mkdir foo', 125 | { command: 'mkdir foo', cwd: 'different/dir' }, 126 | 'mkdir bar' 127 | ]; 128 | var options = { cwd: 'default/dir' }; 129 | nrc.run(commands, options); 130 | ``` 131 | 132 | ### Run commands in parallel 133 | ```javascript 134 | var commands = [ 135 | './runCompute1.sh', 136 | './runCompute2.sh', 137 | './runCompute3.sh', 138 | ]; 139 | var options = { mode: 'parallel' }; 140 | nrc.run(commands, options); 141 | ``` 142 | 143 | ## NRC Methods 144 | ### Run 145 | #### Usage: 146 | ```javsacript 147 | var promise = nrc.run(commands, globalOptions); 148 | ``` 149 | #### Returns: 150 | A promise that resolves to an array of exit codes for commands run. 151 | #### Arguments: 152 | *`commands` can be specified in any one of the below formats* 153 | 154 | | name | type | required | description | example | 155 | |------|------|----------|-------------|---------| 156 | | commands | string | yes | The command to run | `'ls'` | 157 | | commands | array(string) | yes | An array of string commands to run | `['ls', 'mkdir foo']` | 158 | | commands | array(object) | yes | An array of objects describing commands to run. See section Options Object for allowed properties. | `[{ command: 'ls' }, { command: 'mkdir foo' }]` | 159 | | commands | array(object or string) | yes | A mixed array of objects describing commands and string commands to run. See section Options Object for allowed properties. | `[{ command: 'ls' }, 'mkdir foo']` | 160 | | globalOptions | object | no | The global options to set for each command being run. Overridden by command's options. | `{ cwd: 'foo', verbose: true, logger: gutil.log }` | 161 | 162 | ## Options Object 163 | Options available for the `commands` or `globalOptions` argument: 164 | 165 | | property | type | required | default | description | 166 | |----------|------|----------|---------|-------------| 167 | | command | string | yes | - | the command to run | 168 | | cwd | string | no | `process.cwd()` | the directory to run the command in | 169 | | onData | function(data) | no | null | where to send output from stdout. Called each time stdout is written. | 170 | | onError | function(data) | no | null | where to send output from stderr. Called each time stderr is written. | 171 | | onDone | function(code) | no | null | where to send the exit code of the command. Called once. | 172 | | verbose | boolean | no | `false` | show verbose output | 173 | | logger | function(data) | no | `console.log` | what function to use to log *when verbose is set to true* | 174 | | env | object | no | null | Environment key-value pairs 175 | | stdio | string or array | no | null | Child's stdio configuration 176 | | detached | boolean | no | null | Prepare child to run independently of its parent process. Specific behavior depends on the platform. | 177 | | uid | number | no | null | Sets the user identity of the process. | 178 | | gid | number | no | null | Sets the group identity of the process. | 179 | | shell | boolean or string | no | null | If true, runs command inside of a shell. Uses '/bin/sh' on UNIX, and 'cmd.exe' on Windows. A different shell can be specified as a string. The shell should understand the -c switch on UNIX, or /s /c on Windows. Defaults to false (no shell). | 180 | 181 | ### Global-Only Options 182 | These options can only be set in the `globalOptions` argument. 183 | 184 | | property | type | required | default | description | 185 | |----------|------|----------|---------|-------------| 186 | | mode | string | no | `'sequential'` | Whether to run commands in series (sequentially) or in parallel. | 187 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const spawn = require('child_process').spawn; 3 | 4 | const SEQUENTIAL = 'sequential'; 5 | const PARALLEL = 'parallel'; 6 | const DEFAULT_VERBOSE = false; 7 | 8 | /* 9 | * Runs a cordova command with supplied options 10 | * @param {Object} options - an options object 11 | * @param {String} options.command - a command to run 12 | * @param {Boolean} [options.verbose] - whether to print stuff to console 13 | * @param {Boolean} [options.logger] - the logging function to call if verbose is true (default console.log) 14 | * @param {String} [options.cwd] - the working directory 15 | * @param {Function} [options.onData(data)] - function to call when data is written 16 | * @param {Function} [options.onError(data)] - function to call when err is written 17 | * @param {Function} [options.onDone(exitCode)] - function to call when cmd is done 18 | * @param {Object} [options.env] - Environment key-value pairs 19 | * @param {String|Array} [options.stdio] - Child's stdio configuration 20 | * @param {Boolean} [options.detached] - Prepare child to run independently of its parent process. Specific behavior depends on the platform. 21 | * @param {Number} [options.uid] - Sets the user identity of the process. 22 | * @param {Number} [options.gid] - Sets the group identity of the process. 23 | * @param {Boolean|String} [options.shell] - If true, runs command inside of a shell. Uses '/bin/sh' on UNIX, and 'cmd.exe' on Windows. A different shell can be specified as a string. The shell should understand the -c switch on UNIX, or /s /c on Windows. Defaults to false (no shell). 24 | * @returns {Promise} a promise resolved or rejected by the command result 25 | */ 26 | function run(options){ 27 | return new Promise((resolve, reject) => { 28 | if (options.verbose) { 29 | options.logger('$ ' + options.command); 30 | } 31 | 32 | // get the options allowed to be passed to the child process 33 | const spawnOptsAllowedKeys = [ 34 | 'cwd', 35 | 'env', 36 | 'stdio', 37 | 'detached', 38 | 'uid', 39 | 'gid', 40 | 'shell', 41 | ]; 42 | const spawnProcessOpts = {}; 43 | for (const key in options) { 44 | if (spawnOptsAllowedKeys.indexOf(key) > -1) { 45 | spawnProcessOpts[key] = options[key]; 46 | } 47 | } 48 | 49 | // spawn the process to run the command 50 | // below splits the command into pieces to pass to the process; mapping function simply removes quotes from each piece 51 | const cmds = options.command.match(/[^"\s]+|"(?:\\"|[^"])+"/g) 52 | .map(expr => { 53 | return expr.charAt(0) === '"' && expr.charAt(expr.length - 1) === '"' ? expr.slice(1, -1) : expr; 54 | }); 55 | const runCMD = cmds[0]; 56 | cmds.shift(); 57 | const child = spawn(runCMD, cmds, spawnProcessOpts); 58 | 59 | // set stdout listener 60 | child.stdout.on('data', data => { 61 | if (data) { 62 | if (options.verbose) { 63 | options.logger(data.toString()); 64 | } 65 | if (typeof options.onData === 'function') { 66 | options.onData(data.toString()); 67 | } 68 | } 69 | }); 70 | 71 | // set stderr listener 72 | child.stderr.on('data', data => { 73 | if (data) { 74 | if (options.verbose) { 75 | options.logger(data.toString()); 76 | } 77 | if (typeof options.onError === 'function') { 78 | options.onError(data.toString()); 79 | } 80 | } 81 | }); 82 | 83 | // set close listener 84 | child.on('close', code => { 85 | if (typeof options.onDone === 'function') { 86 | options.onDone(code); 87 | } 88 | resolve(code); // resolve all, let caller handle 89 | // code !== 0 ? reject(code) : resolve(code); 90 | }); 91 | 92 | // set error listener 93 | child.on('error', code => { 94 | if (typeof options.onDone === 'function') { 95 | options.onDone(code, true); 96 | } 97 | resolve(code); // resolve all, let caller handle 98 | }); 99 | }); 100 | } 101 | 102 | /** 103 | * Returns a function that resolves any yielded promises inside the generator. 104 | * @param {Function} makeGenerator - the function to turn into an async generator/promise 105 | * @returns {Function} the function that will iterate over interior promises on each call when invoked 106 | */ 107 | function async(makeGenerator) { 108 | return () => { 109 | const generator = makeGenerator(...arguments); 110 | 111 | function handle(result) { 112 | // result => { done: [Boolean], value: [Object] } 113 | if (result.done) { 114 | return Promise.resolve(result.value); 115 | } 116 | 117 | return Promise.resolve(result.value).then(res => { 118 | return handle(generator.next(res)); 119 | }, err => { 120 | return handle(generator.throw(err)); 121 | }); 122 | } 123 | 124 | try { 125 | return handle(generator.next()); 126 | } catch (ex) { 127 | return Promise.reject(ex); 128 | } 129 | }; 130 | } 131 | 132 | 133 | /** 134 | * Run multiple commands. 135 | * @param {Array} commands - an array of objects or strings containing details of commands to run 136 | * @param {Object} options - an object containing global options for each command 137 | * @example 138 | runMultiple(['mkdir test', 'cd test', 'ls']); 139 | * @example 140 | runMultiple(['mkdir test', 'cd test', 'ls'], { cwd: '../myCustomWorkingDirectory' }); 141 | * @example 142 | const commandsToRun = [ 143 | { command: 'ls', onData: function(data) { console.log(data) } }, 144 | // override globalOptions working directory with each commands' options 145 | { command: 'mkdir test', cwd: '../../customCwdNumber2', onDone: function() { console.log('done mkdir!') } }, 146 | 'ls ~/Desktop' // finish up with simple string command 147 | ]; 148 | const globalOptions = { cwd: '../myCustomWorkingDirectory' }; 149 | runMultiple(commandsToRun, globalOptions); 150 | */ 151 | function runMultiple(input, options) { 152 | return new Promise((resolve, reject) => { 153 | let commands = input; 154 | // set default options 155 | const defaultOpts = { 156 | cwd: process.cwd(), 157 | verbose: DEFAULT_VERBOSE, 158 | mode: SEQUENTIAL, 159 | logger: console.log, 160 | }; 161 | 162 | // set global options 163 | const globalOpts = Object.assign({}, defaultOpts, options); 164 | 165 | // resolve string to proper input type 166 | if (typeof commands === 'string') { 167 | commands = [{ command: commands }]; 168 | } 169 | 170 | // start execution 171 | if (commands && typeof commands === 'object') { 172 | if (Object.prototype.toString.call(commands) !== '[object Array]') { 173 | // not array 174 | commands = [commands]; 175 | } 176 | else { 177 | // is array, check type of children 178 | commands = commands.map(cmd => typeof cmd === 'object' ? cmd : { command: cmd }); 179 | } 180 | 181 | // run commands in parallel 182 | if (globalOpts.mode === PARALLEL) { 183 | const promises = commands.map(cmd => { 184 | const resolvedOpts = Object.assign({}, globalOpts, cmd); 185 | return run(resolvedOpts); 186 | }); 187 | Promise.all(promises).then(resolve, reject); 188 | } 189 | else { // run commands in sequence (default) 190 | async(function* () { 191 | try { 192 | const results = []; 193 | for (const i in commands) { 194 | const cmd = commands[i]; 195 | const resolvedOpts = Object.assign({}, globalOpts, cmd); 196 | const result = yield run(resolvedOpts); 197 | results.push(result); 198 | } 199 | if (results) { 200 | resolve(results); 201 | } 202 | else { 203 | reject('Falsy value in results'); 204 | } 205 | } 206 | catch (e) { 207 | reject(e); 208 | } 209 | })(); 210 | } 211 | } 212 | else { 213 | reject('Invalid input'); 214 | } 215 | }); 216 | } 217 | 218 | 219 | const command = { 220 | run: runMultiple, 221 | }; 222 | 223 | module.exports = command; 224 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-run-cmd", 3 | "version": "1.0.1", 4 | "description": "Easily run console/terminal command(s) from Node", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/c-h-/node-run-cmd.git" 12 | }, 13 | "keywords": [ 14 | "node", 15 | "console", 16 | "terminal", 17 | "bash", 18 | "command", 19 | "shell" 20 | ], 21 | "author": "Charlie Hulcher", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/c-h-/node-run-cmd/issues" 25 | }, 26 | "homepage": "https://github.com/c-h-/node-run-cmd#readme", 27 | "devDependencies": { 28 | "chai": "^3.5.0", 29 | "chai-as-promised": "^5.3.0", 30 | "mocha": "^2.5.2", 31 | "sinon": "^1.17.4", 32 | "sinon-chai": "^2.8.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/slowScript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sleep 1; 4 | echo 1; 5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const chaiAsPromised = require('chai-as-promised'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require("sinon-chai"); 7 | const command = require('../index'); 8 | 9 | chai.should(); 10 | chai.use(chaiAsPromised); 11 | chai.use(sinonChai); 12 | 13 | // written to run on OS X 14 | 15 | const SUCCESS = 0; 16 | 17 | describe('run', function() { 18 | it('should not accept a number argument', function() { 19 | const cmds = 6; 20 | return command.run(cmds).should.not.be.fulfilled; 21 | }); 22 | it('should accept a single string argument', function() { 23 | const cmds = 'ls'; 24 | command.run(cmds).then(function(result) { 25 | done(); 26 | }, 27 | function(err) { 28 | done(err); 29 | }); 30 | }); 31 | it('should accept an array of string arguments', function() { 32 | const cmds = ['ls', 'ls ~/Desktop']; 33 | const promise = command.run(cmds); 34 | return promise.should.be.fulfilled; 35 | }); 36 | it('should accept an array of string and object arguments', function() { 37 | const cmds = ['ls', { command: 'ls ~/Desktop' }]; 38 | return command.run(cmds).should.be.fulfilled; 39 | }); 40 | it('should accept an array of object arguments', function() { 41 | const cmds = [{ command: 'ls' }, { command: 'ls ~/Desktop' }]; 42 | return command.run(cmds).should.be.fulfilled; 43 | }); 44 | it('should accept a single object argument', function() { 45 | const cmds = { command: 'ls' }; 46 | return command.run(cmds).should.be.fulfilled; 47 | }); 48 | 49 | it('should run commands sequentially by default', function() { 50 | const progressSpy = sinon.spy(); 51 | const output = []; 52 | const cmds = [ 53 | { 54 | command: './test/slowScript.sh', 55 | onData: function(data){ output.push(data) }, 56 | }, 57 | { 58 | command: 'echo 2', 59 | onData: function(data){ output.push(data) }, 60 | }, 61 | ]; 62 | return command.run(cmds).then(function(results) { 63 | output.should.deep.equal([ '1\n', '2\n' ]); 64 | }); 65 | }); 66 | it('should run commands in parallel', function() { 67 | const progressSpy = sinon.spy(); 68 | const output = []; 69 | const cmds = [ 70 | { 71 | command: './test/slowScript.sh', 72 | onData: function(data){ output.push(data) }, 73 | }, 74 | { 75 | command: 'echo 2', 76 | onData: function(data){ output.push(data) }, 77 | }, 78 | ]; 79 | return command.run(cmds, { mode: 'parallel' }).then(function(results) { 80 | output.should.deep.equal([ '2\n', '1\n' ]); 81 | }); 82 | }); 83 | }); 84 | --------------------------------------------------------------------------------