├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── command.js ├── dist └── .gitignore ├── gulpfile.js ├── index.js ├── package.json └── test ├── _resource └── input.txt ├── command.test.js ├── index.test.js └── util.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "node": true 5 | }, 6 | "globals": { 7 | "describe": true, 8 | "it": true, 9 | "before": true, 10 | "after": true, 11 | "beforeEach": true, 12 | "afterEach": true 13 | }, 14 | "rules": { 15 | "no-var": 0, 16 | "comma-dangle": [2, "never"], 17 | "no-space-before-semi": 0, 18 | "space-after-function-name": 0, 19 | "semi-spacing": [2, { "before": false, "after": true }], 20 | "no-param-reassign": 0, 21 | "spaced-line-comment": 0, 22 | "react/prop-types": 1, 23 | "react/jsx-boolean-value": 0, 24 | "indent": [2, 2, { "SwitchCase": 1 }], 25 | "default-case": 1, 26 | "prefer-arrow-callback": 0, 27 | "prefer-template": 0, 28 | "space-before-function-paren": 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 'node_js' 2 | node_js: 3 | - '5.5' 4 | - '5.4' 5 | - '5.3' 6 | - '5.2' 7 | - '5.1' 8 | - '5.0' 9 | - '4.2' 10 | - '4.1' 11 | - '4.0' 12 | - '0.12' 13 | - '0.11' 14 | - '0.10' 15 | - 'iojs' 16 | before_script: 17 | - 'npm install -g gulp' 18 | script: 'gulp' 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The ISC License (ISC) 2 | ===================== 3 | Copyright (c) 2014 Chris Barrick 4 | Copyright (c) 2016 Marc Binder 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-run 2 | [![Codacy Badge](https://api.codacy.com/project/badge/grade/c40b0223c6314a1ebab8d0024cfd1d41)](https://www.codacy.com/app/mrboolean/gulp-run) 3 | 4 | Use shell commands in your gulp or vinyl pipeline. 5 | 6 | Many command line interfaces are built around the idea of piping. Let's take advantage of that in our Gulp pipeline! To use gulp-run, simply tell it the command to process your files; gulp-run accepts any command you could write into your shell, including I/O redirection like ` python < baz.py | cat foo.txt - bar.txt`. Additionally, `node_modules/.bin` is included on the path, so you can call programs supplied by your installed packages. Supports Unix and Windows. 7 | 8 | This plugin is inspired by [gulp-shell] and [gulp-spawn] and attempts to improve upon their great work. 9 | 10 | ## Usage 11 | 12 | ```javascript 13 | var run = require('gulp-run'); 14 | 15 | // use gulp-run to start a pipeline 16 | gulp.task('hello-world', function() { 17 | return run('echo Hello World').exec() // prints "Hello World\n". 18 | .pipe(gulp.dest('output')) // writes "Hello World\n" to output/echo. 19 | ; 20 | }) 21 | 22 | 23 | // use gulp-run in the middle of a pipeline: 24 | gulp.task('even-lines', function() { 25 | return gulp 26 | .src('path/to/input/*') // get input files. 27 | .pipe(run('awk "NR % 2 == 0"')) // use awk to extract the even lines. 28 | .pipe(gulp.dest('path/to/output')) // profit. 29 | ; 30 | }); 31 | 32 | // use gulp-run without gulp 33 | var cmd = new run.Command('cat'); // create a command object for `cat`. 34 | cmd.exec('hello world'); // call `cat` with 'hello world' on stdin. 35 | ``` 36 | 37 | ## API 38 | ### `run(template, [options])` 39 | Creates a Vinyl (gulp) stream that transforms its input by piping it to a shell command. 40 | 41 | See `run.Command` for a description of the arguments. 42 | 43 | #### Returns 44 | *(stream.Transform in Object Mode)*: Returns a Transform stream that receives Vinyl files. For each input, a subprocess is started taking the contents of the input on stdin. A new file is pushed downstream containing the process's stdout. 45 | 46 | #### Example 47 | ```javascript 48 | gulp.task('even-lines', function() { 49 | return gulp 50 | .src('path/to/input/*') // get input files. 51 | .pipe(run('awk "NR % 2 == 0"')) // use awk to extract the even lines. 52 | .pipe(gulp.dest('path/to/output')) // profit. 53 | ; 54 | }) 55 | ``` 56 | 57 | --- 58 | 59 | ### `run(...).exec([stdin], [callback])` 60 | Start a gulp pipeline and execute the command immediately, pushing the results downstream. 61 | 62 | #### Arguments 63 | 1. `[stdin]` *(String | Buffer | Vinyl)*: If given, this will be used as stdin for the command. 64 | 1. `[callback]` *(Function)*: The callback is called once the command has exited. An Error is passed if the exit status was non-zero. The error will have a `status` property set to the exit status. 65 | 66 | #### Returns 67 | *(Stream.Readable in Object Mode)*: Returns a Vinyl (gulp) stream which will push downstream the stdout of the command as a Vinyl file. The default path of the Vinyl file is the first word of the template; use [gulp-rename] for more versatility. 68 | 69 | #### Example 70 | ```javascript 71 | gulp.task('hello-world', function() { 72 | return run('echo Hello World').exec() // prints "[echo] Hello World\n". 73 | .pipe(gulp.dest('output')) // writes "Hello World\n" to output/echo. 74 | ; 75 | }) 76 | ``` 77 | 78 | --- 79 | 80 | 81 | ### `new run.Command(template, [options])` 82 | Represents a command to be run in a subshell. 83 | 84 | #### Arguments 85 | 1. `template` *(String)*: The command to run. It can be a [template] interpolating the variable `file` which references the [Vinyl] file being input. The template need not interpolate anything; a simple shell command will do just fine. The command is passed as an argument to `sh -c`, so I/O redirection and the like will work as you would expect from a terminal. 86 | 1. `options` *(Object)*: 87 | - `env` *(Object)*: The environmental variables for the child process. Defaults to `process.env`. 88 | - `cwd` *(String)*: The initial working directory for the child process. Defaults to `process.cwd()`. 89 | - `silent` *(Boolean)*: If true, do not print the command's output. This is the same as setting verbosity to 1. Defaults to false. 90 | - `verbosity` *(Number)*: Sets the verbosity level. Defaults to `2`. 91 | - `0`: Do not print anything, ever. 92 | - `1`: Print the command being run and its stderr. 93 | - `2`: Print the command, its stderr, and its stdout. 94 | - `3`: Print the command, its stderr, and its stdout progressivly. Not useful if you have concurrent gulp-run instances, as the outputs may get mixed. 95 | - `usePowerShell` *(Boolean)*: *Windows only*. If `true` uses the PowerShell instead of `cmd.exe` for command execution. 96 | 97 | --- 98 | 99 | ### `run.Command#exec([stdin], [callback])` 100 | Spawn a subshell and execute the command. 101 | 102 | #### Arguments 103 | 1. `[stdin]` *(String | Buffer | Vinyl)*: If given, this will be used as stdin for the command. 104 | 2. `[callback]` *(Function)*: The callback is called once the command has exited. An Error is passed if the exit status was non-zero. The error will have a `status` property set to the exit status. 105 | 106 | #### Returns 107 | *(Vinyl)*: Returns a [Vinyl] file wrapping the stdout of the command. 108 | 109 | #### Example 110 | ```javascript 111 | var cmd = new run.Command('cat'); // create a command object for `cat`. 112 | cmd.exec('hello world'); // call `cat` with 'hello world' on stdin. 113 | ``` 114 | 115 | --- 116 | 117 | ## The ISC License 118 | Copyright (c) 2014 Chris Barrick 119 | Copyright (c) 2016 Marc Binder 120 | 121 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 122 | 123 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 124 | 125 | [gulp-rename]: https://github.com/hparra/gulp-rename 126 | [gulp-shell]: https://github.com/sun-zheng-an/gulp-shell 127 | [gulp-spawn]: https://github.com/hparra/gulp-spawn 128 | [template]: http://lodash.com/docs#template 129 | [Vinyl]: https://github.com/wearefractal/vinyl 130 | -------------------------------------------------------------------------------- /command.js: -------------------------------------------------------------------------------- 1 | var cp = require('child_process'); 2 | var path = require('path'); 3 | var stream = require('stream'); 4 | var util = require('util'); 5 | var defaults = require('lodash.defaults'); 6 | var applyTemplate = require('lodash.template'); 7 | var Vinyl = require('vinyl'); 8 | var gutil = require('gulp-util'); 9 | 10 | /** 11 | * Creates a new `gulp-run` command. 12 | * 13 | * @param {string} command 14 | * @param {object} options 15 | */ 16 | function Command(command, options) { 17 | var previousPath; 18 | 19 | this.command = command; 20 | 21 | // We're on Windows if `process.platform` starts with "win", i.e. "win32". 22 | this.isWindows = (process.platform.lastIndexOf('win') === 0); 23 | 24 | // the cwd and environment of the command are the same as the main node 25 | // process by default. 26 | this.options = defaults(options || {}, { 27 | cwd: process.cwd(), 28 | env: process.env, 29 | verbosity: (options && options.silent) ? 1 : 2, 30 | usePowerShell: false 31 | }); 32 | 33 | // include node_modules/.bin on the path when we execute the command. 34 | previousPath = this.options.env.PATH; 35 | this.options.env.PATH = path.join(this.options.cwd, 'node_modules', '.bin'); 36 | this.options.env.PATH += path.delimiter; 37 | this.options.env.PATH += previousPath; 38 | } 39 | 40 | /** 41 | * Execute the command, invoking the callback when the command exits. 42 | * Returns a Vinyl file wrapping the command's stdout. 43 | * 44 | * @param {string} stdin 45 | * @param {function} callback 46 | * @return {Stream} 47 | */ 48 | Command.prototype.exec = function exec(stdin, callback) { 49 | var self = this; 50 | var command; 51 | var fileName; 52 | var directory; 53 | var subShell; 54 | var log; 55 | var err; 56 | var stdout; 57 | 58 | // parse the arguments, both are optional. 59 | // after parsing, stdin is a vinyl file to use as standard input to 60 | // the command (possibly empty), and callback is a function. 61 | if (typeof stdin === 'function') { 62 | callback = stdin; 63 | stdin = undefined; 64 | } else if (typeof callback !== 'function') { 65 | callback = function noop() {}; 66 | } 67 | 68 | if (!(stdin instanceof Vinyl)) { 69 | fileName = this.command.split(' ')[0]; 70 | directory = path.join(this.options.cwd, fileName); 71 | 72 | if (typeof stdin === 'string') { 73 | stdin = new Vinyl({ 74 | path: directory, 75 | contents: new Buffer(stdin) 76 | }); 77 | } else if (stdin instanceof Buffer || stdin instanceof stream.Readable) { 78 | stdin = new Vinyl({ 79 | path: directory, 80 | contents: stdin 81 | }); 82 | } else { 83 | stdin = new Vinyl(stdin); 84 | 85 | if (!stdin.path) { 86 | stdin.path = directory; 87 | } 88 | } 89 | } 90 | 91 | // execute the command. 92 | // we spawn the command in a subshell, so things like i/o redirection 93 | // just work. e.g. `echo hello world >> ./hello.txt` works as expected. 94 | command = applyTemplate(this.command)({ 95 | file: stdin 96 | }); 97 | 98 | if (this.isWindows && this.options.usePowerShell) { 99 | // windows powershell 100 | subShell = cp.spawn('powershell.exe', ['-NonInteractive', '-NoLogo', '-Command', command], { 101 | env: this.options.env, 102 | cwd: this.options.cwd 103 | }); 104 | } else if (this.isWindows) { 105 | // windows cmd.exe 106 | subShell = cp.spawn('cmd.exe', ['/c', command], { 107 | env: this.options.env, 108 | cwd: this.options.cwd 109 | }); 110 | } else { 111 | // POSIX shell 112 | subShell = cp.spawn('sh', ['-c', command], { 113 | env: this.options.env, 114 | cwd: this.options.cwd 115 | }); 116 | } 117 | 118 | // setup the output 119 | // 120 | // - if verbosity equals to 3, the command prints directly to the terminal. 121 | // - if verbosity equals to 2, the command's stdout and stderr are buffered 122 | // and printed to the user's terminal after the command exits (this 123 | // prevents overlaping output of multiple commands) 124 | // - if verbosity equals to 1, the command's stderr is buffered as in 2, but 125 | // the stdout is silenced. 126 | log = new stream.PassThrough(); 127 | 128 | function sendLog(context) { 129 | var title = util.format( 130 | '$ %s%s', 131 | gutil.colors.blue(command), 132 | (self.options.verbosity < 2) ? gutil.colors.grey(' # Silenced\n') : '\n' 133 | ); 134 | 135 | context.write(title); 136 | } 137 | 138 | switch (this.options.verbosity) { 139 | case 3: 140 | sendLog(process.stdout); 141 | subShell.stdout.pipe(process.stdout); 142 | subShell.stderr.pipe(process.stderr); 143 | break; 144 | case 2: 145 | subShell.stdout.pipe(log); 146 | // fallthrough 147 | case 1: 148 | subShell.stderr.pipe(log); 149 | sendLog(log); 150 | break; 151 | } 152 | 153 | // setup the cleanup proceedure for when the command finishes. 154 | subShell.once('close', function handleSubShellClose() { 155 | // write the buffered output to stdout 156 | var content = log.read(); 157 | 158 | if (content !== null) { 159 | process.stdout.write(content); 160 | } 161 | }); 162 | 163 | subShell.once('exit', function handleSubShellExit(code) { 164 | // report an error if the command exited with a non-zero exit code. 165 | if (code !== 0) { 166 | err = new Error(util.format('Command `%s` exited with code %s', command, code)); 167 | err.status = code; 168 | 169 | return callback(err); 170 | } 171 | 172 | callback(null); 173 | }); 174 | 175 | // the file wrapping stdout is as the one wrapping stdin (same metadata) 176 | // with different contents. 177 | stdout = stdin.clone(); 178 | stdout.contents = subShell.stdout.pipe(new stream.PassThrough()); 179 | 180 | // finally, write the input to the process's stdin. 181 | stdin.pipe(subShell.stdin); 182 | 183 | return stdout; 184 | }; 185 | 186 | /** 187 | * Returns the command template. 188 | * 189 | * @return {string} 190 | */ 191 | Command.prototype.toString = function toString() { 192 | return this.command; 193 | }; 194 | 195 | module.exports = Command; 196 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var eslint = require('gulp-eslint'); 3 | var istanbul = require('gulp-istanbul'); 4 | var mocha = require('gulp-mocha'); 5 | var del = require('del'); 6 | var gi = require('gulp-if'); 7 | var codacy = require('gulp-codacy'); 8 | var sequence = require('gulp-sequence'); 9 | var run = require('./'); 10 | 11 | gulp.task('clean', ['clean.dist']); 12 | 13 | gulp.task('clean.dist', function cleanDistTask() { 14 | return del(['dist/**', '!dist/.gitignore']); 15 | }); 16 | 17 | gulp.task('test.instrument', function instrumentTask() { 18 | return gulp 19 | .src(['command.js', 'index.js']) 20 | .pipe(istanbul({ 21 | includeUntested: true 22 | })) 23 | .pipe(istanbul.hookRequire()) 24 | ; 25 | }); 26 | 27 | gulp.task('test', ['test.instrument'], function testTask() { 28 | process.env.NODE_ENV = 'test'; 29 | 30 | return gulp 31 | .src(['test/**/*.test.js']) 32 | .pipe(mocha({ 33 | require: ['should'] 34 | })) 35 | .pipe(istanbul.writeReports({ 36 | dir: './dist/test-report' 37 | })) 38 | .pipe(istanbul.enforceThresholds({ 39 | thresholds: { global: 50 } 40 | })) 41 | ; 42 | }); 43 | 44 | gulp.task('lint', function lintTask() { 45 | return gulp 46 | .src([ 47 | 'index.js', 48 | 'gulpfile.js', 49 | 'lib/**/*.js', 50 | 'test/**/*.js' 51 | ]) 52 | .pipe(eslint()) 53 | .pipe(eslint.format()) 54 | .pipe(eslint.failOnError()) 55 | ; 56 | }); 57 | 58 | gulp.task('codacy', function codacyTask() { 59 | return gulp 60 | .src(['dist/test-report/lcov.info'], { read: false }) 61 | .pipe(gi(process.env.CODACY, codacy({ 62 | token: process.env.CODACY 63 | }))) 64 | ; 65 | }); 66 | 67 | gulp.task('example', function exampleTask() { 68 | return gulp.src('README.md') 69 | .pipe(run('awk "NR % 2 == 0"')) 70 | .pipe(run('sed -n 1,4p')) 71 | .pipe(gulp.dest('dist')) 72 | ; 73 | }); 74 | 75 | gulp.task('default', sequence('lint', 'test', 'codacy')); 76 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pipe shell commands in gulp. 3 | * @module gulp-run 4 | */ 5 | 6 | var inherit = require('util').inherits; 7 | var Transform = require('stream').Transform; 8 | var Command = require('./command'); 9 | 10 | /** 11 | * Creates a GulpRunner. 12 | * 13 | * A GulpRunner is a Vinyl transform stream that spawns a child process to 14 | * transform the file. A separate process is spawned to handle each file 15 | * passing through the stream. 16 | * 17 | * @param {string} template 18 | * @param {object} options 19 | */ 20 | function GulpRunner(template, options) { 21 | if (!(this instanceof GulpRunner)) { 22 | return new GulpRunner(template, options); 23 | } 24 | 25 | this.command = new Command(template, options || {}); 26 | Transform.call(this, { objectMode: true }); 27 | } 28 | 29 | /** 30 | * @extends {Stream.Transform} 31 | */ 32 | inherit(GulpRunner, Transform); 33 | 34 | /** 35 | * @param {string} file 36 | * @param {string} encoding 37 | * @param {function} callback 38 | * @return {void} 39 | */ 40 | GulpRunner.prototype._transform = function _transform(file, encoding, callback) { 41 | var newfile = this.command.exec(file, callback); 42 | this.push(newfile); 43 | }; 44 | 45 | /** 46 | * Writes `stdin` to itself and returns itself. 47 | * 48 | * Whenever an object is written into the GulpRunner, a new process is 49 | * spawned taking that data as standard input, and a Vinyl file wrapping the 50 | * process's standard output is pushed downstream. 51 | * 52 | * `stdin` may be a String, Buffer, Readable stream, or Vinyl file. 53 | * 54 | * @param {mixed} stdin 55 | * @param {function} callback 56 | * @return {GulpRunner} 57 | */ 58 | GulpRunner.prototype.exec = function exec(stdin, callback) { 59 | this.write(stdin, callback); 60 | this.end(); 61 | return this; 62 | }; 63 | 64 | /** 65 | * @static 66 | * @type {Command} 67 | */ 68 | GulpRunner.Command = Command; 69 | 70 | module.exports = GulpRunner; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-run", 3 | "version": "1.7.1", 4 | "description": "Pipe to shell commands in gulp", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/MrBoolean/gulp-run.git" 9 | }, 10 | "keywords": [ 11 | "gulpplugin", 12 | "gulp", 13 | "run", 14 | "shell", 15 | "command", 16 | "sh" 17 | ], 18 | "author": "Marc Binder ", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/MrBoolean/gulp-run/issues" 22 | }, 23 | "homepage": "https://github.com/MrBoolean/gulp-run", 24 | "devDependencies": { 25 | "babel-eslint": "^4.1.8", 26 | "del": "^2.2.0", 27 | "eslint": "^1.10.3", 28 | "eslint-config-airbnb": "^4.0.0", 29 | "eslint-plugin-react": "^3.16.1", 30 | "gulp": "^3.6.2", 31 | "gulp-codacy": "^1.0.0", 32 | "gulp-eslint": "^1.1.1", 33 | "gulp-if": "^2.0.0", 34 | "gulp-istanbul": "^0.10.3", 35 | "gulp-mocha": "^2.2.0", 36 | "gulp-rename": "^1.2.0", 37 | "gulp-sequence": "^0.4.4", 38 | "mocha": "^2.1.0", 39 | "should": "^8.2.1" 40 | }, 41 | "dependencies": { 42 | "gulp-util": "^3.0.0", 43 | "lodash.defaults": "^4.0.1", 44 | "lodash.template": "^4.0.2", 45 | "vinyl": "^0.4.6" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/_resource/input.txt: -------------------------------------------------------------------------------- 1 | 1 This file is just a list of line numbers 2 | 2 3 | 3 4 | 4 5 | 5 6 | 6 7 | 7 8 | 8 9 | 9 10 | 10 11 | 11 12 | 12 13 | -------------------------------------------------------------------------------- /test/command.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var Command = require('../command'); 3 | 4 | describe('gulp-run/command', function commandTestCase() { 5 | describe('#constructor', function constructorTestCase() { 6 | it('merges the passed options', function passOptionsTest() { 7 | var options = { 8 | cwd: path.resolve(__dirname), 9 | env: { 10 | PATH: path.resolve(path.join(__dirname, '_resource')) 11 | }, 12 | verbosity: 3, 13 | usePowerShell: false 14 | }; 15 | 16 | var command = new Command('', options); 17 | command.options.should.eql(options); 18 | }); 19 | 20 | it('takes care of options.silent', function silentTest() { 21 | var command = new Command('', { silent: true }); 22 | command.options.verbosity.should.equal(1); 23 | }); 24 | 25 | it('uses the default options', function defaultOptionsTest() { 26 | var command = new Command(''); 27 | command.options.should.be.an.Object(); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-loop-func: 0 */ 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var rename = require('gulp-rename'); 6 | var run = require('../'); 7 | var util = require('./util'); 8 | 9 | describe('gulp-run', function gulpRunTestCase() { 10 | var inputFileName = path.join(__dirname, '_resource/input.txt'); 11 | 12 | it('includes `node_modules/.bin` on the PATH', function includeNodeModulesTest(done) { 13 | var nodeModulesPath = path.join(__dirname, '..', 'node_modules', '.bin'); 14 | 15 | run('echo $PATH', { verbosity: 0 }).exec() 16 | .pipe(util.compare(new RegExp('^' + nodeModulesPath))) 17 | .on('finish', done) 18 | ; 19 | }); 20 | 21 | it('lets you set the initial cwd of the command', function cwdTest(done) { 22 | run('pwd', { cwd: '/', verbosity: 0 }).exec() 23 | .pipe(util.compare('/\n')) 24 | .on('finish', done) 25 | ; 26 | }); 27 | 28 | describe('in a vinyl pipeline', function vinylPipelineTestCase() { 29 | it('works with buffers', function bufferTest(done) { 30 | // use awk to extract the even lines of a file 31 | gulp.src(inputFileName, { buffer: true }) 32 | .pipe(run('awk "NR % 2 == 0"', { 33 | verbosity: 0 34 | })) 35 | .pipe(util.compare('2\n4\n6\n8\n10\n12\n')) 36 | .on('finish', done) 37 | ; 38 | }); 39 | 40 | it('works with streams', function streamTest(done) { 41 | // use awk to extract the even lines of a file 42 | gulp.src(inputFileName, { buffer: false }) 43 | .pipe(run('awk "NR % 2 == 0"', { 44 | verbosity: 0 45 | })) 46 | .pipe(util.compare('2\n4\n6\n8\n10\n12\n')) 47 | .on('finish', done) 48 | ; 49 | }); 50 | 51 | it('supports command templates, i.e. `echo <%= file.path %>`', function templateTest(done) { 52 | gulp.src(inputFileName) 53 | .pipe(run('echo <%= file.path %>', { 54 | verbosity: 0 55 | })) 56 | .pipe(util.compare(inputFileName + '\n')) 57 | .on('finish', done) 58 | ; 59 | }); 60 | 61 | it('emits an `error` event on a failed command', function errorTest(done) { 62 | gulp.src(inputFileName) 63 | .pipe(run('exit 1', { verbosity: 0 })) 64 | .on('error', util.noop(done)) 65 | ; 66 | }); 67 | 68 | it('maintains metadata of incoming file', function metaDataTest(done) { 69 | gulp.src(inputFileName) 70 | .pipe(util.inspect(function handleInspect(file) { 71 | file.custom = 'custom metadata'; 72 | })) 73 | .pipe(run('cat', { 74 | verbosity: 0 75 | })) 76 | .pipe(util.inspect(function captureInspect(file) { 77 | file.custom.should.equal('custom metadata'); 78 | })) 79 | .on('finish', done) 80 | ; 81 | }); 82 | }); 83 | 84 | describe('direct execution (`.exec`)', function execTestCase() { 85 | it('is asynchronous (this test sleeps for 1s)', function asyncTest(done) { 86 | var semaphore = new util.Semaphore(2, done); 87 | var startTime = process.hrtime()[0]; 88 | var index; 89 | 90 | for (index = 0; index < 2; index += 1) { 91 | run('sleep 1', { verbosity: 0 }) 92 | .exec(function handleExec() { 93 | var delta = process.hrtime()[0] - startTime; 94 | delta.should.equal(1); 95 | semaphore.done(); 96 | }) 97 | ; 98 | } 99 | }); 100 | 101 | it('returns a vinyl stream wrapping stdout', function wrappingStdOutTest(done) { 102 | run('echo Hello World', { verbosity: 0 }) 103 | .exec() 104 | .pipe(util.compare('Hello World\n')) 105 | .on('finish', done) 106 | ; 107 | }); 108 | 109 | it('emits an `error` event on a failed command', function emitErrorTest(done) { 110 | run('exit 1', { verbosity: 0 }) 111 | .exec() 112 | .on('error', util.noop(done)) 113 | ; 114 | }); 115 | 116 | it('closes the stream when done', function streamCloseTest(done) { 117 | run('echo Hello World', { verbosity: 0 }) 118 | .exec() 119 | .on('finish', done) 120 | ; 121 | }); 122 | }); 123 | 124 | describe('issues', function issuesTestCase() { 125 | it('#18 - file names and paths', function issue18(done) { 126 | run('echo hello world', { cwd: './', verbosity: 0 }).exec() 127 | .pipe(rename('dest.txt')) 128 | .on('finish', done) 129 | ; 130 | }); 131 | 132 | it('#35 - no callback if command not found', function commandNotFoundTest(done) { 133 | run('nonexistant', { verbosity: 0 }) 134 | .exec() 135 | .on('error', function handleError() { 136 | done(); 137 | }) 138 | ; 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | var Stream = require('stream'); 2 | 3 | /** 4 | * Returns a no-op function. If a callback is given, it is called when the 5 | * returned function is called. This is useful for wrapping functions to ignore 6 | * arguments. 7 | * @param {function} callback 8 | * @return {function} 9 | */ 10 | module.exports.noop = function noop(callback) { 11 | if (typeof callback !== 'function') { 12 | callback = function() {}; 13 | } 14 | 15 | return function() { 16 | callback(); 17 | }; 18 | }; 19 | 20 | /** 21 | * Constructs an async-semaphore that calls back when semaphore#done() 22 | * has been called a given number of times. 23 | * 24 | * @param {number} count 25 | * @param {function} callback 26 | * @return {Semaphore} 27 | */ 28 | module.exports.Semaphore = function Semaphore(count, callback) { 29 | this.done = function done() { 30 | count -= 1; 31 | return (count <= 0) ? callback() : this; 32 | }; 33 | 34 | return this; 35 | }; 36 | 37 | /** 38 | * Returns a pass-through Vinyl stream that throws an error if the contents of 39 | * the incoming file doesn't match a pattern. 40 | * 41 | * @param {string|RegExp} match 42 | * @return {Stream} 43 | */ 44 | module.exports.compare = function compare(match) { 45 | var stream; 46 | 47 | if (!(match instanceof RegExp)) { 48 | match = new RegExp('^' + match.toString() + '$'); 49 | } 50 | 51 | stream = new Stream.Transform({ 52 | objectMode: true 53 | }); 54 | 55 | stream._transform = function _transform(file, encoding, callback) { 56 | var contents; 57 | var clonedFile; 58 | 59 | if (file.isStream()) { 60 | clonedFile = file.clone(); 61 | clonedFile.contents = new Stream.Transform(); 62 | clonedFile.contents._transform = function _clonedFileTransform(chunk, __, next) { 63 | clonedFile.contents.push(chunk); 64 | return next(); 65 | }; 66 | 67 | contents = ''; 68 | file.contents.on('readable', function handleReadable() { 69 | var chunk; 70 | 71 | function loop() { 72 | chunk = file.contents.read(); 73 | if (chunk) { 74 | contents += chunk; 75 | loop(); 76 | } 77 | } 78 | 79 | loop(); 80 | }); 81 | 82 | file.contents.on('end', function handleEnd() { 83 | contents.should.match(match); 84 | 85 | clonedFile.contents.push(contents); 86 | clonedFile.contents.end(); 87 | stream.push(clonedFile); 88 | 89 | process.nextTick(callback); 90 | }); 91 | 92 | return; 93 | } 94 | 95 | contents = (file.isBuffer()) ? file.contents.toString() : file.contents; 96 | contents.should.match(match); 97 | 98 | stream.push(file); 99 | 100 | process.nextTick(callback); 101 | 102 | return; 103 | }; 104 | 105 | return stream; 106 | }; 107 | 108 | /** 109 | * Returns a pass-through Vinyl stream that allows us to inspect the file 110 | * 111 | * @param {function} callback 112 | * @return {Stream} 113 | */ 114 | module.exports.inspect = function inspect(callback) { 115 | var stream = new Stream.Transform({ 116 | objectMode: true 117 | }); 118 | 119 | stream._transform = function _transform(file, enc, next) { 120 | callback(file); 121 | stream.push(file); 122 | 123 | next(); 124 | }; 125 | 126 | return stream; 127 | }; 128 | --------------------------------------------------------------------------------