From d94e61388fb1c33fd82cc620ea21473d7c211225 Mon Sep 17 00:00:00 2001 From: anseki Date: Sat, 11 Apr 2015 12:11:52 +0900 Subject: [PATCH] `limit` `trueValue` `falseValue` accept function --- README.md | 27 ++-- lib/read.sh | 2 +- lib/readline-sync.js | 334 +++++++++++++++++++++++++++++++++---------- 3 files changed, 275 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 64ce97a..ef60812 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Synchronous [Readline](http://nodejs.org/api/readline.html) for interactively running to have a conversation with the user via a console(TTY). -readlineSync tries to read and write a console, even when the input or output is redirected like `your-script bar.log`. +readlineSync tries to make your script have a conversation with the user via a console, even when the input/output is redirected like `your-script bar.log`. ## Example @@ -11,19 +11,30 @@ var readlineSync = require('readline-sync'); var userName = readlineSync.question('May I have your name? :'); // Wait for user's response. var favFood = readlineSync.question('Hi ' + userName + '! What is your favorite food? :'); - console.log('Oh, ' + userName + ' likes ' + favFood + '!'); ``` +```console +May I have your name? :CookieMonster +Hi CookieMonster! What is your favorite food? :tofu +Oh, CookieMonster likes tofu! ``` -May I have your name? :AnSeki -Hi AnSeki! What is your favorite food? :chocolate -Oh, AnSeki likes chocolate! + +```js +var readlineSync = require('readline-sync'); + +// The user does not have to press an Enter key. +if (readlineSync.keyInYN('Do you want this module?')) { + // 'Y' key was pressed. + installModule(); +} else { + searchAnother(); +} ``` ## Installation -``` +```shell npm install readline-sync ``` @@ -184,7 +195,7 @@ console.log('Login ...'); The typed text is not shown on screen. -``` +```console PASSWORD :******** Login ... ``` @@ -216,7 +227,7 @@ if (readlineSync.keyIn('Are you sure? :', {limit: 'yn'}) === 'y') { // Accept 'y Type: Boolean Default: `false` -By default, the matching is case-insensitive when `limit` option (see [limit](#limit) option) is specified (i.e. `a` equals `A`). If `true` is specified, the matching is case-sensitive (i.e. `a` and `A` are different). +By default, the matching is case-insensitive when `limit` option (see [limit](#limit) option) is specified (i.e. `a` equals `A`). If `true` is specified, the matching is case-sensitive (i.e. `a` is different from `A`). ### noTrim diff --git a/lib/read.sh b/lib/read.sh index e33bc7c..8195497 100644 --- a/lib/read.sh +++ b/lib/read.sh @@ -4,7 +4,7 @@ # Copyright (c) 2015 anseki # Licensed under the MIT license. -# Use perl for compatibility of sed/awk of GNU / POSIX, BSD. (tr too, maybe) +# Use perl for compatibility of sed/awk of GNU / POSIX, BSD. (and tr) # Hide "\n" from shell by "\fNL" decode_arg() { diff --git a/lib/readline-sync.js b/lib/readline-sync.js index 8c0a4dc..c3bda16 100644 --- a/lib/readline-sync.js +++ b/lib/readline-sync.js @@ -33,10 +33,10 @@ var trueValue: [], falseValue: [] }, - useExt = false, fdR = 'none', fdW, ttyR, isRawMode = false, - extHostPath, extHostArgs, tempdir, salt = 0; + extHostPath, extHostArgs, tempdir, salt = 0, + _DBG_useExt = false, _DBG_checkOptions = false, _DBG_checkMethod = false; /* display: string @@ -59,6 +59,8 @@ function _readlineSync(options) { return res.input; } + if (_DBG_checkOptions) { _DBG_checkOptions(options); } + (function() { // open TTY var fsB, constants; @@ -136,7 +138,7 @@ function _readlineSync(options) { return true; } - if (useExt || !ttyR || + if (_DBG_useExt || !ttyR || typeof fdW !== 'number' && (options.display || !isCooked)) { input = tryExt(); return; @@ -235,6 +237,7 @@ function readlineExt(options) { if (childProc.execFileSync) { hostArgs = getHostArgs(options); + if (_DBG_checkMethod) { _DBG_checkMethod('execFileSync', hostArgs); } try { res.input = childProc.execFileSync(extHostPath, hostArgs, execOptions); } catch (e) { // non-zero exit code @@ -258,7 +261,7 @@ function readlineExt(options) { return res; } -// piping via files (node v0.10-) +// piping via files (for Node v0.10-) function _execFileSync(options, execOptions) { function getTempfile(name) { @@ -322,6 +325,7 @@ function _execFileSync(options, execOptions) { ' >"' + pathStdout + '"' + '; echo 1 >"' + pathDone + '"']; } + if (_DBG_checkMethod) { _DBG_checkMethod('_execFileSync', hostArgs); } try { childProc.spawn(shellPath, shellArgs, execOptions); } catch (e) { @@ -357,7 +361,7 @@ function _execFileSync(options, execOptions) { } function getHostArgs(options) { - // To send any text to crazy Windows shell safely. + // Send any text to crazy Windows shell safely. function encodeArg(arg) { return arg.replace(/[^\w\u0080-\uFFFF]/g, function(chr) { return '#' + chr.charCodeAt(0) + ';'; @@ -388,19 +392,20 @@ function getHostArgs(options) { function flattenArray(array, validator) { var flatArray = []; - function parseArray(array) { + function _flattenArray(array) { /* jshint eqnull:true */ if (array == null) { return; } /* jshint eqnull:false */ - else if (Array.isArray(array)) { array.forEach(parseArray); } + else if (Array.isArray(array)) { array.forEach(_flattenArray); } else if (!validator || validator(array)) { flatArray.push(array); } } - parseArray(array); + _flattenArray(array); return flatArray; } // margeOptions(options1, options2 ... ) -// margeOptions(true, options1, options2 ... ) // from defaultOptions +// margeOptions(true, options1, options2 ... ) +// arg1=true : Start from defaultOptions and pick elements of that. function margeOptions() { var optionsList = Array.prototype.slice.call(arguments), optionNames, fromDefault; @@ -419,11 +424,13 @@ function margeOptions() { /* jshint eqnull:false */ // ======== DEPRECATED ======== - if (optionsPart.hasOwnProperty('noEchoBack')) { + if (optionsPart.hasOwnProperty('noEchoBack') && + !optionsPart.hasOwnProperty('hideEchoBack')) { optionsPart.hideEchoBack = optionsPart.noEchoBack; delete optionsPart.noEchoBack; } - if (optionsPart.hasOwnProperty('noTrim')) { + if (optionsPart.hasOwnProperty('noTrim') && + !optionsPart.hasOwnProperty('keepWhitespace')) { optionsPart.keepWhitespace = optionsPart.noTrim; delete optionsPart.noTrim; } @@ -450,7 +457,7 @@ function margeOptions() { // ================ number case 'bufferSize': // * * if (!isNaN(value = parseInt(value, 10)) && typeof value === 'number') - { options[optionName] = value; } + { options[optionName] = value; } // limited updating (number is needed) break; // ================ boolean case 'hideEchoBack': // * * @@ -467,16 +474,13 @@ function margeOptions() { case 'limit': // * * readlineExt case 'trueValue': // * case 'falseValue': // * -/* jshint eqnull:true */ - value = value != null ? - flattenArray(value, function(value) { - return typeof value === 'string' || typeof value === 'number' || - value instanceof RegExp; - }) : []; -/* jshint eqnull:false */ - options[optionName] = value.map(function(value) { - return typeof value === 'string' ? value.replace(/[\r\n]/g, '') : value; - }); + options[optionName] = flattenArray(value, function(value) { + var type = typeof value; + return type === 'string' || type === 'number' || + type === 'function' || value instanceof RegExp; + }).map(function(value) { + return typeof value === 'string' ? value.replace(/[\r\n]/g, '') : value; + }); break; // ================ other case 'prompt': // * @@ -493,11 +497,11 @@ function margeOptions() { function isMatched(res, comps, caseSensitive) { return comps.some(function(comp) { - if (typeof comp === 'number') { comp += ''; } - return (typeof comp === 'string' ? ( - caseSensitive ? - res === comp : res.toLowerCase() === comp.toLowerCase() - ) : + var type = typeof comp; + if (type === 'number') { comp += ''; } + return (type === 'string' ? + (caseSensitive ? res === comp : res.toLowerCase() === comp.toLowerCase()) : + type === 'function' ? comp(res) : comp instanceof RegExp ? comp.test(res) : false); }); } @@ -511,8 +515,51 @@ function replacePlaceholder(text, generator) { }); } +function array2charlist(array, caseSensitive, collectSymbols) { + var values, group = [], groupClass = -1, charCode = 0, symbols = '', suppressed; + function addGroup(groups, group) { + if (group.length > 3) { // ellipsis + groups.push(group[0] + '...' + group[group.length - 1]); + suppressed = true; + } else if (group.length) { + groups = groups.concat(group); + } + return groups; + } + + values = array.reduce(function(chars, value) + { return chars.concat((value + '').split('')); }, []) + .reduce(function(groups, curChar) { + var curGroupClass, curCharCode; + if (!caseSensitive) { curChar = curChar.toUpperCase(); } + curGroupClass = /^\d$/.test(curChar) ? 1 : + /^[A-Z]$/.test(curChar) ? 2 : /^[a-z]$/.test(curChar) ? 3 : 0; + if (collectSymbols && curGroupClass === 0) { + symbols += curChar; + } else { + curCharCode = curChar.charCodeAt(0); + if (curGroupClass && curGroupClass === groupClass && + curCharCode === charCode + 1) { + group.push(curChar); + } else { + groups = addGroup(groups, group); + group = [curChar]; + groupClass = curGroupClass; + } + charCode = curCharCode; + } + return groups; + }, []); + values = addGroup(values, group); // last group + if (symbols) { values.push(symbols); suppressed = true; } + return {values: values, suppressed: suppressed}; +} + +function joinChunks(chunks, suppressed) + { return chunks.join(chunks.length > 2 ? ', ' : suppressed ? ' / ' : '/'); } + function placeholderInMessage(param, options) { - var text, values, group = [], groupClass = -1, charCode = 0, suppressed; + var text, values, resCharlist = {}; switch (param) { case 'hideEchoBack': case 'mask': @@ -533,42 +580,15 @@ function placeholderInMessage(param, options) { case 'falseValue': values = options[options.hasOwnProperty(param + 'Src') ? param + 'Src' : param]; if (options.keyIn) { // suppress - values = values.reduce(function(chars, value) { - return chars.concat((value + '').split('')); - }, []) - .reduce(function(groups, curChar) { - var curGroupClass, curCharCode; - if (!options.caseSensitive) { curChar = curChar.toUpperCase(); } - curGroupClass = /^\d$/.test(curChar) ? 1 : - /^[A-Z]$/.test(curChar) ? 2 : /^[a-z]$/.test(curChar) ? 3 : 0; - curCharCode = curChar.charCodeAt(0); - if (curGroupClass && curGroupClass === groupClass && - curCharCode === charCode + 1) { - group.push(curChar); - } else { - if (group.length > 3) { // ellipsis - groups.push(group[0] + ' ... ' + group[group.length - 1]); - suppressed = true; - } else { - groups = groups.concat(group); - } - group = [curChar]; - groupClass = curGroupClass; - } - charCode = curCharCode; - return groups; - }, []); - if (group.length > 3) { // ellipsis - values.push(group[0] + ' ... ' + group[group.length - 1]); - suppressed = true; - } else { - values = values.concat(group); - } + resCharlist = array2charlist(values, options.caseSensitive); + values = resCharlist.values; } else { - values = values.filter(function(value) - { return typeof value === 'string' || typeof value === 'number'; }); + values = values.filter(function(value) { + var type = typeof value; + return type === 'string' || type === 'number'; + }); } - text = values.join(values.length > 2 ? ', ' : suppressed ? ' / ' : '/'); + text = joinChunks(values, resCharlist.suppressed); break; case 'limitCount': case 'limitCountNotZero': @@ -579,6 +599,17 @@ function placeholderInMessage(param, options) { return text; } +function placeholderCharlist(param) { + var matches = /^(.)-(.)$/.exec(param), text = '', from, to, code, step; + if (!matches) { return; } + from = matches[1].charCodeAt(0); + to = matches[2].charCodeAt(0); + step = from < to ? 1 : -1; + for (code = from; code !== to + step; code += step) + { text += String.fromCharCode(code); } + return text; +} + function readlineWithOptions(options) { var res, generator = function(param) { return placeholderInMessage(param, options); }; @@ -608,7 +639,9 @@ function toBool(res, options) { } // for dev -exports._useExtSet = function(use) { useExt = use; }; +exports._DBG_set_useExt = function(val) { _DBG_useExt = val; }; +exports._DBG_set_checkOptions = function(val) { _DBG_checkOptions = val; }; +exports._DBG_set_checkMethod = function(val) { _DBG_checkMethod = val; }; exports.setDefault = function(options) { defaultOptions = margeOptions(true, options); @@ -638,27 +671,19 @@ exports.keyIn = function(query, options) { }), res; // char list - readOptions.limitSrc = readOptions.limit.filter(function(value) - { return typeof value === 'string' || typeof value === 'number'; }) - .map(function(text) { // placeholders - return replacePlaceholder(text + '', function(param) { // char list - var matches = /^(.)-(.)$/.exec(param), text = '', from, to, code, step; - if (!matches) { return; } - from = matches[1].charCodeAt(0); - to = matches[2].charCodeAt(0); - step = from < to ? 1 : -1; - for (code = from; code !== to + step; code += step) - { text += String.fromCharCode(code); } - return text; - }); - }); + readOptions.limitSrc = readOptions.limit.filter(function(value) { + var type = typeof value; + return type === 'string' || type === 'number'; + }) + .map(function(text) { return replacePlaceholder(text + '', placeholderCharlist); }); // pattern readOptions.limit = readOptions.limitSrc.join('').replace(/[^A-Za-z0-9_ ]/g, '\\$&'); ['trueValue', 'falseValue'].forEach(function(optionName) { var comps = []; readOptions[optionName].forEach(function(comp) { - if (typeof comp === 'string' || typeof comp === 'number') { + var type = typeof comp; + if (type === 'string' || type === 'number') { comps = comps.concat((comp + '').split('')); } else if (comp instanceof RegExp) { comps.push(comp); @@ -671,10 +696,161 @@ exports.keyIn = function(query, options) { function(param) { return placeholderInMessage(param, readOptions); }); res = _readlineSync(readOptions); - return toBool(res, readOptions); }; +// ------------------------------------ + +exports.questionEMail = function(query, options) { +/* jshint eqnull:true */ + if (query == null) { query = 'Input e-mail address :'; } +/* jshint eqnull:false */ + return exports.question(query, margeOptions({ + // -------- default + hideEchoBack: false, + // http://www.w3.org/TR/html5/forms.html#valid-e-mail-address + 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])?)*$/, + limitMessage: 'Input valid e-mail address, please.', + trueValue: null, + falseValue: null + }, options, { + // -------- forced + keepWhitespace: false, + })); +}; + +exports.questionNewPassword = function(query, options) { + var readOptions = margeOptions({ + // -------- default + hideEchoBack: true, + mask: '*', + limitMessage: 'It can include: ${charlist}, the length able to be: ${length}', + caseSensitive: true, + trueValue: null, + falseValue: null, + confirm: 'Reinput same one to confirm it :' + }, options, {/* forced limit */}), + // added: charlist, min, max, confirm + charlist, min, max, resCharlist, res1, res2, limit1, limitMessage1; + +/* jshint eqnull:true */ + if (query == null) { query = 'Input new password :'; } +/* jshint eqnull:false */ + + charlist = options && options.charlist ? options.charlist + '' : '${!-~}'; + charlist = replacePlaceholder(charlist, placeholderCharlist); + if (options) { min = options.min; max = options.max; } + if (isNaN(min = parseInt(min, 10)) || typeof min !== 'number') { min = 12; } + if (isNaN(max = parseInt(max, 10)) || typeof max !== 'number') { max = 24; } + limit1 = new RegExp( + '^[' + charlist.replace(/[^A-Za-z0-9_ ]/g, '\\$&') + ']{' + min + ',' + max + '}$'); + + if (readOptions.limitMessage) { + resCharlist = array2charlist([charlist], readOptions.caseSensitive, true); + resCharlist.text = joinChunks(resCharlist.values, resCharlist.suppressed); + limitMessage1 = replacePlaceholder(readOptions.limitMessage, + function(param) { + return param === 'charlist' ? resCharlist.text : + param === 'length' ? min + '...' + max : null; + }); + } + + while (!res2) { + readOptions.limit = limit1; + readOptions.limitMessage = limitMessage1; + res1 = exports.question(query, readOptions); + + readOptions.limit = [res1, '']; + readOptions.limitMessage = 'Two passwords don\'t match.' + + ' Hit only Enter key if you want to retry from first password.'; + res2 = exports.question(options.confirm, readOptions); + } + + return res1; +}; + +function _keyInYN(query, options, limit) { + var readOptions = margeOptions(options, { + // -------- forced + hideEchoBack: false, + limit: limit, + caseSensitive: false, + trueValue: 'y', + falseValue: 'n' + }), res; + +/* jshint eqnull:true */ + if (query == null) { query = 'Are you sure? :'; } +/* jshint eqnull:false */ + if ((query += '') && options.keyGuide !== false) + { query = query.replace(/\s*:?\s*$/, '') + ' [Y/N] :'; } + + res = exports.keyIn(query, readOptions); + if (typeof res !== 'boolean') { res = ''; } + return res; +} +exports.keyInYN = function(query, options) { return _keyInYN(query, options); }; +exports.keyInYNStrict = function(query, options) + { return _keyInYN(query, options, 'yn'); }; + +exports.keyInPause = function(query, options) { + var readOptions = margeOptions(options, { + // -------- forced + hideEchoBack: true, + mask: '' + }); + +/* jshint eqnull:true */ + if (query == null) { query = 'Continue...'; } +/* jshint eqnull:false */ + if ((query += '') && options.keyGuide !== false) + { query = query.replace(/\s+$/, '') + ' (Hit any key)'; } + + exports.keyIn(query, readOptions); + return; +}; + +exports.keyInSelect = function(query, items, options) { + var readOptions = margeOptions({ + // -------- default + hideEchoBack: false, + }, options, { + // -------- forced + caseSensitive: false, + trueValue: null, + falseValue: null + }), res, keylist = '', key2i = {}, charCode = 49 /* '1' */, display = '\n'; + if (!Array.isArray(items) || items.length > 35) + { throw '`items` must be Array (max length: 35).'; } + + items.forEach(function(item, i) { + var key = String.fromCharCode(charCode); + keylist += key; + key2i[key] = i; + display += '[' + key + '] ' + item.trim() + '\n'; + charCode = charCode === 57 /* '9' */ ? 65 /* 'A' */ : charCode + 1; + }); + if (options.cancel !== false) { + keylist += '0'; + key2i['0'] = -1; + display += '[' + '0' + '] CANCEL\n'; + } + readOptions.limit = keylist; + display += '\n'; + +/* jshint eqnull:true */ + if (query == null) { query = 'Choose one from list :'; } +/* jshint eqnull:false */ + if ((query += '')) { + if (options.keyGuide !== false) + { query = query.replace(/\s*:?\s*$/, '') + ' [${limit}] :'; } + display += query; + } + + res = exports.keyIn(display, readOptions); + return key2i[res.toUpperCase()]; +}; + // ======== DEPRECATED ======== function _setOption(optionName, args) { var options;