From bdd72b0037d2db2b6eb47e7ed10691776808f021 Mon Sep 17 00:00:00 2001 From: anseki Date: Mon, 13 Apr 2015 15:55:33 +0900 Subject: [PATCH] Add `displayOnly` --- README.md | 2 +- lib/read.cs.js | 7 +- lib/read.ps1 | 4 +- lib/read.sh | 5 + lib/readline-sync.js | 300 +++++++++++++++++++++++++++++-------------- 5 files changed, 219 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index ef60812..b9944fc 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 make your script have a conversation with the user via a console, even when the input/output is redirected like `your-script bar.log`. +readlineSync tries to let your script have a conversation with the user via a console, even when the input/output is redirected like `your-script bar.log`. ## Example diff --git a/lib/read.cs.js b/lib/read.cs.js index cdc9c63..e8fbed7 100644 --- a/lib/read.cs.js +++ b/lib/read.cs.js @@ -13,7 +13,7 @@ var PS_MSG = 'Microsoft Windows PowerShell is required.' + ' https://technet.microsoft.com/en-us/library/hh847837.aspx', - input, fso, tty, + input = '', fso, tty, options = (function(conf) { var options = {}, arg, args =// Array.prototype.slice.call(WScript.Arguments), (function() { @@ -53,6 +53,7 @@ var return options; })({ display: 'string', + displayOnly: 'boolean', keyIn: 'boolean', hideEchoBack: 'boolean', mask: 'string' @@ -60,10 +61,10 @@ var if (!options.hideEchoBack && !options.keyIn) { if (options.display) { writeTTY(options.display); } - input = readByFSO(); + if (!options.displayOnly) { input = readByFSO(); } } else if (options.hideEchoBack && !options.keyIn && !options.mask) { if (options.display) { writeTTY(options.display); } - input = readByPW(); + if (!options.displayOnly) { input = readByPW(); } } else { WScript.StdErr.WriteLine(PS_MSG); WScript.Quit(1); diff --git a/lib/read.ps1 b/lib/read.ps1 index 7599f40..938283c 100644 --- a/lib/read.ps1 +++ b/lib/read.ps1 @@ -6,6 +6,7 @@ Param( [string] $display, + [switch] $displayOnly, [switch] $keyIn, [switch] $hideEchoBack, [string] $mask, @@ -25,7 +26,7 @@ function decodeArg ($arg) { } $options = @{} -foreach ($arg in @('display', 'keyIn', 'hideEchoBack', 'mask', 'limit', 'caseSensitive')) { +foreach ($arg in @('display', 'displayOnly', 'keyIn', 'hideEchoBack', 'mask', 'limit', 'caseSensitive')) { $options.Add($arg, (Get-Variable $arg -ValueOnly)) } $argList = New-Object string[] $options.Keys.Count @@ -63,6 +64,7 @@ function writeTTY ($text) { if ($options.display) { writeTTY $options.display } +if ($options.displayOnly) { return "''" } if (-not $options.keyIn -and $options.hideEchoBack -and $options.mask -eq '*') { $inputTTY = execWithTTY ('$text = Read-Host -AsSecureString;' + diff --git a/lib/read.sh b/lib/read.sh index 8195497..07949e8 100644 --- a/lib/read.sh +++ b/lib/read.sh @@ -16,6 +16,7 @@ while [ $# -ge 1 ]; do arg="$(printf '%s' "$1" | grep -E '^-+[^-]+$' | tr '[A-Z]' '[a-z]' | tr -d '-')" case "$arg" in 'display') shift; options_display="$(decode_arg "$1")";; + 'displayOnly') options_displayOnly=true;; 'keyin') options_keyIn=true;; 'hideechoback') options_hideEchoBack=true;; 'mask') shift; options_mask="$(decode_arg "$1")";; @@ -60,6 +61,10 @@ replace_allchars() { ( if [ -n "$options_display" ]; then write_tty "$options_display" fi +if [ "$options_displayOnly" = true ]; then + printf "'%s'" '' + exit 0 +fi if [ "$is_cooked" = true ]; then stty --file=/dev/tty cooked 2>/dev/null || \ diff --git a/lib/readline-sync.js b/lib/readline-sync.js index c3bda16..24a702c 100644 --- a/lib/readline-sync.js +++ b/lib/readline-sync.js @@ -25,21 +25,24 @@ var mask: '*', limit: [], limitMessage: 'Input another, please.${( [)limit(])}', + trueValue: [], + falseValue: [], caseSensitive: false, keepWhitespace: false, encoding: 'utf8', bufferSize: 1024, print: void 0, - trueValue: [], - falseValue: [] + history: true }, fdR = 'none', fdW, ttyR, isRawMode = false, extHostPath, extHostArgs, tempdir, salt = 0, + lastInput = '', inputHistory = [], _DBG_useExt = false, _DBG_checkOptions = false, _DBG_checkMethod = false; /* display: string + displayOnly: boolean keyIn: boolean hideEchoBack: boolean mask: string @@ -148,6 +151,7 @@ function _readlineSync(options) { fs.writeSync(fdW, options.display); options.display = ''; } + if (options.displayOnly) { return; } if (!setRawMode(!isCooked)) { input = tryExt(); @@ -173,8 +177,7 @@ function _readlineSync(options) { } chunk = readSize > 0 ? buffer.toString(options.encoding, 0, readSize) : '\n'; - if (chunk && - typeof(line = (chunk.match(/^(.*?)[\r\n]/) || [])[1]) === 'string') { + if (chunk && typeof(line = (chunk.match(/^(.*?)[\r\n]/) || [])[1]) === 'string') { chunk = line; atEol = true; } @@ -202,13 +205,15 @@ function _readlineSync(options) { setRawMode(false); })(); - if (options.print && !silent) { // must at least write '\n' - options.print(displaySave + (options.hideEchoBack ? - (new Array(input.length + 1)).join(options.mask) : input) + '\n', + if (options.print && !silent) { + options.print(displaySave + (options.displayOnly ? '' : + (options.hideEchoBack ? (new Array(input.length + 1)).join(options.mask) + : input) + '\n'), // must at least write '\n' options.encoding); } - return (options.keepWhitespace || options.keyIn ? input : input.trim()); + return options.displayOnly ? '' : + (lastInput = options.keepWhitespace || options.keyIn ? input : input.trim()); } function readlineExt(options) { @@ -382,6 +387,7 @@ function getHostArgs(options) { return args; })({ display: 'string', + displayOnly: 'boolean', keyIn: 'boolean', hideEchoBack: 'boolean', mask: 'string', @@ -393,9 +399,9 @@ function getHostArgs(options) { function flattenArray(array, validator) { var flatArray = []; function _flattenArray(array) { -/* jshint eqnull:true */ + /* jshint eqnull:true */ if (array == null) { return; } -/* jshint eqnull:false */ + /* jshint eqnull:false */ else if (Array.isArray(array)) { array.forEach(_flattenArray); } else if (!validator || validator(array)) { flatArray.push(array); } } @@ -419,9 +425,9 @@ function margeOptions() { } return optionsList.reduce(function(options, optionsPart) { -/* jshint eqnull:true */ + /* jshint eqnull:true */ if (optionsPart == null) { return options; } -/* jshint eqnull:false */ + /* jshint eqnull:false */ // ======== DEPRECATED ======== if (optionsPart.hasOwnProperty('noEchoBack') && @@ -445,33 +451,31 @@ function margeOptions() { // _readlineSync defaultOptions // ================ string case 'mask': // * * - case 'encoding': // * * case 'limitMessage': // * -/* jshint eqnull:true */ + case 'encoding': // * * + /* jshint eqnull:true */ value = value != null ? value + '' : ''; -/* jshint eqnull:false */ + /* jshint eqnull:false */ if (value && optionName === 'mask' || optionName === 'encoding') { value = value.replace(/[\r\n]/g, ''); } options[optionName] = value; break; - // ================ number + // ================ number(int) case 'bufferSize': // * * if (!isNaN(value = parseInt(value, 10)) && typeof value === 'number') { options[optionName] = value; } // limited updating (number is needed) break; // ================ boolean + case 'displayOnly': // * + case 'keyIn': // * case 'hideEchoBack': // * * case 'caseSensitive': // * * case 'keepWhitespace': // * * - case 'keyIn': // * + case 'history': // * options[optionName] = !!value; break; - // ================ function - case 'print': // * * - options[optionName] = typeof value === 'function' ? value : void 0; - break; // ================ array - case 'limit': // * * readlineExt + case 'limit': // * * to string for readlineExt case 'trueValue': // * case 'falseValue': // * options[optionName] = flattenArray(value, function(value) { @@ -482,12 +486,16 @@ function margeOptions() { return typeof value === 'string' ? value.replace(/[\r\n]/g, '') : value; }); break; + // ================ function + case 'print': // * * + options[optionName] = typeof value === 'function' ? value : void 0; + break; // ================ other case 'prompt': // * - case 'display': // * readlineExt -/* jshint eqnull:true */ + case 'display': // * + /* jshint eqnull:true */ options[optionName] = value != null ? value : ''; -/* jshint eqnull:false */ + /* jshint eqnull:false */ break; } }); @@ -498,11 +506,11 @@ function margeOptions() { function isMatched(res, comps, caseSensitive) { return comps.some(function(comp) { var type = typeof comp; - if (type === 'number') { comp += ''; } - return (type === 'string' ? + return type === 'string' ? (caseSensitive ? res === comp : res.toLowerCase() === comp.toLowerCase()) : + type === 'number' ? parseFloat(res) === comp : type === 'function' ? comp(res) : - comp instanceof RegExp ? comp.test(res) : false); + comp instanceof RegExp ? comp.test(res) : false; }); } @@ -558,8 +566,8 @@ function array2charlist(array, caseSensitive, collectSymbols) { function joinChunks(chunks, suppressed) { return chunks.join(chunks.length > 2 ? ', ' : suppressed ? ' / ' : '/'); } -function placeholderInMessage(param, options) { - var text, values, resCharlist = {}; +function getPhContent(param, options) { + var text, values, resCharlist = {}, arg; switch (param) { case 'hideEchoBack': case 'mask': @@ -567,7 +575,7 @@ function placeholderInMessage(param, options) { case 'keepWhitespace': case 'encoding': case 'bufferSize': - case 'input': + case 'history': text = options.hasOwnProperty(param) ? options[param] + '' : ''; break; case 'prompt': @@ -593,13 +601,25 @@ function placeholderInMessage(param, options) { case 'limitCount': case 'limitCountNotZero': text = options[options.hasOwnProperty('limitSrc') ? 'limitSrc' : 'limit'].length; - text = (text ? text : param === 'limitCountNotZero' ? '' : text) + ''; + text = text || param !== 'limitCountNotZero' ? text + '' : ''; break; + case 'lastInput': + text = lastInput; + break; + case 'cwd': + case 'CWD': + text = process.cwd(); + if (param === 'CWD') { text = require('path').basename(text); } + break; + default: // with arg + if (typeof(arg = (param.match(/^history_m(\d+)$/) || [])[1]) === 'string') { + text = inputHistory[inputHistory.length - arg] || ''; + } } return text; } -function placeholderCharlist(param) { +function getPhCharlist(param) { var matches = /^(.)-(.)$/.exec(param), text = '', from, to, code, step; if (!matches) { return; } from = matches[1].charCodeAt(0); @@ -610,24 +630,56 @@ function placeholderCharlist(param) { return text; } -function readlineWithOptions(options) { - var res, - generator = function(param) { return placeholderInMessage(param, options); }; +function readlineWithOptions(options, preCheck) { + var res, forceNext, resCheck, limitMessage, + matches, histInput; + function _getPhContent(param) { return getPhContent(param, options); } + options.limitSrc = options.limit; options.displaySrc = options.display; options.limit = ''; // for readlineExt - options.display = replacePlaceholder(options.display + '', generator); + options.display = replacePlaceholder(options.display + '', _getPhContent); + while (true) { res = _readlineSync(options); - if (!options.limitSrc.length || - isMatched(res, options.limitSrc, options.caseSensitive)) { break; } - options.input = res; // for placeholder - options.display += (options.display ? '\n' : '') + - (options.limitMessage ? - replacePlaceholder(options.limitMessage, generator) + '\n' : '') + - replacePlaceholder(options.displaySrc + '', generator); + forceNext = false; + limitMessage = ''; + + if (options.history) { + if ((matches = /^\s*\!(?:\!|-1)(:p)?\s*$/.exec(res))) { // `!!` `!-1` +`:p` + histInput = inputHistory[0] || ''; + if (matches[1]) { forceNext = true; } // only display + else { res = histInput; } // replace input + // Show it even if it is empty (NL only). + options.display += + (/[^\n]$/.test(options.display) ? '\n' : '') + histInput + '\n'; + if (!forceNext) { // Loop may break + options.displayOnly = true; + _readlineSync(options); + options.displayOnly = false; + } + } else if (res && res !== inputHistory[inputHistory.length - 1]) { + inputHistory = [res]; + } + } + + if (preCheck) { + resCheck = preCheck(res, options); + res = resCheck.res; + if (resCheck.forceNext) { forceNext = true; } // Don't switch to false. + } + if (!forceNext) { + if (!options.limitSrc.length || + isMatched(res, options.limitSrc, options.caseSensitive)) { break; } + if (options.limitMessage) + { limitMessage = replacePlaceholder(options.limitMessage, _getPhContent); } + } + + options.display += (/[^\n]$/.test(options.display) ? '\n' : '') + + (limitMessage ? limitMessage + '\n' : '') + + replacePlaceholder(options.displaySrc + '', _getPhContent); } - return res; + return toBool(res, options); } function toBool(res, options) { @@ -642,25 +694,25 @@ function toBool(res, options) { 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._DBG_clearHistory = function() { lastInput = ''; inputHistory = []; }; exports.setDefault = function(options) { defaultOptions = margeOptions(true, options); return margeOptions(true); // copy }; +// ------------------------------------ + exports.prompt = function(options) { - var readOptions = margeOptions(true, options), res; + var readOptions = margeOptions(true, options); readOptions.display = readOptions.prompt; - res = readlineWithOptions(readOptions); - return toBool(res, readOptions); + return readlineWithOptions(readOptions); }; exports.question = function(query, options) { - var readOptions = margeOptions(margeOptions(true, options), { + return readlineWithOptions(margeOptions(margeOptions(true, options), { display: query - }), - res = readlineWithOptions(readOptions); - return toBool(res, readOptions); + })); }; exports.keyIn = function(query, options) { @@ -668,14 +720,14 @@ exports.keyIn = function(query, options) { display: query, keyIn: true, keepWhitespace: true - }), res; + }); // char list readOptions.limitSrc = readOptions.limit.filter(function(value) { var type = typeof value; return type === 'string' || type === 'number'; }) - .map(function(text) { return replacePlaceholder(text + '', placeholderCharlist); }); + .map(function(text) { return replacePlaceholder(text + '', getPhCharlist); }); // pattern readOptions.limit = readOptions.limitSrc.join('').replace(/[^A-Za-z0-9_ ]/g, '\\$&'); @@ -693,18 +745,17 @@ exports.keyIn = function(query, options) { }); readOptions.display = replacePlaceholder(readOptions.display + '', - function(param) { return placeholderInMessage(param, readOptions); }); + function(param) { return getPhContent(param, readOptions); }); - res = _readlineSync(readOptions); - return toBool(res, readOptions); + return toBool(_readlineSync(readOptions), readOptions); }; // ------------------------------------ exports.questionEMail = function(query, options) { -/* jshint eqnull:true */ + /* jshint eqnull:true */ if (query == null) { query = 'Input e-mail address :'; } -/* jshint eqnull:false */ + /* jshint eqnull:false */ return exports.question(query, margeOptions({ // -------- default hideEchoBack: false, @@ -724,35 +775,44 @@ exports.questionNewPassword = function(query, options) { // -------- default hideEchoBack: true, mask: '*', - limitMessage: 'It can include: ${charlist}, the length able to be: ${length}', - caseSensitive: true, + limitMessage: 'It can include: ${charlist}\n' + + 'The length able to be: ${length}', 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; + caseSensitive: true + }, options), + // forced: limit + // added: charlist, min, max, confirm + charlist, min, max, confirm, resCharlist, res1, res2, limit1, limitMessage1; -/* jshint eqnull:true */ + function phSpecial(param) { + return param === 'charlist' ? resCharlist.text : + param === 'length' ? min + '...' + max : null; + } + + // bug? `eqnull:true` doesn't work + /* jshint ignore:start */ if (query == null) { query = 'Input new password :'; } -/* jshint eqnull:false */ + /* jshint ignore:end */ charlist = options && options.charlist ? options.charlist + '' : '${!-~}'; - charlist = replacePlaceholder(charlist, placeholderCharlist); + charlist = replacePlaceholder(charlist, getPhCharlist); 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 + '}$'); + /* jshint eqnull:true */ + confirm = options && options.confirm != null ? options.confirm : + 'Reinput same one to confirm it :'; + /* jshint eqnull:false */ + + 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; - }); + // getPhContent is called by readlineWithOptions + limitMessage1 = replacePlaceholder(readOptions.limitMessage, phSpecial); } while (!res2) { @@ -763,31 +823,84 @@ exports.questionNewPassword = function(query, options) { 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); + // getPhContent is called by readlineWithOptions + res2 = exports.question(replacePlaceholder(confirm, phSpecial), readOptions); } return res1; }; +function _questionParse(query, options, fncParse) { + var validValue; + function getValidValue(value) { + validValue = fncParse(value); + return !isNaN(validValue) && typeof validValue === 'number'; + } + exports.question(query, margeOptions(options, { + // -------- forced + limit: getValidValue + // trueValue, falseValue are don't work. + })); + return validValue; +} +exports.questionInt = function(query, options) { + return _questionParse(query, options, function(value) { return parseInt(value, 10); }); +}; +exports.questionFloat = function(query, options) + { return _questionParse(query, options, parseFloat); }; + +exports.questionPath = function(query, options) { + var fs = require('fs'), validPath; + function getValidPath(value) { + var stat; + if (!fs.existsSync(value)) { return; } + validPath = fs.realpathSync(value); + if (options.minSize || options.maxSize || options.isFile || options.isDirectory) { + stat = fs.statSync(validPath); + if (options.minSize && stat.size < +options.minSize || + options.maxSize && stat.size > +options.maxSize || + options.isFile && !stat.isFile() || + options.isDirectory && !stat.isDirectory()) { return false; } + } + if (typeof options.validate === 'function' && !options.validate(validPath)) + { return false; } + return true; + } + exports.question(query, margeOptions(options, { + // -------- forced + limit: getValidPath + // trueValue, falseValue are don't work. + })); + // added: minSize, maxSize, isFile, isDirectory, validate + return validPath; +}; + +exports.promptCWD = function(options) { + var readOptions = margeOptions({ + // -------- default + prompt: '[${cwd}]$ ' + }, options); + return exports.prompt(readOptions); +}; + function _keyInYN(query, options, limit) { var readOptions = margeOptions(options, { // -------- forced hideEchoBack: false, limit: limit, - caseSensitive: false, trueValue: 'y', - falseValue: 'n' + falseValue: 'n', + caseSensitive: false }), res; -/* jshint eqnull:true */ + /* jshint eqnull:true */ if (query == null) { query = 'Are you sure? :'; } -/* jshint eqnull:false */ - if ((query += '') && options.keyGuide !== false) + /* jshint eqnull:false */ + if ((query += '') && options.guide !== false) { query = query.replace(/\s*:?\s*$/, '') + ' [Y/N] :'; } res = exports.keyIn(query, readOptions); - if (typeof res !== 'boolean') { res = ''; } - return res; + return typeof res === 'boolean' ? res : ''; } exports.keyInYN = function(query, options) { return _keyInYN(query, options); }; exports.keyInYNStrict = function(query, options) @@ -800,10 +913,10 @@ exports.keyInPause = function(query, options) { mask: '' }); -/* jshint eqnull:true */ + /* jshint eqnull:true */ if (query == null) { query = 'Continue...'; } -/* jshint eqnull:false */ - if ((query += '') && options.keyGuide !== false) + /* jshint eqnull:false */ + if ((query += '') && options.guide !== false) { query = query.replace(/\s+$/, '') + ' (Hit any key)'; } exports.keyIn(query, readOptions); @@ -816,10 +929,10 @@ exports.keyInSelect = function(query, items, options) { hideEchoBack: false, }, options, { // -------- forced - caseSensitive: false, trueValue: null, - falseValue: null - }), res, keylist = '', key2i = {}, charCode = 49 /* '1' */, display = '\n'; + falseValue: null, + caseSensitive: false + }), keylist = '', key2i = {}, charCode = 49 /* '1' */, display = '\n'; if (!Array.isArray(items) || items.length > 35) { throw '`items` must be Array (max length: 35).'; } @@ -838,17 +951,16 @@ exports.keyInSelect = function(query, items, options) { readOptions.limit = keylist; display += '\n'; -/* jshint eqnull:true */ + /* jshint eqnull:true */ if (query == null) { query = 'Choose one from list :'; } -/* jshint eqnull:false */ + /* jshint eqnull:false */ if ((query += '')) { - if (options.keyGuide !== false) + if (options.guide !== false) { query = query.replace(/\s*:?\s*$/, '') + ' [${limit}] :'; } display += query; } - res = exports.keyIn(display, readOptions); - return key2i[res.toUpperCase()]; + return key2i[exports.keyIn(display, readOptions).toUpperCase()]; }; // ======== DEPRECATED ========