on master help sign in
[nodejs] respect lackof TTYs (#72)

* drop readline-sync + respect lackof TTYs in node cleanup a lil' * cleanup a lil'

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