Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### v3.2.1
- [#31](https://github.com/darkguy2008/parallelshell/issues/31) Report a
missing npm once instead of also marking the script as successful.
Simplify `--npm` and `--timeout` handling and align `--help` output.

### v3.2.0
- [#31](https://github.com/darkguy2008/parallelshell/issues/31) Add
`-n, --npm <pattern>` to run npm scripts with wildcard matching,
Expand Down
48 changes: 17 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,48 +64,34 @@ argument parser.

Available options:
```
-h, --help output usage information
-v, --verbose verbose logging
-w, --wait will not close sibling processes on error
-h, --help output usage information
-v, --verbose verbose logging
-w, --wait will not close sibling processes on error
-t, --timeout <seconds> stop remaining commands after the deadline
-n, --npm <pattern> run matching npm scripts from package.json

-n, --npm <pattern> run matching npm scripts from package.json
```

Use `-n` (or `--npm`) before each npm script name or pattern:
Use `-n` (or `--npm`) before each npm script name or
[minimatch](https://github.com/isaacs/minimatch) pattern, mixed freely with
ordinary commands:

```bash
parallelshell -n "build:*" "echo ordinary command" -n "test:{unit,integration}"
```

Scripts are read from `package.json` in the current directory and run through
`npm run`, including npm's pre/post lifecycle scripts. npm must be on PATH.
Exact script names take precedence over patterns. Patterns use minimatch syntax:
`*`, `?`, character classes, braces and extended globs such as `build:+(js|css)`.
Matching is case-sensitive, includes names starting with a dot and treats leading
`#` and `!` literally. `/` separates pattern segments; use `**` across segments.
Quote patterns with double quotes so they work on Windows and are not expanded
by your shell. Matches follow package.json order; repeated selections run again.
All selections are validated before any command starts. Missing values, unmatched
patterns and missing or invalid package.json files exit with code 1.

This shorthand uses the existing shutdown behavior. The npm-script shutdown
limitation on Debian/Ubuntu tracked in #22 remains unchanged.
Scripts come from `package.json` in the current directory and run through
`npm run`, so pre/post scripts still apply. Exact names win over patterns.
Patterns match names starting with `.` and treat a leading `#` or `!` literally.
Every selection is checked before anything starts: a missing value, an unmatched
pattern or an unreadable `package.json` exits with code 1.

Use `--timeout` (or `-t`) to limit the overall run, including when `--wait` is
enabled:
Use `-t` (or `--timeout`) to stop everything still running after a number of
seconds, including with `--wait`:

```bash
parallelshell --timeout 10 "node server.js" "node request.js"
```

The deadline starts after the commands are launched. Seconds may be fractional
and must be positive and no greater than 2147483.647. If commands are still
running at the deadline, parallelshell reports the timeout and exits with code
124, or preserves an earlier non-zero command exit code with `--wait`. Commands
that finish before the deadline retain their normal exit status.

Timeouts use the existing shutdown behavior: SIGINT to direct children on Unix,
and forced process-tree termination on Windows. On Unix this is not a guaranteed
hard deadline: commands that ignore SIGINT or leave descendants running may keep
the run alive.
On timeout parallelshell exits with code 124, or with an earlier failure's code
under `--wait`. Commands are stopped the same way as on failure, so on Unix a
command that ignores SIGINT can outlive the deadline.
84 changes: 38 additions & 46 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

'use strict';
var spawn = require('child_process').spawn;
var spawnNpm = require('cross-spawn');
var fs = require('fs');
var minimatch = require('minimatch');
var path = require('path');
var signals = require('constants');
var SIGNAL_EXIT_CODE_BASE = 128;
Expand All @@ -13,6 +11,7 @@ var WINDOWS_CONTROL_C_EXIT = 0xC000013A;
var TIMEOUT_EXIT_CODE = 124;
var MILLISECONDS_PER_SECOND = 1000;
var MAX_TIMEOUT_MS = Math.pow(2, 31) - 1;
var SCRIPT_PATTERN_OPTIONS = { dot: true, nocomment: true, nonegate: true, allowWindowsEscape: true };

var WINDOWS = process.platform === 'win32';
var commandPrefix = WINDOWS ? '' : 'exec ';
Expand All @@ -24,22 +23,9 @@ for (i = 0, len = args.length; i < len; i++) {
switch (args[i]) {
case '-n':
case '--npm':
var pattern = args[++i];
if (!pattern || pattern[0] === '-') {
console.error('--npm requires a script name or pattern');
process.exit(1);
}
try {
if (!scripts) {
scripts = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).scripts;
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) throw new Error('package.json must contain a scripts object');
}
var names = Object.keys(scripts);
var matches = Object.prototype.hasOwnProperty.call(scripts, pattern) ? [pattern] : minimatch.match(names, pattern, { dot: true, nocomment: true, nonegate: true, allowWindowsEscape: true });
if (!matches.length) throw new Error('no npm scripts match ' + JSON.stringify(pattern));
matches.forEach(function (name) {
if (typeof scripts[name] !== 'string') throw new Error('npm script ' + JSON.stringify(name) + ' must be a string');
cmds.push({ script: name });
matchingScripts(args[++i]).forEach(function (name) {
cmds.push({ cmd: 'npm run -- ' + name, script: name });
});
} catch (error) {
console.error('--npm: ' + error.message);
Expand All @@ -48,8 +34,8 @@ for (i = 0, len = args.length; i < len; i++) {
break;
case '-t':
case '--timeout':
timeout = Number(args[++i]) * MILLISECONDS_PER_SECOND;
if (!isFinite(timeout) || timeout <= 0 || timeout > MAX_TIMEOUT_MS) {
timeout = Number(args[++i]);
if (!(timeout > 0 && timeout * MILLISECONDS_PER_SECOND <= MAX_TIMEOUT_MS)) {
console.error('--timeout requires positive seconds no greater than ' + MAX_TIMEOUT_MS / MILLISECONDS_PER_SECOND);
process.exit(1);
}
Expand All @@ -64,23 +50,36 @@ for (i = 0, len = args.length; i < len; i++) {
break;
case '-h':
case '--help':
console.log('-h, --help output usage information');
console.log('-v, --verbose verbose logging')
console.log('-w, --wait will not close sibling processes on error')
console.log('-h, --help output usage information');
console.log('-v, --verbose verbose logging');
console.log('-w, --wait will not close sibling processes on error');
console.log('-t, --timeout <seconds> stop remaining commands after the deadline');
console.log('-n, --npm <pattern> run matching npm scripts from package.json');
console.log('-n, --npm <pattern> run matching npm scripts from package.json');
process.exit();
break;
}
} else {
cmds.push(args[i]);
cmds.push({ cmd: commandPrefix + args[i] });
}
}

function matchingScripts (pattern) {
if (!pattern || pattern[0] === '-') throw new Error('requires a script name or pattern');
scripts = scripts || JSON.parse(fs.readFileSync('package.json', 'utf8')).scripts;
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) throw new Error('package.json must contain a scripts object');
var matches = Object.prototype.hasOwnProperty.call(scripts, pattern) ? [pattern] : require('minimatch').match(Object.keys(scripts), pattern, SCRIPT_PATTERN_OPTIONS);
if (!matches.length) throw new Error('no npm scripts match ' + JSON.stringify(pattern));
matches.forEach(function (name) {
if (typeof scripts[name] !== 'string') throw new Error('npm script ' + JSON.stringify(name) + ' must be a string');
});
return matches;
}

function isRunning (child) {
return child.exitCode === null && child.signalCode === null;
}

function childClose (code, signal) {
if (children.every(function (child) {
return child.exitCode !== null || child.signalCode !== null;
})) clearTimeout(timer);
code = signal ? SIGNAL_EXIT_CODE_BASE + signals[signal] : code;
if (verbose) {
if (code > 0) {
Expand All @@ -104,7 +103,7 @@ function status () {
console.log('\n');
console.log('### Status ###');
for (i = 0, len = children.length; i < len; i++) {
if (children[i].exitCode === null && children[i].signalCode === null) {
if (isRunning(children[i])) {
console.log('`' + children[i].cmd + '` is still running');
} else if (children[i].exitCode !== 0) {
console.log('`' + children[i].cmd + '` errored');
Expand All @@ -118,9 +117,7 @@ function status () {

function close (signal) {
clearTimeout(timer);
var running = children.filter(function (child) {
return child.exitCode === null && child.signalCode === null;
});
var running = children.filter(isRunning);
var remaining = running.length;
if (remaining === 0) return exit(signal);
running.forEach(function (child) {
Expand Down Expand Up @@ -162,29 +159,24 @@ FORWARDED_SIGNALS.forEach(function (signal) {
});

children = cmds.map(function (cmd) {
var command = cmd.script === undefined ? commandPrefix + cmd : 'npm run -- ' + JSON.stringify(cmd.script);
var options = {
stdio: ['pipe', process.stdout, process.stderr]
};
var child;
if (cmd.script === undefined) {
options.shell = true;
child = spawn(command, options);
} else {
child = spawnNpm('npm', ['run', '--', cmd.script], options);
}
var stdio = ['pipe', process.stdout, process.stderr];
var child = cmd.script === undefined
? spawn(cmd.cmd, { shell: true, stdio: stdio })
: require('cross-spawn')('npm', ['run', '--', cmd.script], { stdio: stdio });
child.on('error', function (error) {
console.error(error.message);
this.removeListener('close', childClose);
childClose.call(this, 1);
}).on('close', childClose);
child.cmd = command;
child.cmd = cmd.cmd;
return child;
});

if (timeout && children.length) {
if (timeout) {
timer = setTimeout(function () {
console.error('parallelshell timed out after ' + timeout / MILLISECONDS_PER_SECOND + ' seconds');
console.error('parallelshell timed out after ' + timeout + ' seconds');
process.exitCode = process.exitCode || TIMEOUT_EXIT_CODE;
close();
}, timeout);
}, timeout * MILLISECONDS_PER_SECOND);
timer.unref();
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parallelshell",
"version": "3.2.0",
"version": "3.2.1",
"description": "Invoke multiple commands, running in parallel",
"homepage": "https://github.com/darkguy2008/parallelshell",
"repository": {
Expand Down
Loading
Loading