├── .gitignore ├── History.md ├── README.md ├── index.js ├── package.json └── test ├── index.coffee └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ### v3.0.2 2 | - [#60](https://github.com/keithamus/parallelshell/pulls/60) < Node 8 fix 3 | - [#33](https://github.com/keithamus/parallelshell/issues/33) Readme fix 4 | ([@darkguy2008](https://github.com/darkguy2008)) 5 | 6 | ### v3.0.1 7 | - [#58](https://github.com/keithamus/parallelshell/issues/58) Fix CRLF 8 | problem not allowing parallelshell to start up (shame on me!). 9 | ([@darkguy2008](https://github.com/darkguy2008)) 10 | 11 | ### v3.0.0 12 | - [#56](https://github.com/keithamus/parallelshell/issues/56) 13 | [#57](https://github.com/keithamus/parallelshell/issues/57) Fix problem 14 | with node >= 8 where it says that cwd is not a string. 15 | ([@darkguy2008](https://github.com/darkguy2008)) 16 | 17 | ### v1.1.1 18 | 19 | - [#11](https://github.com/keithamus/parallelshell/pull/11) Fix regression 20 | where shebang went missing. Fixes issues with Mac finding binary. 21 | ([@eliias](https://github.com/eliias)) 22 | 23 | ### v1.1.0 24 | 25 | - [#9](https://github.com/keithamus/parallelshell/pull/9) Add shell colour 26 | support (passing through colours from child shells). (Fixes #7) 27 | ([@paulpflug](https://github.com/paulpflug)) 28 | - [#8](https://github.com/keithamus/parallelshell/pull/8) Fix examples in 29 | README to better support Windows. 30 | ([@jeffcharles](https://github.com/jeffcharles)) 31 | 32 | ### v1.0.4 33 | 34 | - [#4](https://github.com/keithamus/parallelshell/pull/4) Ensure app doesn't 35 | crash if the exit code isn't present (on some windows machines) 36 | ([@jackysee](https://github.com/jackysee)) 37 | 38 | ### v1.0.3 39 | 40 | Fix deploy issues 41 | 42 | ### v1.0.2 43 | 44 | Fix deploy issues 45 | 46 | ### v1.0.1 47 | 48 | Fix deploy issues 49 | 50 | ### v1.0.0 51 | 52 | Initial Release 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Parallel Shell 2 | 3 | This is a super simple npm module to run shell commands in parallel. All 4 | processes will share the same stdout/stderr, and if any command exits with a 5 | non-zero exit status, the rest are stopped and the exit code carries through. 6 | 7 | ### Version compatibility notes 8 | 9 | * Fully compatible with Node up to v8 and later! 10 | 11 | ### Maintenance has been resumed by [@darkguy2008](https://github.com/darkguy2008). However, there are also better options, see [Consolidation of multiple similar libraries](https://github.com/mysticatea/npm-run-all/issues/10). 12 | 13 | ### Motivation 14 | 15 | **How is this different than:** 16 | 17 | $ cmd1 & cmd2 & cmd3 18 | 19 | * Cross platform -- works on Unix or Windows. 20 | 21 | * `&` creates a background process, which only exits if you kill it or it ends. `parallelshell` will autokill processes if one of the others dies. 22 | 23 | * `command1 & command2 & command3` will wait in the terminal until command3 ends only. parallelshell will wait until all 3 end. 24 | 25 | * If command1 or command2 exit with non-zero exit code, then this will not effect the outcome of your shell (i.e. they can fail and npm/bash/whatever will ignore it). `parallelshell` will not ignore it, and will exit with the first non-zero exit code. 26 | 27 | * Pressing Ctrl+C will exit command3 but not 1 or 2. `parallelshell` will exit all 3 28 | 29 | * `parallelshell` outputs all jobs stdout/err to its stdout/err. background jobs do that... kind of coincidentally (read: unreliably) 30 | 31 | **So what's the difference between GNU parallel and this?** 32 | 33 | The biggest difference is that parallelshell is an npm module and GNU parallel isn't. While they probably do similar things, albeit (GNU) parallel being more advanced, parallelshell is an easier option to work with when using npm (because it's an npm module). 34 | 35 | If you have GNU parallel installed on all the machines you project will be on, then by all means use it! :) 36 | 37 | ### Install 38 | 39 | Simply run the following to install this to your project: 40 | 41 | ```bash 42 | npm i --save-dev parallelshell 43 | ``` 44 | 45 | Or, to install it globally, run: 46 | 47 | ```bash 48 | npm i -g parallelshell 49 | ``` 50 | 51 | ### Usage 52 | 53 | To use the command, simply call it with a set of strings - which correspond to 54 | shell arguments, for example: 55 | 56 | ```bash 57 | parallelshell "echo 1" "echo 2" "echo 3" 58 | ``` 59 | 60 | This will execute the commands `echo 1` `echo 2` and `echo 3` simultaneously. 61 | 62 | Note that on Windows, you need to use double-quotes to avoid confusing the 63 | argument parser. 64 | 65 | Available options: 66 | ``` 67 | -h, --help output usage information 68 | -v, --verbose verbose logging 69 | -w, --wait will not close sibling processes on error 70 | 71 | ``` 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | var spawn = require('child_process').spawn; 5 | 6 | var sh, shFlag, children, args, wait, cmds, verbose, i ,len; 7 | // parsing argv 8 | cmds = []; 9 | args = process.argv.slice(2); 10 | for (i = 0, len = args.length; i < len; i++) { 11 | if (args[i][0] === '-') { 12 | switch (args[i]) { 13 | case '-w': 14 | case '--wait': 15 | wait = true; 16 | break; 17 | case '-v': 18 | case '--verbose': 19 | verbose = true; 20 | break; 21 | case '-h': 22 | case '--help': 23 | console.log('-h, --help output usage information'); 24 | console.log('-v, --verbose verbose logging') 25 | console.log('-w, --wait will not close sibling processes on error') 26 | process.exit(); 27 | break; 28 | } 29 | } else { 30 | cmds.push(args[i]); 31 | } 32 | } 33 | 34 | // called on close of a child process 35 | function childClose (code) { 36 | var i, len; 37 | code = code ? (code.code || code) : code; 38 | if (verbose) { 39 | if (code > 0) { 40 | console.error('`' + this.cmd + '` failed with exit code ' + code); 41 | } else { 42 | console.log('`' + this.cmd + '` ended successfully'); 43 | } 44 | } 45 | if (code > 0 && !wait) close(code); 46 | status(); 47 | } 48 | 49 | function status () { 50 | if (verbose) { 51 | var i, len; 52 | console.log('\n'); 53 | console.log('### Status ###'); 54 | for (i = 0, len = children.length; i < len; i++) { 55 | if (children[i].exitCode === null) { 56 | console.log('`' + children[i].cmd + '` is still running'); 57 | } else if (children[i].exitCode > 0) { 58 | console.log('`' + children[i].cmd + '` errored'); 59 | } else { 60 | console.log('`' + children[i].cmd + '` finished'); 61 | } 62 | } 63 | console.log('\n'); 64 | } 65 | } 66 | 67 | // closes all children and the process 68 | function close (code) { 69 | var i, len, closed = 0, opened = 0; 70 | 71 | for (i = 0, len = children.length; i < len; i++) { 72 | if (!children[i].exitCode) { 73 | opened++; 74 | children[i].removeAllListeners('close'); 75 | children[i].kill("SIGINT"); 76 | if (verbose) console.log('`' + children[i].cmd + '` will now be closed'); 77 | children[i].on('close', function() { 78 | closed++; 79 | if (opened == closed) { 80 | process.exit(code); 81 | } 82 | }); 83 | } 84 | } 85 | if (opened == closed) {process.exit(code);} 86 | 87 | } 88 | 89 | // cross platform compatibility 90 | if (process.platform === 'win32') { 91 | sh = 'cmd'; 92 | shFlag = '/c'; 93 | } else { 94 | sh = 'sh'; 95 | shFlag = '-c'; 96 | } 97 | 98 | // start the children 99 | children = []; 100 | cmds.forEach(function (cmd) { 101 | if (process.platform != 'win32') { 102 | cmd = "exec "+cmd; 103 | } 104 | var child = spawn(sh,[shFlag,cmd], { 105 | cwd: parseInt(process.versions.node) < 8 ? process.cwd : process.cwd(), 106 | env: process.env, 107 | stdio: ['pipe', process.stdout, process.stderr] 108 | }) 109 | .on('close', childClose); 110 | child.cmd = cmd 111 | children.push(child) 112 | }); 113 | 114 | // close all children on ctrl+c 115 | process.on('SIGINT', close) 116 | 117 | /* Return true if version >= minimumRequired 118 | * @string minimumRequired example : 8.0.0 119 | * @string version example : 10.0.0 120 | */ 121 | function checkNodeVersion (minimumRequired, version) { 122 | var minVer = minimumRequired.split('.') 123 | var ver = version.split('.') 124 | var result = false; 125 | 126 | for (var i in minVer) { 127 | var min = parseInt(minVer[i]) 128 | if (!ver[i]) { break ; } 129 | var vers = parseInt(ver[i]) 130 | if (vers > min) { 131 | return true 132 | } 133 | else if (vers == min) { 134 | result = true 135 | } 136 | else { 137 | return false 138 | } 139 | } 140 | return result 141 | } 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parallelshell", 3 | "version": "3.0.2", 4 | "description": "Invoke multiple commands, running in parallel", 5 | "homepage": "https://github.com/darkguy2008/parallelshell", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:darkguy2008/parallelshell.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/darkguy2008/parallelshell/issues" 12 | }, 13 | "main": "index.js", 14 | "bin": { 15 | "parallelshell": "./index.js" 16 | }, 17 | "scripts": { 18 | "test": "mocha" 19 | }, 20 | "keywords": [ 21 | "parallel", 22 | "shell" 23 | ], 24 | "author": "DARKGuy ", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "bluebird": "^3.5.0", 28 | "chai": "^4.0.2", 29 | "coffee-script": "^1.12.6", 30 | "mocha": "^3.4.2" 31 | } 32 | } -------------------------------------------------------------------------------- /test/index.coffee: -------------------------------------------------------------------------------- 1 | chai = require "chai" 2 | should = chai.should() 3 | spawn = require("child_process").spawn 4 | Promise = require("bluebird") 5 | 6 | verbose = 0 7 | 8 | # cross platform compatibility 9 | if process.platform == "win32" 10 | sh = "cmd" 11 | shArg = "/c" 12 | else 13 | sh = "sh" 14 | shArg = "-c" 15 | 16 | # children 17 | waitingProcess = "\"node -e 'setTimeout(function(){},10000);'\"" 18 | failingProcess = "\"node -e 'throw new Error();'\"" 19 | 20 | usageInfo = """ 21 | -h, --help output usage information 22 | -v, --verbose verbose logging 23 | -w, --wait will not close sibling processes on error 24 | """.split("\n") 25 | 26 | cmdWrapper = (cmd) -> 27 | if process.platform != "win32" 28 | cmd = "exec "+cmd 29 | if verbose 30 | console.log "Calling: "+cmd 31 | return cmd 32 | 33 | spawnParallelshell = (cmd) -> 34 | return spawn sh, [shArg, cmdWrapper("node ./index.js "+cmd )], { 35 | cwd: process.cwd() 36 | } 37 | 38 | killPs = (ps) -> 39 | ps.kill "SIGINT" 40 | 41 | spyOnPs = (ps, verbosity=1) -> 42 | if verbose >= verbosity 43 | ps.stdout.setEncoding("utf8") 44 | ps.stdout.on "data", (data) -> 45 | console.log data 46 | ps.stderr.setEncoding("utf8") 47 | ps.stderr.on "data", (data) -> 48 | console.log "err: "+data 49 | 50 | testOutput = (cmd, expectedOutput) -> 51 | return new Promise (resolve) -> 52 | ps = spawnParallelshell(cmd) 53 | spyOnPs ps, 3 54 | ps.stdout.setEncoding("utf8") 55 | output = [] 56 | ps.stdout.on "data", (data) -> 57 | lines = data.split("\n") 58 | lines.pop() if lines[lines.length-1] == "" 59 | output = output.concat(lines) 60 | ps.stdout.on "end", () -> 61 | for line,i in expectedOutput 62 | line.should.equal output[i] 63 | resolve() 64 | 65 | describe "parallelshell", -> 66 | it "should print on -h and --help", (done) -> 67 | Promise.all([testOutput("-h", usageInfo), testOutput("--help", usageInfo)]) 68 | .then -> done() 69 | .catch done 70 | return 71 | 72 | it "should close with exitCode 1 on child error", (done) -> 73 | ps = spawnParallelshell(failingProcess) 74 | spyOnPs ps, 2 75 | ps.on "close", () -> 76 | ps.exitCode.should.equal 1 77 | done() 78 | 79 | it "should run with a normal child", (done) -> 80 | ps = spawnParallelshell(waitingProcess) 81 | spyOnPs ps, 1 82 | ps.on "close", () -> 83 | ps.signalCode.should.equal "SIGINT" 84 | done() 85 | 86 | setTimeout (() -> 87 | should.not.exist(ps.signalCode) 88 | killPs(ps) 89 | ),25 90 | 91 | 92 | it "should close sibling processes on child error", (done) -> 93 | ps = spawnParallelshell([waitingProcess,failingProcess,waitingProcess].join(" ")) 94 | spyOnPs ps,2 95 | ps.on "close", () -> 96 | ps.exitCode.should.equal 1 97 | done() 98 | 99 | it "should wait for sibling processes on child error when called with -w or --wait", (done) -> 100 | ps = spawnParallelshell(["-w",waitingProcess,failingProcess,waitingProcess].join(" ")) 101 | ps2 = spawnParallelshell(["--wait",waitingProcess,failingProcess,waitingProcess].join(" ")) 102 | spyOnPs ps,2 103 | spyOnPs ps2,2 104 | setTimeout (() -> 105 | should.not.exist(ps.signalCode) 106 | should.not.exist(ps2.signalCode) 107 | killPs(ps) 108 | killPs(ps2) 109 | ),25 110 | Promise.all [new Promise((resolve) -> ps.on("close",resolve)), 111 | new Promise (resolve) -> ps2.on("close",resolve)] 112 | .then -> done() 113 | .catch done 114 | return 115 | 116 | it "should close on CTRL+C / SIGINT", (done) -> 117 | ps = spawnParallelshell(["-w",waitingProcess,failingProcess,waitingProcess].join(" ")) 118 | spyOnPs ps,2 119 | ps.on "close", () -> 120 | ps.signalCode.should.equal "SIGINT" 121 | done() 122 | killPs(ps) 123 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script/register 2 | --timeout 500 --------------------------------------------------------------------------------