UNPKG

6.86 kBJavaScriptView Raw
1/// gulp-run
2/// ==================================================
3/// Pipe to shell commands in gulp.
4
5'use strict';
6
7var child_process = require('child_process');
8var pathlib = require('path');
9var stream = require('stream');
10var util = require('util');
11
12var _ = require('lodash');
13var color = require('cli-color');
14var Vinyl = require('vinyl');
15
16var Logger = require('./lib/logger');
17
18
19/// `var cmd = run(command, [options])`
20/// --------------------------------------------------
21/// Gets a through stream for a shell command to which you can pipe vinyl files. For each file
22/// piped, a new process is spawned, the file is read into the processes's stdin, and a file
23/// containing the processes's stdout is pushed.
24///
25/// Additionally, `./node_modules/.bin` is prepended to the PATH for the child process, so you have
26/// access to all the binaries provided by your module's dependencies.
27///
28/// ### Arguments
29/// 1. `command` *(String)*: The command to run. It can be a [template] interpolating the vinyl file
30/// as the variable `file`.
31/// 2. `[options]` *(Object)*:
32/// - `env` *(Object)*: The environmental variables for the child process. Defaults to
33/// `process.env`. The path `node_modules/.bin` is automatically prepended to the PATH.
34/// - `cwd` *(String)*: The initial working directory for the child process. Defaults to
35/// `process.cwd()`.
36/// - `silent` *(Boolean)*: If true, do not print the command's output. This is the same as
37/// setting verbosity to 1. Defaults to `false`.
38/// - `verbosity` *(Number)*: Sets the verbosity level. Defaults to `2`.
39/// - `0` never outputs anything.
40/// - `1` outputs basic logs.
41/// - `2` outputs basic logs and the stdout of the child process.
42///
43/// [template]: http://lodash.com/docs#template
44///
45/// ### Returns
46/// *(stream.Transform in Object Mode)*: The through stream you so desire.
47///
48/// ### Example
49/// ```javascript
50/// gulp.task('even-lines', function () {
51/// gulp.src('path/to/input/*') // Get input files.
52/// .pipe(run('awk "NR % 2 == 0"')) // Use awk to extract the even lines.
53/// .pipe(gulp.dest('path/to/output')) // Profit.
54/// })
55/// ```
56
57var run = module.exports = function (command, opts) {
58 var command_stream = new stream.Transform({objectMode: true}); // The stream of vinyl files.
59
60 // Options
61 opts = _.defaults(opts||{}, {
62 cwd: process.cwd(),
63 env: process.env,
64 silent: false,
65 verbosity: (opts && opts.silent) ? 1 : 2
66 });
67
68 // Compile the command template.
69 var command_template = _.template(command);
70
71 // Setup logging
72 var logger = new Logger(opts.verbosity);
73 logger.stream.pipe(process.stdout);
74
75
76 // exec(command, [input], [callback])
77 // --------------------------------------------------
78 // TODO: Document
79
80 var exec = function (command, input, callback) {
81 var child; // The child process.
82 var env; // The environmental variables for the child.
83 var out_stream; // The contents of the returned vinyl file.
84
85 // Parse arguments.
86 if (typeof arguments[1] === 'function') {
87 input = null;
88 callback = arguments[1];
89 }
90
91 // Log start message.
92 var start_message = '$ ' + color.cyan(command);
93 if (input && input.relative) {
94 start_message += ' <<< ' + input.relative;
95 }
96 logger.log(1, start_message);
97
98 // Setup environment of child process.
99 opts.env.PATH = pathlib.join('node_modules', '.bin') + ':' + opts.env.PATH;
100
101 // Spawn the process.
102 child = child_process.spawn('sh', ['-c', command], {env:opts.env, cwd:opts.cwd});
103
104 // When the child process is done.
105 child.on('close', function (code) {
106 var err; // Only defined if an error occured
107
108 // Handle errors
109 if (code !== 0) {
110 var error_message = "`" + command + "` exited with code " + code;
111 err = new Error(error_message);
112 logger.log(1, error_message);
113 }
114
115 if (typeof callback === 'function') {
116 process.nextTick(callback.bind(undefined, err));
117 }
118 });
119
120 // Handle input.
121 if (input && typeof input.pipe === 'function') {
122 input.pipe(child.stdin);
123 } else if (input !== undefined && input !== null) {
124 child.stdin.end(input.toString());
125 } else {
126 child.stdin.end();
127 }
128
129 // Handle output.
130 out_stream = new stream.Transform();
131 out_stream._transform = function (chunk, enc, callback) {
132 out_stream.push(chunk);
133 logger.write(2, chunk, enc, callback);
134 };
135 child.stdout.pipe(out_stream);
136 child.stderr.pipe(logger.stream);
137
138 // Return a vinyl file wrapping the command's stdout.
139 return new Vinyl({
140 path: command.split(/\s+/)[0], // first word of the command
141 contents: out_stream
142 });
143
144 }
145
146
147 // The stream.Transform interface
148 // --------------------------------------------------
149 // This method is called automatically for each file piped into the stream. It spawns a
150 // command for each file, using the file's contents as stdin, and pushes downstream a new file
151 // wrapping stdout.
152
153 command_stream._transform = function (file, enc, done) {
154 var output;
155
156 // Spawn the command.
157 output = exec(command_template({file:file}), file, function (err) {
158 if (err) command_stream.emit('error', err);
159 else process.nextTick(done);
160 });
161
162 // Push downstream a vinyl file wrapping the command's stdout.
163 command_stream.push(output);
164 }
165
166
167 /// `cmd.exec([callback])`
168 /// --------------------------------------------------
169 /// Executes the command immediately, returning a stream of vinyl. A single file containing
170 /// the command's stdout is pushed down the stream.
171 ///
172 /// The name of the file pushed down the stream is the first word of the command.
173 /// See [gulp-rename] if you need more flexibility.
174 ///
175 /// ### Arguments
176 /// 1. `[callback]` *(Function)*: Execution is asynchronous. The callback is called once the
177 /// command's stdout has closed.
178 ///
179 /// ### Returns
180 /// *(stream.Readable in Object Mode)*: A stream containing exactly one vinyl file. The file's
181 /// contents is the stdout stream of the command.
182 ///
183 /// ### Example
184 /// ```javascript
185 /// gulp.task('hello-world', function () {
186 /// run('echo Hello World').exec() // prints "[echo] Hello World\n"
187 /// .pipe(gulp.dest('output')) // Writes "Hello World\n" to output/echo
188 /// })
189 /// ```
190
191 command_stream.exec = function (callback) {
192 var output; // A vinyl file whose contents is the stdout of the command.
193 var wrapper; // The higher-level vinyl stream. `output` is the only thing piped through.
194
195 // Spawn the command.
196 output = exec(command_template(), null, function (err) {
197 if (err) wrapper.emit('error', err);
198 if (typeof callback === 'function') {
199 process.nextTick(callback.bind(undefined, err));
200 }
201 });
202
203 // Wrap the output in a vinyl stream.
204 wrapper = new stream.PassThrough({objectMode:true});
205 wrapper.end(output);
206
207 return wrapper;
208 }
209
210 return command_stream;
211}