UNPKG

13.6 kBJavaScriptView Raw
1// Ignore warning about 'new String()'
2/* eslint no-new-wrappers: 0 */
3'use strict';
4
5var os = require('os');
6var fs = require('fs');
7var glob = require('glob');
8var shell = require('..');
9
10var shellMethods = Object.create(shell);
11
12exports.extend = Object.assign;
13
14// Check if we're running under electron
15var isElectron = Boolean(process.versions.electron);
16
17// Module globals (assume no execPath by default)
18var DEFAULT_CONFIG = {
19 fatal: false,
20 globOptions: {},
21 maxdepth: 255,
22 noglob: false,
23 silent: false,
24 verbose: false,
25 execPath: null,
26 bufLength: 64 * 1024, // 64KB
27};
28
29var config = {
30 reset: function () {
31 Object.assign(this, DEFAULT_CONFIG);
32 if (!isElectron) {
33 this.execPath = process.execPath;
34 }
35 },
36 resetForTesting: function () {
37 this.reset();
38 this.silent = true;
39 },
40};
41
42config.reset();
43exports.config = config;
44
45// Note: commands should generally consider these as read-only values.
46var state = {
47 error: null,
48 errorCode: 0,
49 currentCmd: 'shell.js',
50};
51exports.state = state;
52
53delete process.env.OLDPWD; // initially, there's no previous directory
54
55// Reliably test if something is any sort of javascript object
56function isObject(a) {
57 return typeof a === 'object' && a !== null;
58}
59exports.isObject = isObject;
60
61function log() {
62 /* istanbul ignore next */
63 if (!config.silent) {
64 console.error.apply(console, arguments);
65 }
66}
67exports.log = log;
68
69// Converts strings to be equivalent across all platforms. Primarily responsible
70// for making sure we use '/' instead of '\' as path separators, but this may be
71// expanded in the future if necessary
72function convertErrorOutput(msg) {
73 if (typeof msg !== 'string') {
74 throw new TypeError('input must be a string');
75 }
76 return msg.replace(/\\/g, '/');
77}
78exports.convertErrorOutput = convertErrorOutput;
79
80// Shows error message. Throws if config.fatal is true
81function error(msg, _code, options) {
82 // Validate input
83 if (typeof msg !== 'string') throw new Error('msg must be a string');
84
85 var DEFAULT_OPTIONS = {
86 continue: false,
87 code: 1,
88 prefix: state.currentCmd + ': ',
89 silent: false,
90 };
91
92 if (typeof _code === 'number' && isObject(options)) {
93 options.code = _code;
94 } else if (isObject(_code)) { // no 'code'
95 options = _code;
96 } else if (typeof _code === 'number') { // no 'options'
97 options = { code: _code };
98 } else if (typeof _code !== 'number') { // only 'msg'
99 options = {};
100 }
101 options = Object.assign({}, DEFAULT_OPTIONS, options);
102
103 if (!state.errorCode) state.errorCode = options.code;
104
105 var logEntry = convertErrorOutput(options.prefix + msg);
106 state.error = state.error ? state.error + '\n' : '';
107 state.error += logEntry;
108
109 // Throw an error, or log the entry
110 if (config.fatal) throw new Error(logEntry);
111 if (msg.length > 0 && !options.silent) log(logEntry);
112
113 if (!options.continue) {
114 throw {
115 msg: 'earlyExit',
116 retValue: (new ShellString('', state.error, state.errorCode)),
117 };
118 }
119}
120exports.error = error;
121
122//@
123//@ ### ShellString(str)
124//@
125//@ Examples:
126//@
127//@ ```javascript
128//@ var foo = ShellString('hello world');
129//@ ```
130//@
131//@ Turns a regular string into a string-like object similar to what each
132//@ command returns. This has special methods, like `.to()` and `.toEnd()`.
133function ShellString(stdout, stderr, code) {
134 var that;
135 if (stdout instanceof Array) {
136 that = stdout;
137 that.stdout = stdout.join('\n');
138 if (stdout.length > 0) that.stdout += '\n';
139 } else {
140 that = new String(stdout);
141 that.stdout = stdout;
142 }
143 that.stderr = stderr;
144 that.code = code;
145 // A list of all commands that can appear on the right-hand side of a pipe
146 // (populated by calls to common.wrap())
147 pipeMethods.forEach(function (cmd) {
148 that[cmd] = shellMethods[cmd].bind(that);
149 });
150 return that;
151}
152
153exports.ShellString = ShellString;
154
155// Returns {'alice': true, 'bob': false} when passed a string and dictionary as follows:
156// parseOptions('-a', {'a':'alice', 'b':'bob'});
157// Returns {'reference': 'string-value', 'bob': false} when passed two dictionaries of the form:
158// parseOptions({'-r': 'string-value'}, {'r':'reference', 'b':'bob'});
159// Throws an error when passed a string that does not start with '-':
160// parseOptions('a', {'a':'alice'}); // throws
161function parseOptions(opt, map, errorOptions) {
162 // Validate input
163 if (typeof opt !== 'string' && !isObject(opt)) {
164 throw new Error('options must be strings or key-value pairs');
165 } else if (!isObject(map)) {
166 throw new Error('parseOptions() internal error: map must be an object');
167 } else if (errorOptions && !isObject(errorOptions)) {
168 throw new Error('parseOptions() internal error: errorOptions must be object');
169 }
170
171 if (opt === '--') {
172 // This means there are no options.
173 return {};
174 }
175
176 // All options are false by default
177 var options = {};
178 Object.keys(map).forEach(function (letter) {
179 var optName = map[letter];
180 if (optName[0] !== '!') {
181 options[optName] = false;
182 }
183 });
184
185 if (opt === '') return options; // defaults
186
187 if (typeof opt === 'string') {
188 if (opt[0] !== '-') {
189 throw new Error("Options string must start with a '-'");
190 }
191
192 // e.g. chars = ['R', 'f']
193 var chars = opt.slice(1).split('');
194
195 chars.forEach(function (c) {
196 if (c in map) {
197 var optionName = map[c];
198 if (optionName[0] === '!') {
199 options[optionName.slice(1)] = false;
200 } else {
201 options[optionName] = true;
202 }
203 } else {
204 error('option not recognized: ' + c, errorOptions || {});
205 }
206 });
207 } else { // opt is an Object
208 Object.keys(opt).forEach(function (key) {
209 // key is a string of the form '-r', '-d', etc.
210 var c = key[1];
211 if (c in map) {
212 var optionName = map[c];
213 options[optionName] = opt[key]; // assign the given value
214 } else {
215 error('option not recognized: ' + c, errorOptions || {});
216 }
217 });
218 }
219 return options;
220}
221exports.parseOptions = parseOptions;
222
223// Expands wildcards with matching (ie. existing) file names.
224// For example:
225// expand(['file*.js']) = ['file1.js', 'file2.js', ...]
226// (if the files 'file1.js', 'file2.js', etc, exist in the current dir)
227function expand(list) {
228 if (!Array.isArray(list)) {
229 throw new TypeError('must be an array');
230 }
231 var expanded = [];
232 list.forEach(function (listEl) {
233 // Don't expand non-strings
234 if (typeof listEl !== 'string') {
235 expanded.push(listEl);
236 } else {
237 var ret;
238 try {
239 ret = glob.sync(listEl, config.globOptions);
240 // if nothing matched, interpret the string literally
241 ret = ret.length > 0 ? ret : [listEl];
242 } catch (e) {
243 // if glob fails, interpret the string literally
244 ret = [listEl];
245 }
246 expanded = expanded.concat(ret);
247 }
248 });
249 return expanded;
250}
251exports.expand = expand;
252
253// Normalizes Buffer creation, using Buffer.alloc if possible.
254// Also provides a good default buffer length for most use cases.
255var buffer = typeof Buffer.alloc === 'function' ?
256 function (len) {
257 return Buffer.alloc(len || config.bufLength);
258 } :
259 function (len) {
260 return new Buffer(len || config.bufLength);
261 };
262exports.buffer = buffer;
263
264// Normalizes _unlinkSync() across platforms to match Unix behavior, i.e.
265// file can be unlinked even if it's read-only, see https://github.com/joyent/node/issues/3006
266function unlinkSync(file) {
267 try {
268 fs.unlinkSync(file);
269 } catch (e) {
270 // Try to override file permission
271 /* istanbul ignore next */
272 if (e.code === 'EPERM') {
273 fs.chmodSync(file, '0666');
274 fs.unlinkSync(file);
275 } else {
276 throw e;
277 }
278 }
279}
280exports.unlinkSync = unlinkSync;
281
282// wrappers around common.statFollowLinks and common.statNoFollowLinks that clarify intent
283// and improve readability
284function statFollowLinks() {
285 return fs.statSync.apply(fs, arguments);
286}
287exports.statFollowLinks = statFollowLinks;
288
289function statNoFollowLinks() {
290 return fs.lstatSync.apply(fs, arguments);
291}
292exports.statNoFollowLinks = statNoFollowLinks;
293
294// e.g. 'shelljs_a5f185d0443ca...'
295function randomFileName() {
296 function randomHash(count) {
297 if (count === 1) {
298 return parseInt(16 * Math.random(), 10).toString(16);
299 }
300 var hash = '';
301 for (var i = 0; i < count; i++) {
302 hash += randomHash(1);
303 }
304 return hash;
305 }
306
307 return 'shelljs_' + randomHash(20);
308}
309exports.randomFileName = randomFileName;
310
311// Common wrapper for all Unix-like commands that performs glob expansion,
312// command-logging, and other nice things
313function wrap(cmd, fn, options) {
314 options = options || {};
315 return function () {
316 var retValue = null;
317
318 state.currentCmd = cmd;
319 state.error = null;
320 state.errorCode = 0;
321
322 try {
323 var args = [].slice.call(arguments, 0);
324
325 // Log the command to stderr, if appropriate
326 if (config.verbose) {
327 console.error.apply(console, [cmd].concat(args));
328 }
329
330 // If this is coming from a pipe, let's set the pipedValue (otherwise, set
331 // it to the empty string)
332 state.pipedValue = (this && typeof this.stdout === 'string') ? this.stdout : '';
333
334 if (options.unix === false) { // this branch is for exec()
335 retValue = fn.apply(this, args);
336 } else { // and this branch is for everything else
337 if (isObject(args[0]) && args[0].constructor.name === 'Object') {
338 // a no-op, allowing the syntax `touch({'-r': file}, ...)`
339 } else if (args.length === 0 || typeof args[0] !== 'string' || args[0].length <= 1 || args[0][0] !== '-') {
340 args.unshift(''); // only add dummy option if '-option' not already present
341 }
342
343 // flatten out arrays that are arguments, to make the syntax:
344 // `cp([file1, file2, file3], dest);`
345 // equivalent to:
346 // `cp(file1, file2, file3, dest);`
347 args = args.reduce(function (accum, cur) {
348 if (Array.isArray(cur)) {
349 return accum.concat(cur);
350 }
351 accum.push(cur);
352 return accum;
353 }, []);
354
355 // Convert ShellStrings (basically just String objects) to regular strings
356 args = args.map(function (arg) {
357 if (isObject(arg) && arg.constructor.name === 'String') {
358 return arg.toString();
359 }
360 return arg;
361 });
362
363 // Expand the '~' if appropriate
364 var homeDir = os.homedir();
365 args = args.map(function (arg) {
366 if (typeof arg === 'string' && arg.slice(0, 2) === '~/' || arg === '~') {
367 return arg.replace(/^~/, homeDir);
368 }
369 return arg;
370 });
371
372 // Perform glob-expansion on all arguments after globStart, but preserve
373 // the arguments before it (like regexes for sed and grep)
374 if (!config.noglob && options.allowGlobbing === true) {
375 args = args.slice(0, options.globStart).concat(expand(args.slice(options.globStart)));
376 }
377
378 try {
379 // parse options if options are provided
380 if (isObject(options.cmdOptions)) {
381 args[0] = parseOptions(args[0], options.cmdOptions);
382 }
383
384 retValue = fn.apply(this, args);
385 } catch (e) {
386 /* istanbul ignore else */
387 if (e.msg === 'earlyExit') {
388 retValue = e.retValue;
389 } else {
390 throw e; // this is probably a bug that should be thrown up the call stack
391 }
392 }
393 }
394 } catch (e) {
395 /* istanbul ignore next */
396 if (!state.error) {
397 // If state.error hasn't been set it's an error thrown by Node, not us - probably a bug...
398 e.name = 'ShellJSInternalError';
399 throw e;
400 }
401 if (config.fatal) throw e;
402 }
403
404 if (options.wrapOutput &&
405 (typeof retValue === 'string' || Array.isArray(retValue))) {
406 retValue = new ShellString(retValue, state.error, state.errorCode);
407 }
408
409 state.currentCmd = 'shell.js';
410 return retValue;
411 };
412} // wrap
413exports.wrap = wrap;
414
415// This returns all the input that is piped into the current command (or the
416// empty string, if this isn't on the right-hand side of a pipe
417function _readFromPipe() {
418 return state.pipedValue;
419}
420exports.readFromPipe = _readFromPipe;
421
422var DEFAULT_WRAP_OPTIONS = {
423 allowGlobbing: true,
424 canReceivePipe: false,
425 cmdOptions: null,
426 globStart: 1,
427 pipeOnly: false,
428 wrapOutput: true,
429 unix: true,
430};
431
432// This is populated during plugin registration
433var pipeMethods = [];
434
435// Register a new ShellJS command
436function _register(name, implementation, wrapOptions) {
437 wrapOptions = wrapOptions || {};
438
439 // Validate options
440 Object.keys(wrapOptions).forEach(function (option) {
441 if (!DEFAULT_WRAP_OPTIONS.hasOwnProperty(option)) {
442 throw new Error("Unknown option '" + option + "'");
443 }
444 if (typeof wrapOptions[option] !== typeof DEFAULT_WRAP_OPTIONS[option]) {
445 throw new TypeError("Unsupported type '" + typeof wrapOptions[option] +
446 "' for option '" + option + "'");
447 }
448 });
449
450 // If an option isn't specified, use the default
451 wrapOptions = Object.assign({}, DEFAULT_WRAP_OPTIONS, wrapOptions);
452
453 if (shell.hasOwnProperty(name)) {
454 throw new Error('Command `' + name + '` already exists');
455 }
456
457 if (wrapOptions.pipeOnly) {
458 wrapOptions.canReceivePipe = true;
459 shellMethods[name] = wrap(name, implementation, wrapOptions);
460 } else {
461 shell[name] = wrap(name, implementation, wrapOptions);
462 }
463
464 if (wrapOptions.canReceivePipe) {
465 pipeMethods.push(name);
466 }
467}
468exports.register = _register;