UNPKG

44.3 kBJavaScriptView Raw
1/*
2 * readlineSync
3 * https://github.com/anseki/readline-sync
4 *
5 * Copyright (c) 2016 anseki
6 * Licensed under the MIT license.
7 */
8
9'use strict';
10
11var
12 IS_WIN = process.platform === 'win32',
13
14 ALGORITHM_CIPHER = 'aes-256-cbc',
15 ALGORITHM_HASH = 'sha256',
16 DEFAULT_ERR_MSG = 'The current environment doesn\'t support interactive reading from TTY.',
17
18 fs = require('fs'),
19 TTY = process.binding('tty_wrap').TTY,
20 childProc = require('child_process'),
21 pathUtil = require('path'),
22
23 defaultOptions = {
24 /* eslint-disable key-spacing */
25 prompt: '> ',
26 hideEchoBack: false,
27 mask: '*',
28 limit: [],
29 limitMessage: 'Input another, please.$<( [)limit(])>',
30 defaultInput: '',
31 trueValue: [],
32 falseValue: [],
33 caseSensitive: false,
34 keepWhitespace: false,
35 encoding: 'utf8',
36 bufferSize: 1024,
37 print: void 0,
38 history: true,
39 cd: false,
40 phContent: void 0,
41 preCheck: void 0
42 /* eslint-enable key-spacing */
43 },
44
45 fdR = 'none', fdW, ttyR, isRawMode = false,
46 extHostPath, extHostArgs, tempdir, salt = 0,
47 lastInput = '', inputHistory = [],
48 _DBG_useExt = false, _DBG_checkOptions = false, _DBG_checkMethod = false;
49
50function getHostArgs(options) {
51 // Send any text to crazy Windows shell safely.
52 function encodeArg(arg) {
53 return arg.replace(/[^\w\u0080-\uFFFF]/g, function(chr) {
54 return '#' + chr.charCodeAt(0) + ';';
55 });
56 }
57
58 return extHostArgs.concat((function(conf) {
59 var args = [];
60 Object.keys(conf).forEach(function(optionName) {
61 if (conf[optionName] === 'boolean') {
62 if (options[optionName]) { args.push('--' + optionName); }
63 } else if (conf[optionName] === 'string') {
64 if (options[optionName]) {
65 args.push('--' + optionName, encodeArg(options[optionName]));
66 }
67 }
68 });
69 return args;
70 })({
71 /* eslint-disable key-spacing */
72 display: 'string',
73 displayOnly: 'boolean',
74 keyIn: 'boolean',
75 hideEchoBack: 'boolean',
76 mask: 'string',
77 limit: 'string',
78 caseSensitive: 'boolean'
79 /* eslint-enable key-spacing */
80 }));
81}
82
83// piping via files (for Node.js v0.10-)
84function _execFileSync(options, execOptions) {
85
86 function getTempfile(name) {
87 var filepath, suffix = '', fd;
88 tempdir = tempdir || require('os').tmpdir();
89
90 while (true) {
91 filepath = pathUtil.join(tempdir, name + suffix);
92 try {
93 fd = fs.openSync(filepath, 'wx');
94 } catch (e) {
95 if (e.code === 'EEXIST') {
96 suffix++;
97 continue;
98 } else {
99 throw e;
100 }
101 }
102 fs.closeSync(fd);
103 break;
104 }
105 return filepath;
106 }
107
108 var hostArgs, shellPath, shellArgs, res = {}, exitCode, extMessage,
109 pathStdout = getTempfile('readline-sync.stdout'),
110 pathStderr = getTempfile('readline-sync.stderr'),
111 pathExit = getTempfile('readline-sync.exit'),
112 pathDone = getTempfile('readline-sync.done'),
113 crypto = require('crypto'), shasum, decipher, password;
114
115 shasum = crypto.createHash(ALGORITHM_HASH);
116 shasum.update('' + process.pid + (salt++) + Math.random());
117 password = shasum.digest('hex');
118 decipher = crypto.createDecipher(ALGORITHM_CIPHER, password);
119
120 hostArgs = getHostArgs(options);
121 if (IS_WIN) {
122 shellPath = process.env.ComSpec || 'cmd.exe';
123 process.env.Q = '"'; // The quote (") that isn't escaped.
124 // `()` for ignore space by echo
125 shellArgs = ['/V:ON', '/S', '/C',
126 '(%Q%' + shellPath + '%Q% /V:ON /S /C %Q%' + /* ESLint bug? */ // eslint-disable-line no-path-concat
127 '%Q%' + extHostPath + '%Q%' +
128 hostArgs.map(function(arg) { return ' %Q%' + arg + '%Q%'; }).join('') +
129 ' & (echo !ERRORLEVEL!)>%Q%' + pathExit + '%Q%%Q%) 2>%Q%' + pathStderr + '%Q%' +
130 ' |%Q%' + process.execPath + '%Q% %Q%' + __dirname + '\\encrypt.js%Q%' +
131 ' %Q%' + ALGORITHM_CIPHER + '%Q% %Q%' + password + '%Q%' +
132 ' >%Q%' + pathStdout + '%Q%' +
133 ' & (echo 1)>%Q%' + pathDone + '%Q%'];
134 } else {
135 shellPath = '/bin/sh';
136 shellArgs = ['-c',
137 // Use `()`, not `{}` for `-c` (text param)
138 '("' + extHostPath + '"' + /* ESLint bug? */ // eslint-disable-line no-path-concat
139 hostArgs.map(function(arg) { return " '" + arg.replace(/'/g, "'\\''") + "'"; }).join('') +
140 '; echo $?>"' + pathExit + '") 2>"' + pathStderr + '"' +
141 ' |"' + process.execPath + '" "' + __dirname + '/encrypt.js"' +
142 ' "' + ALGORITHM_CIPHER + '" "' + password + '"' +
143 ' >"' + pathStdout + '"' +
144 '; echo 1 >"' + pathDone + '"'];
145 }
146 if (_DBG_checkMethod) { _DBG_checkMethod('_execFileSync', hostArgs); }
147 try {
148 childProc.spawn(shellPath, shellArgs, execOptions);
149 } catch (e) {
150 res.error = new Error(e.message);
151 res.error.method = '_execFileSync - spawn';
152 res.error.program = shellPath;
153 res.error.args = shellArgs;
154 }
155
156 while (fs.readFileSync(pathDone, {encoding: options.encoding}).trim() !== '1') {} // eslint-disable-line no-empty
157 if ((exitCode =
158 fs.readFileSync(pathExit, {encoding: options.encoding}).trim()) === '0') {
159 res.input =
160 decipher.update(fs.readFileSync(pathStdout, {encoding: 'binary'}),
161 'hex', options.encoding) +
162 decipher.final(options.encoding);
163 } else {
164 extMessage = fs.readFileSync(pathStderr, {encoding: options.encoding}).trim();
165 res.error = new Error(DEFAULT_ERR_MSG + (extMessage ? '\n' + extMessage : ''));
166 res.error.method = '_execFileSync';
167 res.error.program = shellPath;
168 res.error.args = shellArgs;
169 res.error.extMessage = extMessage;
170 res.error.exitCode = +exitCode;
171 }
172
173 fs.unlinkSync(pathStdout);
174 fs.unlinkSync(pathStderr);
175 fs.unlinkSync(pathExit);
176 fs.unlinkSync(pathDone);
177
178 return res;
179}
180
181function readlineExt(options) {
182 var hostArgs, res = {}, extMessage,
183 execOptions = {env: process.env, encoding: options.encoding};
184
185 if (!extHostPath) {
186 if (IS_WIN) {
187 if (process.env.PSModulePath) { // Windows PowerShell
188 extHostPath = 'powershell.exe';
189 extHostArgs = ['-ExecutionPolicy', 'Bypass', '-File', __dirname + '\\read.ps1']; // eslint-disable-line no-path-concat
190 } else { // Windows Script Host
191 extHostPath = 'cscript.exe';
192 extHostArgs = ['//nologo', __dirname + '\\read.cs.js']; // eslint-disable-line no-path-concat
193 }
194 } else {
195 extHostPath = '/bin/sh';
196 extHostArgs = [__dirname + '/read.sh']; // eslint-disable-line no-path-concat
197 }
198 }
199 if (IS_WIN && !process.env.PSModulePath) { // Windows Script Host
200 // ScriptPW (Win XP and Server2003) needs TTY stream as STDIN.
201 // In this case, If STDIN isn't TTY, an error is thrown.
202 execOptions.stdio = [process.stdin];
203 }
204
205 if (childProc.execFileSync) {
206 hostArgs = getHostArgs(options);
207 if (_DBG_checkMethod) { _DBG_checkMethod('execFileSync', hostArgs); }
208 try {
209 res.input = childProc.execFileSync(extHostPath, hostArgs, execOptions);
210 } catch (e) { // non-zero exit code
211 extMessage = e.stderr ? (e.stderr + '').trim() : '';
212 res.error = new Error(DEFAULT_ERR_MSG + (extMessage ? '\n' + extMessage : ''));
213 res.error.method = 'execFileSync';
214 res.error.program = extHostPath;
215 res.error.args = hostArgs;
216 res.error.extMessage = extMessage;
217 res.error.exitCode = e.status;
218 res.error.code = e.code;
219 res.error.signal = e.signal;
220 }
221 } else {
222 res = _execFileSync(options, execOptions);
223 }
224 if (!res.error) {
225 res.input = res.input.replace(/^\s*'|'\s*$/g, '');
226 options.display = '';
227 }
228
229 return res;
230}
231
232/*
233 display: string
234 displayOnly: boolean
235 keyIn: boolean
236 hideEchoBack: boolean
237 mask: string
238 limit: string (pattern)
239 caseSensitive: boolean
240 keepWhitespace: boolean
241 encoding, bufferSize, print
242*/
243function _readlineSync(options) {
244 var input = '', displaySave = options.display,
245 silent = !options.display &&
246 options.keyIn && options.hideEchoBack && !options.mask;
247
248 function tryExt() {
249 var res = readlineExt(options);
250 if (res.error) { throw res.error; }
251 return res.input;
252 }
253
254 if (_DBG_checkOptions) { _DBG_checkOptions(options); }
255
256 (function() { // open TTY
257 var fsB, constants, verNum;
258
259 function getFsB() {
260 if (!fsB) {
261 fsB = process.binding('fs'); // For raw device path
262 constants = process.binding('constants');
263 }
264 return fsB;
265 }
266
267 if (typeof fdR !== 'string') { return; }
268 fdR = null;
269
270 if (IS_WIN) {
271 // iojs-v2.3.2+ input stream can't read first line. (#18)
272 // ** Don't get process.stdin before check! **
273 // Fixed v5.1.0
274 // Fixed v4.2.4
275 // It regressed again in v5.6.0, it is fixed in v6.2.0.
276 verNum = (function(ver) { // getVerNum
277 var nums = ver.replace(/^\D+/, '').split('.');
278 var verNum = 0;
279 if ((nums[0] = +nums[0])) { verNum += nums[0] * 10000; }
280 if ((nums[1] = +nums[1])) { verNum += nums[1] * 100; }
281 if ((nums[2] = +nums[2])) { verNum += nums[2]; }
282 return verNum;
283 })(process.version);
284 if (!(verNum >= 20302 && verNum < 40204 || verNum >= 50000 && verNum < 50100 || verNum >= 50600 && verNum < 60200) &&
285 process.stdin.isTTY) {
286 process.stdin.pause();
287 fdR = process.stdin.fd;
288 ttyR = process.stdin._handle;
289 } else {
290 try {
291 // The stream by fs.openSync('\\\\.\\CON', 'r') can't switch to raw mode.
292 // 'CONIN$' might fail on XP, 2000, 7 (x86).
293 fdR = getFsB().open('CONIN$', constants.O_RDWR, parseInt('0666', 8));
294 ttyR = new TTY(fdR, true);
295 } catch (e) { /* ignore */ }
296 }
297
298 if (process.stdout.isTTY) {
299 fdW = process.stdout.fd;
300 } else {
301 try {
302 fdW = fs.openSync('\\\\.\\CON', 'w');
303 } catch (e) { /* ignore */ }
304 if (typeof fdW !== 'number') { // Retry
305 try {
306 fdW = getFsB().open('CONOUT$', constants.O_RDWR, parseInt('0666', 8));
307 } catch (e) { /* ignore */ }
308 }
309 }
310
311 } else {
312 if (process.stdin.isTTY) {
313 process.stdin.pause();
314 try {
315 fdR = fs.openSync('/dev/tty', 'r'); // device file, not process.stdin
316 ttyR = process.stdin._handle;
317 } catch (e) { /* ignore */ }
318 } else {
319 // Node.js v0.12 read() fails.
320 try {
321 fdR = fs.openSync('/dev/tty', 'r');
322 ttyR = new TTY(fdR, false);
323 } catch (e) { /* ignore */ }
324 }
325
326 if (process.stdout.isTTY) {
327 fdW = process.stdout.fd;
328 } else {
329 try {
330 fdW = fs.openSync('/dev/tty', 'w');
331 } catch (e) { /* ignore */ }
332 }
333 }
334 })();
335
336 (function() { // try read
337 var atEol, limit,
338 isCooked = !options.hideEchoBack && !options.keyIn,
339 buffer, reqSize, readSize, chunk, line;
340
341 // Node.js v0.10- returns an error if same mode is set.
342 function setRawMode(mode) {
343 if (mode === isRawMode) { return true; }
344 if (ttyR.setRawMode(mode) !== 0) { return false; }
345 isRawMode = mode;
346 return true;
347 }
348
349 if (_DBG_useExt || !ttyR ||
350 typeof fdW !== 'number' && (options.display || !isCooked)) {
351 input = tryExt();
352 return;
353 }
354
355 if (options.display) {
356 fs.writeSync(fdW, options.display);
357 options.display = '';
358 }
359 if (options.displayOnly) { return; }
360
361 if (!setRawMode(!isCooked)) {
362 input = tryExt();
363 return;
364 }
365
366 // https://github.com/nodejs/node/issues/4660
367 // https://github.com/nodejs/node/pull/4682
368 if (Buffer.alloc) {
369 buffer = Buffer.alloc((reqSize = options.keyIn ? 1 : options.bufferSize));
370 } else {
371 buffer = new Buffer((reqSize = options.keyIn ? 1 : options.bufferSize));
372 }
373
374 if (options.keyIn && options.limit) {
375 limit = new RegExp('[^' + options.limit + ']',
376 'g' + (options.caseSensitive ? '' : 'i'));
377 }
378
379 while (true) {
380 readSize = 0;
381 try {
382 readSize = fs.readSync(fdR, buffer, 0, reqSize);
383 } catch (e) {
384 if (e.code !== 'EOF') {
385 setRawMode(false);
386 input += tryExt();
387 return;
388 }
389 }
390 chunk = readSize > 0 ? buffer.toString(options.encoding, 0, readSize) : '\n';
391
392 if (chunk && typeof (line = (chunk.match(/^(.*?)[\r\n]/) || [])[1]) === 'string') {
393 chunk = line;
394 atEol = true;
395 }
396
397 // other ctrl-chars
398 if (chunk) { chunk = chunk.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ''); }
399 if (chunk && limit) { chunk = chunk.replace(limit, ''); }
400
401 if (chunk) {
402 if (!isCooked) {
403 if (!options.hideEchoBack) {
404 fs.writeSync(fdW, chunk);
405 } else if (options.mask) {
406 fs.writeSync(fdW, (new Array(chunk.length + 1)).join(options.mask));
407 }
408 }
409 input += chunk;
410 }
411
412 if (!options.keyIn && atEol ||
413 options.keyIn && input.length >= reqSize) { break; }
414 }
415
416 if (!isCooked && !silent) { fs.writeSync(fdW, '\n'); }
417 setRawMode(false);
418 })();
419
420 if (options.print && !silent) {
421 options.print(displaySave + (options.displayOnly ? '' :
422 (options.hideEchoBack ? (new Array(input.length + 1)).join(options.mask)
423 : input) + '\n'), // must at least write '\n'
424 options.encoding);
425 }
426
427 return options.displayOnly ? '' :
428 (lastInput = options.keepWhitespace || options.keyIn ? input : input.trim());
429}
430
431function flattenArray(array, validator) {
432 var flatArray = [];
433 function _flattenArray(array) {
434 if (array == null) { // eslint-disable-line eqeqeq
435 return;
436 } else if (Array.isArray(array)) {
437 array.forEach(_flattenArray);
438 } else if (!validator || validator(array)) {
439 flatArray.push(array);
440 }
441 }
442 _flattenArray(array);
443 return flatArray;
444}
445
446function escapePattern(pattern) {
447 return pattern.replace(/[\x00-\x7f]/g,
448 function(s) { return '\\x' + ('00' + s.charCodeAt().toString(16)).substr(-2); });
449}
450
451// margeOptions(options1, options2 ... )
452// margeOptions(true, options1, options2 ... )
453// arg1=true : Start from defaultOptions and pick elements of that.
454function margeOptions() {
455 var optionsList = Array.prototype.slice.call(arguments),
456 optionNames, fromDefault;
457
458 if (optionsList.length && typeof optionsList[0] === 'boolean') {
459 fromDefault = optionsList.shift();
460 if (fromDefault) {
461 optionNames = Object.keys(defaultOptions);
462 optionsList.unshift(defaultOptions);
463 }
464 }
465
466 return optionsList.reduce(function(options, optionsPart) {
467 if (optionsPart == null) { return options; } // eslint-disable-line eqeqeq
468
469 // ======== DEPRECATED ========
470 if (optionsPart.hasOwnProperty('noEchoBack') &&
471 !optionsPart.hasOwnProperty('hideEchoBack')) {
472 optionsPart.hideEchoBack = optionsPart.noEchoBack;
473 delete optionsPart.noEchoBack;
474 }
475 if (optionsPart.hasOwnProperty('noTrim') &&
476 !optionsPart.hasOwnProperty('keepWhitespace')) {
477 optionsPart.keepWhitespace = optionsPart.noTrim;
478 delete optionsPart.noTrim;
479 }
480 // ======== /DEPRECATED ========
481
482 if (!fromDefault) { optionNames = Object.keys(optionsPart); }
483 optionNames.forEach(function(optionName) {
484 var value;
485 if (!optionsPart.hasOwnProperty(optionName)) { return; }
486 value = optionsPart[optionName];
487 switch (optionName) {
488 // _readlineSync <- * * -> defaultOptions
489 // ================ string
490 case 'mask': // * *
491 case 'limitMessage': // *
492 case 'defaultInput': // *
493 case 'encoding': // * *
494 value = value != null ? value + '' : ''; // eslint-disable-line eqeqeq
495 if (value && optionName !== 'limitMessage') { value = value.replace(/[\r\n]/g, ''); }
496 options[optionName] = value;
497 break;
498 // ================ number(int)
499 case 'bufferSize': // * *
500 if (!isNaN(value = parseInt(value, 10)) && typeof value === 'number') {
501 options[optionName] = value; // limited updating (number is needed)
502 }
503 break;
504 // ================ boolean
505 case 'displayOnly': // *
506 case 'keyIn': // *
507 case 'hideEchoBack': // * *
508 case 'caseSensitive': // * *
509 case 'keepWhitespace': // * *
510 case 'history': // *
511 case 'cd': // *
512 options[optionName] = !!value;
513 break;
514 // ================ array
515 case 'limit': // * * to string for readlineExt
516 case 'trueValue': // *
517 case 'falseValue': // *
518 options[optionName] = flattenArray(value, function(value) {
519 var type = typeof value;
520 return type === 'string' || type === 'number' ||
521 type === 'function' || value instanceof RegExp;
522 }).map(function(value) {
523 return typeof value === 'string' ? value.replace(/[\r\n]/g, '') : value;
524 });
525 break;
526 // ================ function
527 case 'print': // * *
528 case 'phContent': // *
529 case 'preCheck': // *
530 options[optionName] = typeof value === 'function' ? value : void 0;
531 break;
532 // ================ other
533 case 'prompt': // *
534 case 'display': // *
535 options[optionName] = value != null ? value : ''; // eslint-disable-line eqeqeq
536 break;
537 // no default
538 }
539 });
540 return options;
541 }, {});
542}
543
544function isMatched(res, comps, caseSensitive) {
545 return comps.some(function(comp) {
546 var type = typeof comp;
547 return type === 'string' ?
548 (caseSensitive ? res === comp : res.toLowerCase() === comp.toLowerCase()) :
549 type === 'number' ? parseFloat(res) === comp :
550 type === 'function' ? comp(res) :
551 comp instanceof RegExp ? comp.test(res) : false;
552 });
553}
554
555function replaceHomePath(path, expand) {
556 var homePath = pathUtil.normalize(
557 IS_WIN ? (process.env.HOMEDRIVE || '') + (process.env.HOMEPATH || '') :
558 process.env.HOME || '').replace(/[\/\\]+$/, '');
559 path = pathUtil.normalize(path);
560 return expand ? path.replace(/^~(?=\/|\\|$)/, homePath) :
561 path.replace(new RegExp('^' + escapePattern(homePath) +
562 '(?=\\/|\\\\|$)', IS_WIN ? 'i' : ''), '~');
563}
564
565function replacePlaceholder(text, generator) {
566 var PTN_INNER = '(?:\\(([\\s\\S]*?)\\))?(\\w+|.-.)(?:\\(([\\s\\S]*?)\\))?',
567 rePlaceholder = new RegExp('(\\$)?(\\$<' + PTN_INNER + '>)', 'g'),
568 rePlaceholderCompat = new RegExp('(\\$)?(\\$\\{' + PTN_INNER + '\\})', 'g');
569
570 function getPlaceholderText(s, escape, placeholder, pre, param, post) {
571 var text;
572 return escape || typeof (text = generator(param)) !== 'string' ? placeholder :
573 text ? (pre || '') + text + (post || '') : '';
574 }
575
576 return text.replace(rePlaceholder, getPlaceholderText)
577 .replace(rePlaceholderCompat, getPlaceholderText);
578}
579
580function array2charlist(array, caseSensitive, collectSymbols) {
581 var values, group = [], groupClass = -1, charCode = 0, symbols = '', suppressed;
582 function addGroup(groups, group) {
583 if (group.length > 3) { // ellipsis
584 groups.push(group[0] + '...' + group[group.length - 1]);
585 suppressed = true;
586 } else if (group.length) {
587 groups = groups.concat(group);
588 }
589 return groups;
590 }
591
592 values = array.reduce(
593 function(chars, value) { return chars.concat((value + '').split('')); }, [])
594 .reduce(function(groups, curChar) {
595 var curGroupClass, curCharCode;
596 if (!caseSensitive) { curChar = curChar.toLowerCase(); }
597 curGroupClass = /^\d$/.test(curChar) ? 1 :
598 /^[A-Z]$/.test(curChar) ? 2 : /^[a-z]$/.test(curChar) ? 3 : 0;
599 if (collectSymbols && curGroupClass === 0) {
600 symbols += curChar;
601 } else {
602 curCharCode = curChar.charCodeAt(0);
603 if (curGroupClass && curGroupClass === groupClass &&
604 curCharCode === charCode + 1) {
605 group.push(curChar);
606 } else {
607 groups = addGroup(groups, group);
608 group = [curChar];
609 groupClass = curGroupClass;
610 }
611 charCode = curCharCode;
612 }
613 return groups;
614 }, []);
615 values = addGroup(values, group); // last group
616 if (symbols) { values.push(symbols); suppressed = true; }
617 return {values: values, suppressed: suppressed};
618}
619
620function joinChunks(chunks, suppressed) {
621 return chunks.join(chunks.length > 2 ? ', ' : suppressed ? ' / ' : '/');
622}
623
624function getPhContent(param, options) {
625 var text, values, resCharlist = {}, arg;
626 if (options.phContent) {
627 text = options.phContent(param, options);
628 }
629 if (typeof text !== 'string') {
630 switch (param) {
631 case 'hideEchoBack':
632 case 'mask':
633 case 'defaultInput':
634 case 'caseSensitive':
635 case 'keepWhitespace':
636 case 'encoding':
637 case 'bufferSize':
638 case 'history':
639 case 'cd':
640 text = !options.hasOwnProperty(param) ? '' :
641 typeof options[param] === 'boolean' ? (options[param] ? 'on' : 'off') :
642 options[param] + '';
643 break;
644 // case 'prompt':
645 // case 'query':
646 // case 'display':
647 // text = options.hasOwnProperty('displaySrc') ? options.displaySrc + '' : '';
648 // break;
649 case 'limit':
650 case 'trueValue':
651 case 'falseValue':
652 values = options[options.hasOwnProperty(param + 'Src') ? param + 'Src' : param];
653 if (options.keyIn) { // suppress
654 resCharlist = array2charlist(values, options.caseSensitive);
655 values = resCharlist.values;
656 } else {
657 values = values.filter(function(value) {
658 var type = typeof value;
659 return type === 'string' || type === 'number';
660 });
661 }
662 text = joinChunks(values, resCharlist.suppressed);
663 break;
664 case 'limitCount':
665 case 'limitCountNotZero':
666 text = options[options.hasOwnProperty('limitSrc') ?
667 'limitSrc' : 'limit'].length;
668 text = text || param !== 'limitCountNotZero' ? text + '' : '';
669 break;
670 case 'lastInput':
671 text = lastInput;
672 break;
673 case 'cwd':
674 case 'CWD':
675 case 'cwdHome':
676 text = process.cwd();
677 if (param === 'CWD') {
678 text = pathUtil.basename(text);
679 } else if (param === 'cwdHome') {
680 text = replaceHomePath(text);
681 }
682 break;
683 case 'date':
684 case 'time':
685 case 'localeDate':
686 case 'localeTime':
687 text = (new Date())['to' +
688 param.replace(/^./, function(str) { return str.toUpperCase(); }) +
689 'String']();
690 break;
691 default: // with arg
692 if (typeof (arg = (param.match(/^history_m(\d+)$/) || [])[1]) === 'string') {
693 text = inputHistory[inputHistory.length - arg] || '';
694 }
695 }
696 }
697 return text;
698}
699
700function getPhCharlist(param) {
701 var matches = /^(.)-(.)$/.exec(param), text = '', from, to, code, step;
702 if (!matches) { return null; }
703 from = matches[1].charCodeAt(0);
704 to = matches[2].charCodeAt(0);
705 step = from < to ? 1 : -1;
706 for (code = from; code !== to + step; code += step) { text += String.fromCharCode(code); }
707 return text;
708}
709
710// cmd "arg" " a r g " "" 'a"r"g' "a""rg" "arg
711function parseCl(cl) {
712 var reToken = new RegExp(/(\s*)(?:("|')(.*?)(?:\2|$)|(\S+))/g), matches,
713 taken = '', args = [], part;
714 cl = cl.trim();
715 while ((matches = reToken.exec(cl))) {
716 part = matches[3] || matches[4] || '';
717 if (matches[1]) {
718 args.push(taken);
719 taken = '';
720 }
721 taken += part;
722 }
723 if (taken) { args.push(taken); }
724 return args;
725}
726
727function toBool(res, options) {
728 return (
729 (options.trueValue.length &&
730 isMatched(res, options.trueValue, options.caseSensitive)) ? true :
731 (options.falseValue.length &&
732 isMatched(res, options.falseValue, options.caseSensitive)) ? false : res);
733}
734
735function getValidLine(options) {
736 var res, forceNext, limitMessage,
737 matches, histInput, args, resCheck;
738
739 function _getPhContent(param) { return getPhContent(param, options); }
740 function addDisplay(text) { options.display += (/[^\r\n]$/.test(options.display) ? '\n' : '') + text; }
741
742 options.limitSrc = options.limit;
743 options.displaySrc = options.display;
744 options.limit = ''; // for readlineExt
745 options.display = replacePlaceholder(options.display + '', _getPhContent);
746
747 while (true) {
748 res = _readlineSync(options);
749 forceNext = false;
750 limitMessage = '';
751
752 if (options.defaultInput && !res) { res = options.defaultInput; }
753
754 if (options.history) {
755 if ((matches = /^\s*\!(?:\!|-1)(:p)?\s*$/.exec(res))) { // `!!` `!-1` +`:p`
756 histInput = inputHistory[0] || '';
757 if (matches[1]) { // only display
758 forceNext = true;
759 } else { // replace input
760 res = histInput;
761 }
762 // Show it even if it is empty (NL only).
763 addDisplay(histInput + '\n');
764 if (!forceNext) { // Loop may break
765 options.displayOnly = true;
766 _readlineSync(options);
767 options.displayOnly = false;
768 }
769 } else if (res && res !== inputHistory[inputHistory.length - 1]) {
770 inputHistory = [res];
771 }
772 }
773
774 if (!forceNext && options.cd && res) {
775 args = parseCl(res);
776 switch (args[0].toLowerCase()) {
777 case 'cd':
778 if (args[1]) {
779 try {
780 process.chdir(replaceHomePath(args[1], true));
781 } catch (e) {
782 addDisplay(e + '');
783 }
784 }
785 forceNext = true;
786 break;
787 case 'pwd':
788 addDisplay(process.cwd());
789 forceNext = true;
790 break;
791 // no default
792 }
793 }
794
795 if (!forceNext && options.preCheck) {
796 resCheck = options.preCheck(res, options);
797 res = resCheck.res;
798 if (resCheck.forceNext) { forceNext = true; } // Don't switch to false.
799 }
800
801 if (!forceNext) {
802 if (!options.limitSrc.length ||
803 isMatched(res, options.limitSrc, options.caseSensitive)) { break; }
804 if (options.limitMessage) {
805 limitMessage = replacePlaceholder(options.limitMessage, _getPhContent);
806 }
807 }
808
809 addDisplay((limitMessage ? limitMessage + '\n' : '') +
810 replacePlaceholder(options.displaySrc + '', _getPhContent));
811 }
812 return toBool(res, options);
813}
814
815// for dev
816exports._DBG_set_useExt = function(val) { _DBG_useExt = val; };
817exports._DBG_set_checkOptions = function(val) { _DBG_checkOptions = val; };
818exports._DBG_set_checkMethod = function(val) { _DBG_checkMethod = val; };
819exports._DBG_clearHistory = function() { lastInput = ''; inputHistory = []; };
820
821// ------------------------------------
822
823exports.setDefaultOptions = function(options) {
824 defaultOptions = margeOptions(true, options);
825 return margeOptions(true); // copy
826};
827
828exports.question = function(query, options) {
829 /* eslint-disable key-spacing */
830 return getValidLine(margeOptions(margeOptions(true, options), {
831 display: query
832 }));
833 /* eslint-enable key-spacing */
834};
835
836exports.prompt = function(options) {
837 var readOptions = margeOptions(true, options);
838 readOptions.display = readOptions.prompt;
839 return getValidLine(readOptions);
840};
841
842exports.keyIn = function(query, options) {
843 /* eslint-disable key-spacing */
844 var readOptions = margeOptions(margeOptions(true, options), {
845 display: query,
846 keyIn: true,
847 keepWhitespace: true
848 });
849 /* eslint-enable key-spacing */
850
851 // char list
852 readOptions.limitSrc = readOptions.limit.filter(function(value) {
853 var type = typeof value;
854 return type === 'string' || type === 'number';
855 })
856 .map(function(text) { return replacePlaceholder(text + '', getPhCharlist); });
857 // pattern
858 readOptions.limit = escapePattern(readOptions.limitSrc.join(''));
859
860 ['trueValue', 'falseValue'].forEach(function(optionName) {
861 readOptions[optionName] = readOptions[optionName].reduce(function(comps, comp) {
862 var type = typeof comp;
863 if (type === 'string' || type === 'number') {
864 comps = comps.concat((comp + '').split(''));
865 } else { comps.push(comp); }
866 return comps;
867 }, []);
868 });
869
870 readOptions.display = replacePlaceholder(readOptions.display + '',
871 function(param) { return getPhContent(param, readOptions); });
872
873 return toBool(_readlineSync(readOptions), readOptions);
874};
875
876// ------------------------------------
877
878exports.questionEMail = function(query, options) {
879 if (query == null) { query = 'Input e-mail address :'; } // eslint-disable-line eqeqeq
880 /* eslint-disable key-spacing */
881 return exports.question(query, margeOptions({
882 // -------- default
883 hideEchoBack: false,
884 // http://www.w3.org/TR/html5/forms.html#valid-e-mail-address
885 limit: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
886 limitMessage: 'Input valid e-mail address, please.',
887 trueValue: null,
888 falseValue: null
889 }, options, {
890 // -------- forced
891 keepWhitespace: false,
892 cd: false
893 }));
894 /* eslint-enable key-spacing */
895};
896
897exports.questionNewPassword = function(query, options) {
898 /* eslint-disable key-spacing */
899 var resCharlist, min, max,
900 readOptions = margeOptions({
901 // -------- default
902 hideEchoBack: true,
903 mask: '*',
904 limitMessage: 'It can include: $<charlist>\n' +
905 'And the length must be: $<length>',
906 trueValue: null,
907 falseValue: null,
908 caseSensitive: true
909 }, options, {
910 // -------- forced
911 history: false,
912 cd: false,
913 // limit (by charlist etc.),
914 phContent: function(param) {
915 return param === 'charlist' ? resCharlist.text :
916 param === 'length' ? min + '...' + max : null;
917 }
918 }),
919 // added: charlist, min, max, confirmMessage, unmatchMessage
920 charlist, confirmMessage, unmatchMessage,
921 limit, limitMessage, res1, res2;
922 /* eslint-enable key-spacing */
923 options = options || {};
924
925 charlist = replacePlaceholder(
926 options.charlist ? options.charlist + '' : '$<!-~>', getPhCharlist);
927 if (isNaN(min = parseInt(options.min, 10)) || typeof min !== 'number') { min = 12; }
928 if (isNaN(max = parseInt(options.max, 10)) || typeof max !== 'number') { max = 24; }
929 limit = new RegExp('^[' + escapePattern(charlist) +
930 ']{' + min + ',' + max + '}$');
931 resCharlist = array2charlist([charlist], readOptions.caseSensitive, true);
932 resCharlist.text = joinChunks(resCharlist.values, resCharlist.suppressed);
933
934 confirmMessage = options.confirmMessage != null ? options.confirmMessage : // eslint-disable-line eqeqeq
935 'Reinput a same one to confirm it :';
936 unmatchMessage = options.unmatchMessage != null ? options.unmatchMessage : // eslint-disable-line eqeqeq
937 'It differs from first one.' +
938 ' Hit only the Enter key if you want to retry from first one.';
939
940 if (query == null) { query = 'Input new password :'; } // eslint-disable-line eqeqeq
941
942 limitMessage = readOptions.limitMessage;
943 while (!res2) {
944 readOptions.limit = limit;
945 readOptions.limitMessage = limitMessage;
946 res1 = exports.question(query, readOptions);
947
948 readOptions.limit = [res1, ''];
949 readOptions.limitMessage = unmatchMessage;
950 res2 = exports.question(confirmMessage, readOptions);
951 }
952
953 return res1;
954};
955
956function _questionNum(query, options, parser) {
957 var validValue;
958 function getValidValue(value) {
959 validValue = parser(value);
960 return !isNaN(validValue) && typeof validValue === 'number';
961 }
962 /* eslint-disable key-spacing */
963 exports.question(query, margeOptions({
964 // -------- default
965 limitMessage: 'Input valid number, please.'
966 }, options, {
967 // -------- forced
968 limit: getValidValue,
969 cd: false
970 // trueValue, falseValue, caseSensitive, keepWhitespace don't work.
971 }));
972 /* eslint-enable key-spacing */
973 return validValue;
974}
975exports.questionInt = function(query, options) {
976 return _questionNum(query, options, function(value) { return parseInt(value, 10); });
977};
978exports.questionFloat = function(query, options) {
979 return _questionNum(query, options, parseFloat);
980};
981
982exports.questionPath = function(query, options) {
983 /* eslint-disable key-spacing */
984 var validPath, error = '',
985 readOptions = margeOptions({
986 // -------- default
987 hideEchoBack: false,
988 limitMessage: '$<error(\n)>Input valid path, please.' +
989 '$<( Min:)min>$<( Max:)max>',
990 history: true,
991 cd: true
992 }, options, {
993 // -------- forced
994 keepWhitespace: false,
995 limit: function(value) {
996 var exists, stat, res;
997 value = replaceHomePath(value, true);
998 error = ''; // for validate
999 // mkdir -p
1000 function mkdirParents(dirPath) {
1001 dirPath.split(/\/|\\/).reduce(function(parents, dir) {
1002 var path = pathUtil.resolve((parents += dir + pathUtil.sep));
1003 if (!fs.existsSync(path)) {
1004 fs.mkdirSync(path);
1005 } else if (!fs.statSync(path).isDirectory()) {
1006 throw new Error('Non directory already exists: ' + path);
1007 }
1008 return parents;
1009 }, '');
1010 }
1011
1012 try {
1013 exists = fs.existsSync(value);
1014 validPath = exists ? fs.realpathSync(value) : pathUtil.resolve(value);
1015 // options.exists default: true, not-bool: no-check
1016 if (!options.hasOwnProperty('exists') && !exists ||
1017 typeof options.exists === 'boolean' && options.exists !== exists) {
1018 error = (exists ? 'Already exists' : 'No such file or directory') +
1019 ': ' + validPath;
1020 return false;
1021 }
1022 if (!exists && options.create) {
1023 if (options.isDirectory) {
1024 mkdirParents(validPath);
1025 } else {
1026 mkdirParents(pathUtil.dirname(validPath));
1027 fs.closeSync(fs.openSync(validPath, 'w')); // touch
1028 }
1029 validPath = fs.realpathSync(validPath);
1030 }
1031 if (exists && (options.min || options.max ||
1032 options.isFile || options.isDirectory)) {
1033 stat = fs.statSync(validPath);
1034 // type check first (directory has zero size)
1035 if (options.isFile && !stat.isFile()) {
1036 error = 'Not file: ' + validPath;
1037 return false;
1038 } else if (options.isDirectory && !stat.isDirectory()) {
1039 error = 'Not directory: ' + validPath;
1040 return false;
1041 } else if (options.min && stat.size < +options.min ||
1042 options.max && stat.size > +options.max) {
1043 error = 'Size ' + stat.size + ' is out of range: ' + validPath;
1044 return false;
1045 }
1046 }
1047 if (typeof options.validate === 'function' &&
1048 (res = options.validate(validPath)) !== true) {
1049 if (typeof res === 'string') { error = res; }
1050 return false;
1051 }
1052 } catch (e) {
1053 error = e + '';
1054 return false;
1055 }
1056 return true;
1057 },
1058 // trueValue, falseValue, caseSensitive don't work.
1059 phContent: function(param) {
1060 return param === 'error' ? error :
1061 param !== 'min' && param !== 'max' ? null :
1062 options.hasOwnProperty(param) ? options[param] + '' : '';
1063 }
1064 });
1065 // added: exists, create, min, max, isFile, isDirectory, validate
1066 /* eslint-enable key-spacing */
1067 options = options || {};
1068
1069 if (query == null) { query = 'Input path (you can "cd" and "pwd") :'; } // eslint-disable-line eqeqeq
1070
1071 exports.question(query, readOptions);
1072 return validPath;
1073};
1074
1075// props: preCheck, args, hRes, limit
1076function getClHandler(commandHandler, options) {
1077 var clHandler = {}, hIndex = {};
1078 if (typeof commandHandler === 'object') {
1079 Object.keys(commandHandler).forEach(function(cmd) {
1080 if (typeof commandHandler[cmd] === 'function') {
1081 hIndex[options.caseSensitive ? cmd : cmd.toLowerCase()] = commandHandler[cmd];
1082 }
1083 });
1084 clHandler.preCheck = function(res) {
1085 var cmdKey;
1086 clHandler.args = parseCl(res);
1087 cmdKey = clHandler.args[0] || '';
1088 if (!options.caseSensitive) { cmdKey = cmdKey.toLowerCase(); }
1089 clHandler.hRes =
1090 cmdKey !== '_' && hIndex.hasOwnProperty(cmdKey) ?
1091 hIndex[cmdKey].apply(res, clHandler.args.slice(1)) :
1092 hIndex.hasOwnProperty('_') ? hIndex._.apply(res, clHandler.args) : null;
1093 return {res: res, forceNext: false};
1094 };
1095 if (!hIndex.hasOwnProperty('_')) {
1096 clHandler.limit = function() { // It's called after preCheck.
1097 var cmdKey = clHandler.args[0] || '';
1098 if (!options.caseSensitive) { cmdKey = cmdKey.toLowerCase(); }
1099 return hIndex.hasOwnProperty(cmdKey);
1100 };
1101 }
1102 } else {
1103 clHandler.preCheck = function(res) {
1104 clHandler.args = parseCl(res);
1105 clHandler.hRes = typeof commandHandler === 'function' ?
1106 commandHandler.apply(res, clHandler.args) : true; // true for break loop
1107 return {res: res, forceNext: false};
1108 };
1109 }
1110 return clHandler;
1111}
1112
1113exports.promptCL = function(commandHandler, options) {
1114 /* eslint-disable key-spacing */
1115 var readOptions = margeOptions({
1116 // -------- default
1117 hideEchoBack: false,
1118 limitMessage: 'Requested command is not available.',
1119 caseSensitive: false,
1120 history: true
1121 }, options),
1122 // -------- forced
1123 // trueValue, falseValue, keepWhitespace don't work.
1124 // preCheck, limit (by clHandler)
1125 clHandler = getClHandler(commandHandler, readOptions);
1126 /* eslint-enable key-spacing */
1127 readOptions.limit = clHandler.limit;
1128 readOptions.preCheck = clHandler.preCheck;
1129 exports.prompt(readOptions);
1130 return clHandler.args;
1131};
1132
1133exports.promptLoop = function(inputHandler, options) {
1134 /* eslint-disable key-spacing */
1135 var readOptions = margeOptions({
1136 // -------- default
1137 hideEchoBack: false,
1138 trueValue: null,
1139 falseValue: null,
1140 caseSensitive: false,
1141 history: true
1142 }, options);
1143 /* eslint-enable key-spacing */
1144 while (true) { if (inputHandler(exports.prompt(readOptions))) { break; } }
1145 return;
1146};
1147
1148exports.promptCLLoop = function(commandHandler, options) {
1149 /* eslint-disable key-spacing */
1150 var readOptions = margeOptions({
1151 // -------- default
1152 hideEchoBack: false,
1153 limitMessage: 'Requested command is not available.',
1154 caseSensitive: false,
1155 history: true
1156 }, options),
1157 // -------- forced
1158 // trueValue, falseValue, keepWhitespace don't work.
1159 // preCheck, limit (by clHandler)
1160 clHandler = getClHandler(commandHandler, readOptions);
1161 /* eslint-enable key-spacing */
1162 readOptions.limit = clHandler.limit;
1163 readOptions.preCheck = clHandler.preCheck;
1164 while (true) {
1165 exports.prompt(readOptions);
1166 if (clHandler.hRes) { break; }
1167 }
1168 return;
1169};
1170
1171exports.promptSimShell = function(options) {
1172 /* eslint-disable key-spacing */
1173 return exports.prompt(margeOptions({
1174 // -------- default
1175 hideEchoBack: false,
1176 history: true
1177 }, options, {
1178 // -------- forced
1179 prompt: (function() {
1180 return IS_WIN ?
1181 '$<cwd>>' :
1182 // 'user@host:cwd$ '
1183 (process.env.USER || '') +
1184 (process.env.HOSTNAME ?
1185 '@' + process.env.HOSTNAME.replace(/\..*$/, '') : '') +
1186 ':$<cwdHome>$ ';
1187 })()
1188 }));
1189 /* eslint-enable key-spacing */
1190};
1191
1192function _keyInYN(query, options, limit) {
1193 var res;
1194 if (query == null) { query = 'Are you sure? :'; } // eslint-disable-line eqeqeq
1195 if ((!options || options.guide !== false) && (query += '')) {
1196 query = query.replace(/\s*:?\s*$/, '') + ' [y/n] :';
1197 }
1198 /* eslint-disable key-spacing */
1199 res = exports.keyIn(query, margeOptions(options, {
1200 // -------- forced
1201 hideEchoBack: false,
1202 limit: limit,
1203 trueValue: 'y',
1204 falseValue: 'n',
1205 caseSensitive: false
1206 // mask doesn't work.
1207 }));
1208 // added: guide
1209 /* eslint-enable key-spacing */
1210 return typeof res === 'boolean' ? res : '';
1211}
1212exports.keyInYN = function(query, options) { return _keyInYN(query, options); };
1213exports.keyInYNStrict = function(query, options) { return _keyInYN(query, options, 'yn'); };
1214
1215exports.keyInPause = function(query, options) {
1216 if (query == null) { query = 'Continue...'; } // eslint-disable-line eqeqeq
1217 if ((!options || options.guide !== false) && (query += '')) {
1218 query = query.replace(/\s+$/, '') + ' (Hit any key)';
1219 }
1220 /* eslint-disable key-spacing */
1221 exports.keyIn(query, margeOptions({
1222 // -------- default
1223 limit: null
1224 }, options, {
1225 // -------- forced
1226 hideEchoBack: true,
1227 mask: ''
1228 }));
1229 // added: guide
1230 /* eslint-enable key-spacing */
1231 return;
1232};
1233
1234exports.keyInSelect = function(items, query, options) {
1235 /* eslint-disable key-spacing */
1236 var readOptions = margeOptions({
1237 // -------- default
1238 hideEchoBack: false
1239 }, options, {
1240 // -------- forced
1241 trueValue: null,
1242 falseValue: null,
1243 caseSensitive: false,
1244 // limit (by items),
1245 phContent: function(param) {
1246 return param === 'itemsCount' ? items.length + '' :
1247 param === 'firstItem' ? (items[0] + '').trim() :
1248 param === 'lastItem' ? (items[items.length - 1] + '').trim() : null;
1249 }
1250 }),
1251 // added: guide, cancel
1252 keylist = '', key2i = {}, charCode = 49 /* '1' */, display = '\n';
1253 /* eslint-enable key-spacing */
1254 if (!Array.isArray(items) || !items.length || items.length > 35) {
1255 throw '`items` must be Array (max length: 35).';
1256 }
1257
1258 items.forEach(function(item, i) {
1259 var key = String.fromCharCode(charCode);
1260 keylist += key;
1261 key2i[key] = i;
1262 display += '[' + key + '] ' + (item + '').trim() + '\n';
1263 charCode = charCode === 57 /* '9' */ ? 97 /* 'a' */ : charCode + 1;
1264 });
1265 if (!options || options.cancel !== false) {
1266 keylist += '0';
1267 key2i['0'] = -1;
1268 display += '[0] ' +
1269 (options && options.cancel != null && typeof options.cancel !== 'boolean' ? // eslint-disable-line eqeqeq
1270 (options.cancel + '').trim() : 'CANCEL') + '\n';
1271 }
1272 readOptions.limit = keylist;
1273 display += '\n';
1274
1275 if (query == null) { query = 'Choose one from list :'; } // eslint-disable-line eqeqeq
1276 if ((query += '')) {
1277 if (!options || options.guide !== false) {
1278 query = query.replace(/\s*:?\s*$/, '') + ' [$<limit>] :';
1279 }
1280 display += query;
1281 }
1282
1283 return key2i[exports.keyIn(display, readOptions).toLowerCase()];
1284};
1285
1286// ======== DEPRECATED ========
1287function _setOption(optionName, args) {
1288 var options;
1289 if (args.length) { options = {}; options[optionName] = args[0]; }
1290 return exports.setDefaultOptions(options)[optionName];
1291}
1292exports.setPrint = function() { return _setOption('print', arguments); };
1293exports.setPrompt = function() { return _setOption('prompt', arguments); };
1294exports.setEncoding = function() { return _setOption('encoding', arguments); };
1295exports.setMask = function() { return _setOption('mask', arguments); };
1296exports.setBufferSize = function() { return _setOption('bufferSize', arguments); };