├── .travis.yml ├── README.md ├── collector.js ├── compat ├── buffer.js ├── child_process.js └── index.js ├── examples ├── git-unstash ├── stream_map.js └── stream_pipe.js ├── package.json ├── pass-through-stream.js ├── procstreams.js ├── protochains.js └── tests ├── bin ├── env-test.js ├── err-test.js ├── out-test.js └── out-test2.js ├── fixtures ├── 10lines.txt ├── 3lines.txt └── long.txt ├── run_tests.js ├── run_tests.sh ├── test-arguments.js ├── test-error.js ├── test-methods.js ├── test-operators.js ├── test-options.js ├── test-pipe.js ├── test-promises.js ├── test-types.js └── timers.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.6" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Procstreams 2 | 3 | `procstreams` is module to facilitate shell scripting in node. 4 | [![Build Status](https://secure.travis-ci.org/polotek/procstreams.png)](http://travis-ci.org/polotek/procstreams) 5 | 6 | This is the first phase. Right now all it does is make it easier 7 | to create child processes and compose them together in a similar way to 8 | unix command line scripting. 9 | 10 | var $p = require('procstreams'); 11 | $p('cat lines.txt').pipe('wc -l') 12 | .data(function(err, stdout, stderr) { 13 | // handle error 14 | 15 | console.log(stdout); // prints number of lines in the file lines.txt 16 | }); 17 | 18 | $p('mkdir foo') 19 | .and('cp file.txt foo/') 20 | .and('rm file.txt') 21 | .on('exit', function() { 22 | console.log('done'); 23 | }); 24 | 25 | ## procstream function 26 | 27 | The procstream function is the main entry point which creates a child 28 | process that's pipeable and composable. It takes arguments in several 29 | formats. It returns a `ProcStream` object that represents the child process. 30 | 31 | procstream(cmd, argsArray, options, callback) 32 | 33 | `cmd` can be the name of the command, or an array of strings with cmd and args 34 | or a string of cmd + args. 35 | 36 | `args` (optional) can be a string of args or an array of arg strings 37 | 38 | `options` (optional) options object 39 | 40 | `callback` (optional) callback to be called on the "exit" event from the proc. 41 | It receives the same arguments as the child process exit callback 42 | 43 | ### options 44 | 45 | The options object supports all of the options from [`child_process.spawn`](http://nodejs.org/docs/v0.6.5/api/child_processes.html#child_process.spawn) plus 46 | a few additions specific to procstreams: 47 | 48 | `out` - Boolean that determines if the proc output is directed to the main 49 | process output 50 | 51 | If this options is `true` (strictly), the stdout and stderr of the 52 | child process is directed to the stdout and stderr of the calling 53 | process. This is false by default. 54 | 55 | 56 | ## ProcStream 57 | 58 | The ProcStream object represents the child process that is being 59 | executed. It is an `EventEmitter` and it also has various methods for 60 | chaining procstreams together. 61 | 62 | 63 | ### procstream methods 64 | 65 | Each procstream has a set of methods that aid composition. Each of these 66 | methods takes as input a procstream or a set of arguments like the 67 | procstream function. Each method returns the input procstream so it can 68 | be chained. 69 | 70 | **proc1.pipe(proc2)** 71 | 72 | Similar to node's `Stream.pipe`, this is modeled after unix command 73 | piping. The stdout of `proc1` is directed to the stdin of `proc2`. This 74 | method chains by returning `proc2`. 75 | 76 | `proc2` can also be a node `Stream` object and can be interleaved with piping to 77 | commands: 78 | 79 | var $p = require('procstreams'); 80 | 81 | $p('cat tests/fixtures/10lines.txt') 82 | .pipe('grep even') 83 | .pipe('wc -l') 84 | .pipe(process.stdout) 85 | 86 | If your `Stream` object has a `write()` function and emits `'data'` 87 | events then you can interleave shell commands with streaming map 88 | functions: 89 | 90 | var $p = require('../') 91 | var Stream = require('stream').Stream 92 | 93 | // build a custom stream to grep even lines from input 94 | var grepEven = new Stream 95 | grepEven.writable = true 96 | grepEven.readable = true 97 | 98 | var data = '' 99 | grepEven.write = function (buf) { data += buf } 100 | grepEven.end = function () { 101 | this.emit('data', data 102 | .split('\n') 103 | .map(function (line) { return line + '\n' }) 104 | .filter(function (line) { return line.match(/even/) }) 105 | .join('') 106 | ) 107 | this.emit('end') 108 | } 109 | 110 | $p('cat ../tests/fixtures/10lines.txt') 111 | .pipe(grepEven) 112 | .pipe('wc -l') 113 | .pipe(process.stdout) 114 | 115 | **proc1.then(proc2)** 116 | 117 | Like 2 commands run in succession (separated by ';'), `proc1` is run to 118 | completion; then `proc2` is run. This method chains by returning 119 | `proc2`. 120 | 121 | **proc1.and(proc2)** 122 | 123 | Like the `&&` operator, `proc1` is run to completion; if it exits with a 124 | 0 error code, `proc2` is run. If the error code is non-zero, `proc2` is 125 | not run. This method chains by returning `proc2`. 126 | 127 | **proc1.or(proc2)** 128 | 129 | Like the `||` operator, `proc1` is run to completion; if it exits with a 130 | non-zero error code, `proc2` is run. If the error code is zero, `proc2` 131 | is not run. This method chains by returning `proc2`. 132 | 133 | **proc.data(fn)** 134 | 135 | $('cat some-large-file.txt') 136 | .data(function(err, stdout, stderr) { 137 | if(err) { 138 | console.log(err.code, err.signal); 139 | throw err; 140 | } 141 | // process the full output of the proc 142 | }) 143 | 144 | This function will cause the output of the proc to be collected and 145 | passed to this callback on exit. The callback receives an error object 146 | as the first parameter, and the stdout and stderr of the proc. The error 147 | object includes a `code` property representing the exit code of the 148 | proc, and a `signal` property representing a signal that was used to 149 | exit the proc. This method chains by returning the same proc. 150 | 151 | **proc.out()** 152 | 153 | Direct the stdout and stderr of the proc to the calling process. Use 154 | this if you want to forward the output from a child process to the 155 | main process. This method chains by returning the same proc. 156 | 157 | 158 | ## Why? 159 | 160 | Shell scripting languages are extremely powerful, but they're also 161 | annoyingly esoteric. They're difficult to read because of the terse and 162 | obscure syntax. And for most web programmers they only come up often 163 | enough to be frustrating. Many people now use general purpose languages 164 | like python and ruby because they're more familiar and easily installed 165 | in most environments. 166 | 167 | But currently node isn't very good for this type of scripting. So 168 | procstreams is my attempt to add some nice abstractions to the node api 169 | that enable easier scripting in javascript. 170 | 171 | 172 | ## TODO 173 | 174 | * Better `cd` support. Right now you have to pass the `cwd` option to each proc. 175 | * Add options for converting the format of proc output, e.g. numbers, json, etc. 176 | * Add better ways to take action at various events in the proc chain execution 177 | * Allow execution of a custom function as part of the proc chain 178 | 179 | 180 | ## The MIT License 181 | 182 | Copyright (c) 183 | 184 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 185 | 186 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 187 | 188 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 189 | -------------------------------------------------------------------------------- /collector.js: -------------------------------------------------------------------------------- 1 | var Stream = require('stream').Stream 2 | , inherits = require('inherits'); 3 | 4 | var Collector = function() { 5 | this.data = ''; 6 | this.writable = true; 7 | } 8 | inherits(Collector, Stream) 9 | Collector.prototype.write = function(d) { 10 | this.data +=d; 11 | this.emit('data', d); 12 | } 13 | Collector.prototype.end = function() { 14 | this.writable = false; 15 | this.emit('end'); 16 | } 17 | exports.Collector = Collector; 18 | -------------------------------------------------------------------------------- /compat/buffer.js: -------------------------------------------------------------------------------- 1 | if(!Buffer.concat) { 2 | Buffer.concat = function(list, length) { 3 | if (!Array.isArray(list)) { 4 | throw new Error('Usage: Buffer.concat(list, [length])'); 5 | } 6 | 7 | if (list.length === 0) { 8 | return new Buffer(0); 9 | } else if (list.length === 1) { 10 | return list[0]; 11 | } 12 | 13 | if (typeof length !== 'number') { 14 | length = 0; 15 | for (var i = 0; i < list.length; i++) { 16 | var buf = list[i]; 17 | length += buf.length; 18 | } 19 | } 20 | 21 | var buffer = new Buffer(length); 22 | var pos = 0; 23 | for (var i = 0; i < list.length; i++) { 24 | var buf = list[i]; 25 | buf.copy(buffer, pos); 26 | pos += buf.length; 27 | } 28 | return buffer; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /compat/child_process.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver'); 2 | 3 | exports.fix_child_process = function(cp) { 4 | if(semver.lt(process.versions.node, '0.8.0')) { 5 | cp.on('exit', function() { 6 | var self = this 7 | , args = Array.prototype.slice.call(arguments); 8 | 9 | process.nextTick(function() { 10 | args.unshift('close'); 11 | self.emit.apply(self, args); 12 | }); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /compat/index.js: -------------------------------------------------------------------------------- 1 | require('./buffer') 2 | 3 | exports.fix_child_process = require('./child_process').fix_child_process 4 | -------------------------------------------------------------------------------- /examples/git-unstash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * This is a git script that makes it easier to stash and unstash 5 | * multiple patches in a dirty working tree. Taken from this approach 6 | * http://stackoverflow.com/questions/1360712/git-stash-cannot-apply-to-a-dirty-working-tree-please-stage-your-changes 7 | * 8 | * > git stash show -p [stashName] | git apply && git stash drop [stashName] 9 | * $p('git stash show -p', [stashName]).pipe('git apply').and('git stash drop', [stashName]); 10 | * 11 | * Save this file as "git-stash" somewhere in your path. and you can do `git unstash` 12 | * into a dirty working tree. Or `git unstash stash@{2}` 13 | */ 14 | 15 | var $p = require('../procstreams') 16 | , stashName = parsed.remain && parsed.remain[0] 17 | 18 | stash = process.argv[2] || "stash@{0}" 19 | $p('git stash show -p', stashName) 20 | .pipe('git apply') 21 | .and('git stash drop', stashName) 22 | -------------------------------------------------------------------------------- /examples/stream_map.js: -------------------------------------------------------------------------------- 1 | var $p = require('../'); 2 | var Stream = require('stream').Stream; 3 | 4 | var grepEven = new Stream; 5 | grepEven.readable = true; 6 | grepEven.writable = true; 7 | 8 | var data = ''; 9 | grepEven.write = function (buf) { data += buf }; 10 | grepEven.end = function () { 11 | this.emit('data', data 12 | .split('\n') 13 | .map(function (line) { return line + '\n' }) 14 | .filter(function (line) { return line.match(/even/) }) 15 | .join('') 16 | ); 17 | this.emit('end'); 18 | }; 19 | 20 | $p('cat ../tests/fixtures/10lines.txt') 21 | .pipe(grepEven) 22 | .pipe('wc -l') 23 | .pipe(process.stdout); 24 | -------------------------------------------------------------------------------- /examples/stream_pipe.js: -------------------------------------------------------------------------------- 1 | var $p = require('../'); 2 | 3 | $p('cat ../tests/fixtures/10lines.txt') 4 | .pipe('grep even') 5 | .pipe('wc -l') 6 | .pipe(process.stdout) 7 | ; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "procstreams" 3 | , "version": "0.3.0" 4 | , "description": "Enable easier shell scripting in node" 5 | , "url": "https://github.com/polotek/procstreams" 6 | , "keywords": [ 7 | "pipe" 8 | , "child process" 9 | , "command line" 10 | , "cli" 11 | , "shell scripting" 12 | ] 13 | , "author": "polotek (Marco Rogers)" 14 | , "main": "./procstreams.js" 15 | , "dependencies": { 16 | "inherits": "~1.0" 17 | , "shell-quote": "*" 18 | , "data-collector-stream": "*" 19 | , "semver": "*" 20 | } 21 | , "devDependencies": { 22 | "tap": "*" 23 | } 24 | , "scripts": { 25 | "test": "tests/run_tests.sh" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pass-through-stream.js: -------------------------------------------------------------------------------- 1 | var Stream = require('stream') 2 | , util = require('util'); 3 | 4 | var PassThrough = function() { 5 | Stream.apply(this, arguments); 6 | this.readable = this.writable = true; 7 | } 8 | util.inherits(PassThrough, Stream); 9 | PassThrough.prototype.write = function(data) { 10 | this.emit('data', data); 11 | } 12 | PassThrough.prototype.end = function(data) { 13 | if(data) { this.write(data); } 14 | 15 | this.readable = false; 16 | this.writable = false; 17 | 18 | this.emit('end'); 19 | } 20 | 21 | module.exports = PassThrough; 22 | -------------------------------------------------------------------------------- /procstreams.js: -------------------------------------------------------------------------------- 1 | var slice = Array.prototype.slice 2 | , EventEmitter = require('events').EventEmitter 3 | , Stream = require('stream') 4 | , spawn = require('child_process').spawn 5 | , inherits = require('inherits') 6 | , compat = require('./compat') 7 | , parse = require('shell-quote').parse 8 | , utils = require('./protochains') 9 | , PassThrough = require('./pass-through-stream') 10 | , Collector = require('data-collector-stream'); 11 | 12 | var nop = function() {} 13 | 14 | var isProcess = function(cmd) { 15 | if(cmd === process) { return true; } 16 | 17 | return cmd && typeof cmd.spawn == 'function'; 18 | } 19 | 20 | var isStream = function(cmd) { 21 | return cmd && typeof cmd.pipe == 'function' && !procStream.is(cmd); 22 | } 23 | 24 | function procPipe(dest, options) { 25 | options = options || {} 26 | 27 | var source = this 28 | , dest_stdout 29 | , dest_stderr; 30 | 31 | if(source._piped) { 32 | throw new Error('The process has already been piped'); 33 | } 34 | source._piped = true; 35 | 36 | source.stdout.pipe(dest.stdin); 37 | 38 | // stderr goes to console by default, can be overridden 39 | if(options.stderr) { 40 | // Could be an alternate stream to pipe to 41 | if(typeof options.stderr.pipe == 'function') { 42 | dest_stderr = options.stderr; 43 | } else { 44 | dest_stderr = dest.stderr; 45 | } 46 | } else { 47 | dest_stderr = process.stderr; 48 | } 49 | 50 | if(options.stderr !== false) { 51 | source.stderr.pipe(dest_stderr); 52 | } 53 | 54 | dest.emit('pipe', source); 55 | 56 | return dest; 57 | } 58 | 59 | // TODO: make this really robust, use optimist parser? 60 | function parseArgs(args) { 61 | return parse(args.trim()); 62 | } 63 | 64 | function normalizeArguments(cmd, args, opts, callback) { 65 | if(args) { 66 | // options object 67 | if(typeof args != 'string' && !Array.isArray(args)) { 68 | callback = opts; 69 | opts = args; 70 | args = null; 71 | } 72 | } 73 | 74 | if(opts) { 75 | if(typeof opts == 'function') { 76 | callback = opts; 77 | opts = {} 78 | } 79 | } else { 80 | opts = {} 81 | } 82 | if(!opts.env) { opts.env = process.env; } 83 | 84 | var parsedArgs 85 | , val; 86 | 87 | if(typeof cmd == 'string') { 88 | if(typeof args == 'string') { cmd += ' ' + args; } 89 | 90 | parsedArgs = parseArgs(cmd); 91 | cmd = parsedArgs.shift(); 92 | 93 | if(Array.isArray(args)) { 94 | parsedArgs = parsedArgs.concat(args); 95 | } 96 | } else if(Array.isArray(cmd)) { 97 | if(typeof args == 'string') { args = parseArgs(args); } 98 | 99 | parsedArgs = cmd.concat(args); 100 | cmd = parsedArgs.shift(); 101 | } else if(!procStream.is(cmd) && !isStream(cmd)) { 102 | throw new Error('Invalid command'); 103 | } 104 | 105 | return { 106 | cmd: cmd 107 | , args: parsedArgs 108 | , opts: opts 109 | , callback: callback || nop 110 | } 111 | } 112 | 113 | function collect() { 114 | var stdout = new Collector() 115 | , stderr = new Collector(); 116 | 117 | this.stdout.pipe(stdout); 118 | this.stderr.pipe(stderr); 119 | 120 | this.on('close', function(errCode, signal) { 121 | var err = this._err || null; 122 | 123 | this.emit('_output', err, stdout.getData(), stderr.getData()); 124 | }.bind(this)); 125 | } 126 | 127 | function procStream(cmd, args, opts, callback) { 128 | if(!cmd) { throw new Error('Missing command'); } 129 | 130 | var proc = null, o = null; 131 | 132 | // get the args to create a new procstream 133 | o = normalizeArguments(cmd, args, opts, callback); 134 | cmd = o.cmd; 135 | args = o.args; 136 | opts = o.opts; 137 | callback = o.callback; 138 | 139 | // this is a process object 140 | if(isProcess(cmd)) { 141 | // this is already a procstream 142 | if(procStream.is(cmd)) { 143 | cmd.on('close', callback); 144 | return cmd; 145 | } else { 146 | // this is a process that needs to be enhanced 147 | proc = procStream.enhance(cmd); 148 | } 149 | } else if(isStream(cmd)) { 150 | proc = procStream.enhanceStream(cmd); 151 | } else { 152 | proc = spawn(cmd, args, opts); 153 | compat.fix_child_process(proc); 154 | proc = procStream.enhance(proc); 155 | 156 | proc._args = o; 157 | } 158 | 159 | var onExit = function(errCode, signal) { 160 | var err = this._err || {}; 161 | 162 | if(errCode !== 0 || signal) { 163 | err.code = errCode 164 | err.signal = signal 165 | this._err = err; 166 | this.emit('error', err); 167 | } 168 | } 169 | proc.on('close', onExit); 170 | 171 | var onStreamError = function(err) { 172 | this._err = err; 173 | this.emit('error', err); 174 | } 175 | proc.stdout.on('error', onStreamError); 176 | proc.stderr.on('error', onStreamError); 177 | 178 | if(opts.out === true) { 179 | proc.out(); 180 | } 181 | 182 | proc.on('close', callback); 183 | 184 | // TODO: This should be immediate instead of nextTick. But it fails 185 | // for some reason 186 | process.nextTick(function() { proc.emit('start'); }); 187 | return proc; 188 | } 189 | procStream.enhance = utils.enhance; 190 | procStream.is = function(proc) { 191 | if(proc) { 192 | return typeof proc.and == 'function' && typeof proc.pipe == 'function'; 193 | } 194 | return false; 195 | } 196 | procStream.enhanceStream = function(stream) { 197 | var proc = procStream.enhance(new EventEmitter(), { 198 | stdin: stream 199 | , stdout: new PassThrough() 200 | , stderr: new PassThrough() 201 | }); 202 | 203 | var opts = { end: false }; 204 | stream.pipe(proc.stdout, opts); 205 | 206 | stream.once('end', function() { 207 | proc.emit('exit', 0, null); 208 | 209 | proc.stdout.end(); 210 | proc.stderr.end(); 211 | proc.emit('close', 0, null); 212 | }); 213 | return proc; 214 | } 215 | procStream.isProcess = isProcess; 216 | procStream.isStream = isStream; 217 | procStream._prototype = { 218 | out: function out() { 219 | if(this._out) { return; } 220 | this._out = true; 221 | 222 | this.on('start', function() { 223 | var opts = { end: false } 224 | this.stdout.pipe(process.stdout, opts); 225 | this.stderr.pipe(process.stderr, opts); 226 | }); 227 | 228 | return this; 229 | } 230 | , data: function data(fn) { 231 | // data callback suppresses error throwing 232 | if(this.listeners('error').length === 0) { 233 | this.on('error', nop); 234 | } 235 | 236 | this.once('_output', fn); 237 | this.once('start', collect) 238 | 239 | return this; 240 | } 241 | , and: function and() { 242 | var args = slice.call(arguments) 243 | , dest = new procPromise(args); 244 | 245 | this.on('close', function(code, signal) { 246 | if(code === 0) { 247 | dest.resolve(args); 248 | } 249 | }); 250 | 251 | return dest; 252 | } 253 | , or: function or() { 254 | var args = slice.call(arguments) 255 | , dest = new procPromise(args); 256 | 257 | this.on('close', function(code, signal) { 258 | if(code !== 0) { 259 | dest.resolve(); 260 | } 261 | }); 262 | 263 | return dest; 264 | } 265 | , then: function then() { 266 | var args = slice.call(arguments) 267 | , dest = new procPromise(args); 268 | 269 | this.on('close', function(code, signal) { 270 | dest.resolve(); 271 | }); 272 | 273 | return dest; 274 | } 275 | , pipe: function(dest, options) { 276 | var source = this 277 | , args = slice.call(arguments); 278 | 279 | if(typeof source.resolve === 'function') { 280 | dest = new procPromise(args) 281 | 282 | var realSource 283 | , realDest; 284 | 285 | source.on('start', function() { 286 | // FIXME: This is tricky. When "start" is fired 287 | // this handler has been moved from the promise 288 | // to the real proc. 289 | realSource = this; 290 | realDest = dest.resolve(); 291 | procPipe.call(realSource, realDest, options); 292 | }); 293 | } else { 294 | dest = procStream.apply(null, args); 295 | procPipe.call(source, dest, options); 296 | } 297 | 298 | return dest; 299 | } 300 | } 301 | inherits(procStream, EventEmitter, procStream._prototype); 302 | 303 | function procPromise(args) { 304 | this._args = args; 305 | this._resolved = false; 306 | this._proc = null; 307 | 308 | this.resolve = procPromise.prototype.resolve.bind(this); 309 | this.reject = procPromise.prototype.reject.bind(this); 310 | } 311 | procPromise._prototype = { 312 | resolve: function() { 313 | if(this._resolved) { return this._proc; } 314 | this._resolved = true; 315 | 316 | this._proc = procStream.apply(null, this._args); 317 | this._proc._events = utils.mixin({}, this._proc._events, this._events); 318 | 319 | return this._proc; 320 | } 321 | , reject: function() {} 322 | } 323 | inherits(procPromise, procStream, procPromise._prototype); 324 | 325 | module.exports = procStream; 326 | -------------------------------------------------------------------------------- /protochains.js: -------------------------------------------------------------------------------- 1 | var slice = Array.prototype.slice; 2 | 3 | var mixin = function(target) { 4 | var args = slice.call(arguments,1); 5 | 6 | args.forEach(function(p) { 7 | if(!p) { return; } 8 | 9 | Object.keys(p) 10 | .forEach(function(k) { 11 | target[k] = p[k]; 12 | }); 13 | }); 14 | 15 | return target; 16 | } 17 | 18 | var getOwnDescriptors = function() { 19 | var args = slice.call(arguments) 20 | , proto = {}; 21 | 22 | args.forEach(function(p) { 23 | if(!p) { return; } 24 | 25 | Object.getOwnPropertyNames(p) 26 | .forEach(function (k) { 27 | proto[k] = Object.getOwnPropertyDescriptor(p, k); 28 | }); 29 | }); 30 | 31 | return proto; 32 | } 33 | 34 | var extend = function(o, parent, plus) { 35 | var proto = getOwnDescriptors(parent, plus); 36 | 37 | Object.keys(proto).forEach(function(k) { 38 | Object.defineProperty(o, k, proto[k]); 39 | }); 40 | 41 | return o; 42 | } 43 | 44 | var create = function(parent, ctor) { 45 | var obj = Object.create(parent, { 46 | constructor: { 47 | value: ctor, 48 | enumerable: false, 49 | writable: true, 50 | configurable: true 51 | } 52 | }); 53 | 54 | mixin(obj, ctor.prototype); 55 | 56 | return Object.create(obj); 57 | } 58 | 59 | var enhance = function(o, plus) { 60 | return extend(o, this.prototype, plus); 61 | } 62 | 63 | exports.mixin = mixin; 64 | exports.extend = extend; 65 | exports.create = create; 66 | exports.enhance = enhance; 67 | -------------------------------------------------------------------------------- /tests/bin/env-test.js: -------------------------------------------------------------------------------- 1 | console.log(process.env && process.env['ENV_TEST']) 2 | -------------------------------------------------------------------------------- /tests/bin/err-test.js: -------------------------------------------------------------------------------- 1 | console.error('stderr output test') 2 | -------------------------------------------------------------------------------- /tests/bin/out-test.js: -------------------------------------------------------------------------------- 1 | var $p = require(__dirname + '/../../') 2 | 3 | $p('echo output 1') 4 | .pipe('echo output 2') 5 | .pipe('echo output 3').out() 6 | -------------------------------------------------------------------------------- /tests/bin/out-test2.js: -------------------------------------------------------------------------------- 1 | var $p = require(__dirname + '/../../') 2 | 3 | $p('echo output 1') 4 | .pipe('echo output 2', { out: true }) 5 | .pipe('echo output 3') 6 | -------------------------------------------------------------------------------- /tests/fixtures/10lines.txt: -------------------------------------------------------------------------------- 1 | odd 2 | even 3 | odd 4 | even 5 | odd 6 | even 7 | odd 8 | even 9 | odd 10 | even -------------------------------------------------------------------------------- /tests/fixtures/3lines.txt: -------------------------------------------------------------------------------- 1 | line 1 2 | line 2 3 | line 3 4 | -------------------------------------------------------------------------------- /tests/run_tests.js: -------------------------------------------------------------------------------- 1 | // FAILS=0 2 | // for i in tests/test-*.js; do 3 | // echo $i 4 | // node $i || let FAILS++ 5 | // done 6 | // exit $FAILS 7 | 8 | var fs = require('fs') 9 | , $p = require(__dirname + '/..'); 10 | 11 | fs.readdir('tests', function(err, testFiles) { 12 | if(err) { 13 | console.error('Could not read test files'); 14 | throw err; 15 | } 16 | 17 | var chain, fails = 0; 18 | testFiles 19 | .filter(function(file) { 20 | return /^test-[a-zA-Z_.-]+\.js$/.test(file); 21 | }) 22 | .forEach(function(file, idx) { 23 | console.log(file + '\n------'); 24 | // add each test run to the pipe chain 25 | var path = 'tests/' + file; 26 | chain = chain ? chain.then('node', path) : $p('node', path); 27 | chain.on('close', function(code) { 28 | if(code) { 29 | fails++; 30 | } 31 | }); 32 | }); 33 | // chain now has a pipe of all test files ready to be run sequentially 34 | chain.on('close', function() { 35 | console.log('\n' + fails + ' tests failed'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | for i in tests/test-*.js; do 2 | node $i 3 | done 4 | -------------------------------------------------------------------------------- /tests/test-arguments.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , timers = require('./timers') 3 | , exec = require('child_process').exec 4 | , $p = require(__dirname + '/..') 5 | 6 | test('flexible arguments', function(assert) { 7 | var version = process.version 8 | exec('node --version', function(err, output){ 9 | assert.ifError(err) 10 | 11 | assert.equal(version, output.toString().trim()) 12 | 13 | assert.test('string cmd with args', function(assert) { 14 | var t = timers.timer() 15 | $p('node --version') 16 | .data(function(err, output) { 17 | t.stop() 18 | assert.equal(version, output.toString().trim()) 19 | assert.end() 20 | }) 21 | }) 22 | 23 | assert.test('string cmd, string args', function(assert) { 24 | var t = timers.timer() 25 | $p('node', '--version') 26 | .data(function(err, output) { 27 | t.stop() 28 | assert.equal(version, output.toString().trim()) 29 | assert.end() 30 | }) 31 | }) 32 | 33 | assert.test('string cmd, array args', function(assert) { 34 | var t = timers.timer() 35 | $p('node', ['--version']) 36 | .data(function(err, output) { 37 | t.stop() 38 | assert.equal(version, output.toString().trim()) 39 | assert.end() 40 | }) 41 | }) 42 | 43 | assert.test('argument combinations', function(assert) { 44 | var t = timers.multiTimer(6, 3000, function() { 45 | assert.end() 46 | }) 47 | 48 | $p('node --version', function() { 49 | t.stop() 50 | }) 51 | 52 | $p('node', '--version', function() { 53 | t.stop() 54 | }) 55 | 56 | $p('node --version', null, null, function() { 57 | t.stop() 58 | }) 59 | 60 | $p('node --version', function() { 61 | t.stop() 62 | }) 63 | 64 | $p('node --version', { out: true }, function() { 65 | t.stop() 66 | }) 67 | 68 | $p('node', '--version', { out: true }, function() { 69 | t.stop() 70 | }) 71 | }) 72 | 73 | assert.test('args with quotes and newlines', function(assert) { 74 | var t = timers.timer() 75 | $p('echo "new\nline"') 76 | .data(function(err, output) { 77 | t.stop() 78 | assert.equal('new\nline', output.toString().trim()) 79 | assert.end() 80 | }) 81 | }) 82 | 83 | assert.test('non-trivial args', function(assert) { 84 | var t = timers.timer() 85 | $p('echo "foo" "bar baz" \'fizz buzz\'') 86 | .data(function(err, output) { 87 | t.stop() 88 | assert.equal('foo bar baz fizz buzz', output.toString().trim()) 89 | assert.end() 90 | }) 91 | }) 92 | 93 | assert.end() 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /tests/test-error.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , fs = require('fs') 3 | , timers = require(__dirname + '/timers') 4 | , exec = require('child_process').exec 5 | , $p = require(__dirname + '/..') 6 | 7 | test('proc errors', function(assert) { 8 | exec('node does-not-exist', function(err, stdout, stderr) { 9 | assert.ok(err) 10 | 11 | assert.test('data method returns error', function(assert) { 12 | var t = timers.timer() 13 | $p('node does-not-exist') 14 | .data(function(err, stdout, stderr) { 15 | t.stop() 16 | 17 | assert.ok(err) 18 | assert.notEqual(0, err.code) 19 | assert.ok(stderr && /does-not-exist/.test(stderr)) 20 | assert.end() 21 | }) 22 | }) 23 | 24 | assert.test('error event raised with code', function(assert) { 25 | var t = timers.timer() 26 | var err = null 27 | $p('node does-not-exist') 28 | .on('error', function(_err) { 29 | err = _err 30 | }) 31 | .on('close', function(errCode) { 32 | t.stop() 33 | assert.ok(err) 34 | assert.notEqual(0, err.code) 35 | assert.equal(errCode, err.code) 36 | assert.end() 37 | }) 38 | }) 39 | 40 | assert.test('error event raised with kill signal', function(assert) { 41 | var t = timers.timer() 42 | var err = null 43 | , proc = null 44 | proc = $p('node does-not-exist') 45 | .on('error', function(_err) { 46 | err = _err 47 | }) 48 | .on('close', function(errCode, signal) { 49 | t.stop() 50 | assert.ok(err) 51 | assert.ok(err.signal) 52 | assert.equal(signal, err.signal) 53 | assert.end() 54 | }) 55 | 56 | process.nextTick(function() { 57 | proc.kill() 58 | }) 59 | }) 60 | 61 | assert.end() 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /tests/test-methods.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , fs = require('fs') 3 | , timers = require(__dirname + '/timers') 4 | , exec = require('child_process').exec 5 | , $p = require(__dirname + '/..') 6 | 7 | test('data method returns combined output', function(assert) { 8 | var processStdout = '' 9 | process.stdout.on('data', function(d) { 10 | processStdout += d 11 | }) 12 | 13 | fs.readFile('tests/fixtures/long.txt', function(err, fileData) { 14 | assert.ifError(err) 15 | 16 | var t = timers.timer() 17 | $p('cat tests/fixtures/long.txt') 18 | .data(function(err, stdout, stderr) { 19 | assert.ifError(err) 20 | 21 | t.stop() 22 | assert.equal(fileData.toString(), stdout.toString()) 23 | assert.end() 24 | }) 25 | }) 26 | }) 27 | 28 | test('out method sends stdout to process', function(assert) { 29 | var t = timers.timer() 30 | exec('node tests/bin/out-test.js', function(err, output) { 31 | assert.ifError(err) 32 | 33 | t.stop() 34 | assert.equal('output 3', output.toString().trim()) 35 | assert.end() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/test-operators.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , timer = require(__dirname + '/timers').timer 3 | , exec = require('child_process').exec 4 | , $p = require(__dirname + '/..') 5 | 6 | test('and operator only fires on success', function(assert) { 7 | exec('echo && echo pass', function(err, output) { 8 | assert.ifError(err) 9 | assert.equal('pass', output.toString().trim()) 10 | 11 | var t = timer() 12 | $p('echo').and('echo pass') 13 | .data(function(err, output) { 14 | t.stop() 15 | assert.equal('pass', output.toString().trim()) 16 | assert.end() 17 | }) 18 | }) 19 | }) 20 | 21 | test('or operator only fires on failure', function(assert) { 22 | exec('fail || echo pass', function(err, output) { 23 | assert.ifError(err) 24 | assert.equal('pass', output.toString().trim()) 25 | 26 | var t = timer() 27 | $p('fail') 28 | .on('error', function(){}) 29 | .or('echo pass') 30 | .data(function(err, output) { 31 | t.stop() 32 | assert.equal('pass', output.toString().trim()) 33 | assert.end() 34 | }) 35 | }) 36 | }) 37 | 38 | test('then operator fires on success', function(assert) { 39 | exec('echo; echo pass', function(err, output) { 40 | assert.ifError(err) 41 | assert.equal('pass', output.toString().trim()) 42 | 43 | var t = timer() 44 | $p('echo') 45 | .then('echo pass') 46 | .data(function(err, output) { 47 | t.stop() 48 | assert.equal('pass', output.toString().trim()) 49 | assert.end() 50 | }) 51 | }) 52 | }) 53 | 54 | test('then operator fires on failure', function(assert) { 55 | exec('fail; echo pass', function(err, output) { 56 | assert.ifError(err) 57 | assert.equal('pass', output.toString().trim()) 58 | 59 | var t = timer() 60 | $p('fail') 61 | .on('error', function(){}) 62 | .then('echo pass') 63 | .data(function(err, output) { 64 | t.stop() 65 | assert.equal('pass', output.toString().trim()) 66 | assert.end() 67 | }) 68 | }) 69 | }) 70 | 71 | test('chaining and operator', function(assert) { 72 | exec('echo && echo && echo pass2', function(err, output) { 73 | assert.ifError(err) 74 | assert.equal('pass2', output.toString().trim()) 75 | 76 | var t = timer() 77 | $p('echo') 78 | .and('echo pass') 79 | .and('echo pass2') 80 | .data(function(err, output) { 81 | t.stop() 82 | assert.equal('pass2', output.toString().trim()) 83 | assert.end() 84 | }) 85 | }) 86 | }) 87 | 88 | test('chaining different operators', function(assert) { 89 | exec('fail || echo pass && echo pass2', function(err, output) { 90 | assert.ifError(err) 91 | // assert.equal('pass\npass2', output.toString().trim()) 92 | 93 | var t = timer() 94 | $p('fail') 95 | .on('error', function(){}) 96 | .or('echo pass') 97 | .and('echo pass2') 98 | .data(function(err, output) { 99 | t.stop() 100 | assert.equal('pass2', output.toString().trim()) 101 | assert.end() 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /tests/test-options.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , timers = require('./timers') 3 | , Collector = require('data-collector-stream') 4 | , $p = require(__dirname + '/..') 5 | 6 | test('out option sends stdout to process', function(assert) { 7 | var t = timers.timer() 8 | $p('node tests/bin/out-test2.js') 9 | .data(function(err, stdout) { 10 | assert.ifError(err) 11 | 12 | t.stop() 13 | assert.equal('output 2', stdout.toString().trim()) 14 | assert.end() 15 | }) 16 | }) 17 | 18 | test('env option overrides proc environment', function(assert) { 19 | var t = timers.timer() 20 | 21 | var env = { 'ENV_TEST': 'env overridden' } 22 | Object.keys(process.env).forEach(function(k) { 23 | env[k] = process.env[k] 24 | }) 25 | 26 | $p('node tests/bin/env-test.js' 27 | , { 28 | env: env 29 | }) 30 | .data(function(err, stdout) { 31 | assert.ifError(err) 32 | 33 | t.stop() 34 | assert.equal('env overridden', stdout.toString().trim()) 35 | assert.end() 36 | }) 37 | }) 38 | 39 | test('stderr option sends stderr to provided stream', function(assert) { 40 | var t = timers.timer() 41 | 42 | var collector = new Collector() 43 | collector.on('end', function() { 44 | t.stop() 45 | assert.equal('stderr output test', collector.getData().toString().trim()) 46 | assert.end() 47 | }) 48 | $p('node tests/bin/err-test.js').pipe('cat', { stderr: collector }) 49 | }) 50 | -------------------------------------------------------------------------------- /tests/test-pipe.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , timers = require('./timers') 3 | , exec = require('child_process').exec 4 | , $p = require(__dirname + '/..') 5 | , Stream = require('stream').Stream 6 | , Collector = require('data-collector-stream') 7 | 8 | test('simple pipe', function(assert) { 9 | exec('cat tests/fixtures/3lines.txt | wc -l', function(err, output) { 10 | assert.ifError(err) 11 | assert.equal('3', output.toString().trim()) 12 | 13 | var t = timers.timer() 14 | $p('cat tests/fixtures/3lines.txt').pipe('wc -l') 15 | .data(function(err, output) { 16 | assert.ifError(err) 17 | 18 | t.stop() 19 | assert.equal('3', output.toString().trim()) 20 | assert.end() 21 | }) 22 | }) 23 | }) 24 | 25 | test('multiple pipes', function(assert) { 26 | exec('cat tests/fixtures/10lines.txt | grep "even" | wc -l' 27 | , function(err, output) { 28 | assert.ifError(err) 29 | assert.equal('5', output.toString().trim()) 30 | 31 | var t = timers.timer() 32 | $p('cat tests/fixtures/10lines.txt') 33 | .pipe('grep even') 34 | .pipe('wc -l') 35 | .data(function(err, output) { 36 | assert.ifError(err) 37 | 38 | t.stop() 39 | assert.equal('5', output.toString().trim()) 40 | assert.end() 41 | }) 42 | }) 43 | }) 44 | 45 | var getWCStream = function() { 46 | var wcData = ''; 47 | var wc_l = new Stream 48 | wc_l.writable = true 49 | wc_l.readable = true 50 | wc_l.write = function (buf) { wcData += buf } 51 | wc_l.end = function (data) { 52 | if(data) { this.write(data) } 53 | 54 | wc_l.emit('data', wcData.trim().split('\n').length) 55 | wc_l.emit('end') 56 | } 57 | return wc_l 58 | } 59 | 60 | var getGrepStream = function() { 61 | var grep = new Stream 62 | grep.writable = true 63 | grep.readable = true 64 | var grepData = '' 65 | grep.write = function (buf) { grepData += buf } 66 | grep.end = function (data) { 67 | if(data) { this.write(data) } 68 | 69 | grepData = grepData.split('\n').filter(function (line) { 70 | return line.match(/even/) 71 | }).join('\n') + '\n' 72 | grep.emit('data', grepData) 73 | grep.emit('end') 74 | } 75 | return grep 76 | } 77 | 78 | test('pipe to a stream', function(assert) { 79 | exec('cat tests/fixtures/10lines.txt | grep "even"' 80 | , function(err, output) { 81 | assert.ifError(err); 82 | 83 | var t = timers.timer() 84 | $p('cat tests/fixtures/10lines.txt') 85 | .pipe('grep even') 86 | .pipe(new Collector()) 87 | .data(function(err, stdout) { 88 | assert.ifError(err) 89 | 90 | t.stop() 91 | assert.equal(stdout.toString().trim(), output.toString().trim()) 92 | assert.end() 93 | }) 94 | }) 95 | }) 96 | 97 | test('pipe to and from streams', function(assert) { 98 | exec('cat tests/fixtures/10lines.txt | grep "even" | wc -l' 99 | , function(err, output) { 100 | assert.ifError(err); 101 | 102 | var t = timers.multiTimer(2, 3000, function() { 103 | assert.end() 104 | }) 105 | 106 | $p('cat tests/fixtures/10lines.txt') 107 | .pipe('grep even') 108 | .pipe(getWCStream()) 109 | .pipe(new Collector()) 110 | .data(function(err, stdout) { 111 | assert.ifError(err); 112 | 113 | t.stop() 114 | assert.equal(stdout.toString().trim(), output.toString().trim()) 115 | }) 116 | 117 | $p('echo pass').and('cat tests/fixtures/10lines.txt') 118 | .pipe('grep even') 119 | .pipe(getWCStream()) 120 | .pipe(new Collector()) 121 | .data(function(err, stdout) { 122 | assert.ifError(err); 123 | 124 | t.stop() 125 | assert.equal(stdout.toString().trim(), output.toString().trim()) 126 | }) 127 | }) 128 | }) 129 | 130 | test('mix stream and proc pipes', function(assert) { 131 | exec('cat tests/fixtures/10lines.txt | grep "even" | wc -l' 132 | , function(err, output) { 133 | assert.ifError(err); 134 | 135 | var t = timers.multiTimer(2) 136 | var proc = $p('cat tests/fixtures/10lines.txt') 137 | .pipe(getGrepStream()) 138 | .on('close', function() { 139 | t.stop() 140 | }) 141 | .pipe('wc -l') 142 | .pipe(new Collector()) 143 | .data(function(err, stdout) { 144 | assert.ifError(err); 145 | 146 | t.stop() 147 | assert.equal(stdout.toString().trim(), output.toString().trim()) 148 | assert.end() 149 | }) 150 | }) 151 | }) 152 | -------------------------------------------------------------------------------- /tests/test-promises.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , fs = require('fs') 3 | , timers = require(__dirname + '/timers') 4 | , exec = require('child_process').exec 5 | , $p = require(__dirname + '/..') 6 | , nop = function() {} 7 | 8 | 9 | test('proc promises', function(assert) { 10 | assert.test('and method on promise fires on success', function(assert) { 11 | var t = timers.timer() 12 | $p('echo one') 13 | .and('echo two') 14 | .and('echo three') 15 | .data(function(err, stdout, stderr) { 16 | t.stop() 17 | assert.equal('three', stdout.toString().trim()) 18 | assert.end() 19 | }) 20 | }) 21 | 22 | assert.test('or method on promise fires on failure', function(assert) { 23 | var t = timers.timer() 24 | $p('echo one') 25 | .and('fail two').on('error', nop) 26 | .or('echo three') 27 | .data(function(err, stdout, stderr) { 28 | t.stop() 29 | assert.equal('three', stdout.toString().trim()) 30 | assert.end() 31 | }) 32 | }) 33 | 34 | assert.test('then method on promise fires on success', function(assert) { 35 | var t = timers.timer() 36 | $p('echo one') 37 | .then('echo two') 38 | .then('echo three') 39 | .data(function(err, stdout, stderr) { 40 | t.stop() 41 | assert.equal('three', stdout.toString().trim()) 42 | assert.end() 43 | }) 44 | }) 45 | 46 | assert.test('then promise fires on failure', function(assert) { 47 | var t = timers.timer() 48 | $p('echo one') 49 | .then('fail two').on('error', nop) 50 | .then('echo three') 51 | .data(function(err, stdout, stderr) { 52 | t.stop() 53 | assert.equal('three', stdout.toString().trim()) 54 | assert.end() 55 | }) 56 | }) 57 | 58 | assert.test('promise events are transfered to procstream', function(assert) { 59 | var t = timers.timer() 60 | var outProc 61 | $p('echo one') 62 | .and('echo two') 63 | .and('echo three') 64 | .on('start', function() { 65 | outProc = this 66 | }) 67 | .out() 68 | 69 | process.stdout.on('pipe', function(source) { 70 | t.stop() 71 | assert.equal(source, outProc.stdout) 72 | assert.end() 73 | }) 74 | }) 75 | 76 | assert.test('promises can be piped', function(assert) { 77 | var t = timers.timer() 78 | $p('echo one') 79 | .and('echo two') 80 | .and('echo three') 81 | .pipe('cat') 82 | .data(function(err, stdout, stderr) { 83 | t.stop() 84 | assert.equal('three', stdout.toString().trim()) 85 | assert.end() 86 | }) 87 | }) 88 | 89 | assert.end() 90 | }) 91 | -------------------------------------------------------------------------------- /tests/test-types.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , Stream = require('stream') 3 | , fs = require('fs') 4 | , cp = require('child_process') 5 | , exec = cp.exec 6 | , spawn = cp.spawn 7 | , $p = require(__dirname + '/..') 8 | 9 | test('the types behave properly', function(assert) { 10 | var child = spawn('echo "test"') 11 | , child2 = exec('echo "test" | cat') 12 | 13 | assert.ok($p.isProcess(process), 'process is a process') 14 | assert.ok($p.isProcess(child), 'spawn returns a process') 15 | assert.ok($p.isProcess(child2), 'exec returns a process') 16 | 17 | // a process is not a procstream 18 | assert.ok(!$p.is(process), 'process is not a procstream') 19 | assert.ok(!$p.is(child), 'child_process is not a procstream'); 20 | 21 | var stream = new Stream() 22 | , file = fs.createReadStream('tests/fixtures/3lines.txt') 23 | assert.ok($p.isStream(process.stdout), 'process stdout is a stream') 24 | assert.ok($p.isStream(process.stdin), 'process stdin is a stream') 25 | assert.ok($p.isStream(stream), 'stream is a stream') 26 | assert.ok($p.isStream(file), 'filestream is a stream') 27 | 28 | // a normal stream is not a procstream 29 | assert.ok(!$p.is(process.stdout), 'process streams are not procstreams') 30 | assert.ok(!$p.is(stream), 'stream is not a procstream') 31 | assert.ok(!$p.is(file), 'file stream is not a procstream') 32 | assert.ok(!$p.is(), "falsy check doesn't error") 33 | 34 | var proc = $p('echo "test"') 35 | , pstream = $p.enhanceStream(stream) 36 | assert.ok($p.is(proc), 'procstream is a procstream') 37 | assert.ok($p.is(pstream), 'enhanced stream is a procstream') 38 | // a procstream is also a process 39 | assert.ok($p.isProcess(proc), 'procstream is a process') 40 | // a procstream is not like a normal stream 41 | assert.ok(!$p.isStream(proc), 'procstream is not a normal stream') 42 | assert.ok(!$p.isProcess(pstream), 'enhanced stream is not a process') 43 | assert.ok(!$p.isStream(pstream), 'enhanced stream is not a normal stream') 44 | 45 | assert.end() 46 | }) 47 | -------------------------------------------------------------------------------- /tests/timers.js: -------------------------------------------------------------------------------- 1 | exports.timer = function(delay, msg) { 2 | var t = { 3 | stop: function() { 4 | clearTimeout(t._id) 5 | } 6 | } 7 | t._id = setTimeout(function() { 8 | throw new Error('Timer timed out' + (msg ? ': ' + msg : '')) 9 | }, delay || 1000) 10 | 11 | return t 12 | } 13 | 14 | exports.multiTimer = function(stops, delay, msg, callback) { 15 | if(typeof msg === 'function') { 16 | callback = msg 17 | msg = null 18 | if(typeof delay === 'string') { 19 | msg = delay 20 | delay = null 21 | } 22 | } 23 | 24 | if(typeof delay === 'functon') { 25 | callback = delay 26 | msg = null 27 | delay = null 28 | } 29 | 30 | stops = stops || 1 31 | var t = exports.timer(delay, msg) 32 | t._stop = t.stop 33 | t.stop = function() { 34 | --stops 35 | if(stops === 0) { 36 | t._stop() 37 | if(typeof callback === 'function') { 38 | return callback(); 39 | } 40 | } else if (stops < 0) { 41 | throw new Error('Too many timer stops: ', msg) 42 | } 43 | } 44 | 45 | return t 46 | } 47 | --------------------------------------------------------------------------------