1 | /// gulp-run
|
2 | /// ==================================================
|
3 | /// Pipe to shell commands in gulp.
|
4 |
|
5 | ;
|
6 |
|
7 | var child_process = require('child_process');
|
8 | var pathlib = require('path');
|
9 | var stream = require('stream');
|
10 | var util = require('util');
|
11 |
|
12 | var _ = require('lodash');
|
13 | var color = require('cli-color');
|
14 | var Vinyl = require('vinyl');
|
15 |
|
16 | var 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 |
|
57 | var 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 | }
|