diff --git a/lib/readline-sync.js b/lib/readline-sync.js index c2efd8b..aac2394 100644 --- a/lib/readline-sync.js +++ b/lib/readline-sync.js @@ -273,11 +273,11 @@ function readlineExt(options) { function _execFileSync(options, execOptions) { function getTempfile(name) { - var path = require('path'), filepath, suffix = '', fd; + var pathUtil = require('path'), filepath, suffix = '', fd; tempdir = tempdir || require('os').tmpdir(); while (true) { - filepath = path.join(tempdir, name + suffix); + filepath = pathUtil.join(tempdir, name + suffix); try { fd = fs.openSync(filepath, 'wx'); } catch (e) { @@ -662,10 +662,29 @@ function getPhCharlist(param) { return text; } +// cmd "arg" " a r g " 'a"r"g' "a""rg" "arg +function parseCL(cl) { + var reToken = new RegExp(/(\s*)(?:("|')(.*?)(?:\2|$)|(\S+))/g), matches, + taken = '', args = [], part; + cl = cl.trim(); + while ((matches = reToken.exec(cl))) { + part = matches[3] || matches[4]; + if (matches[1] && taken) { + args.push(taken); + taken = ''; + } + taken += part; + } + if (taken) { args.push(taken); } + return args; +} + function getValidLine(options, preCheck) { var res, forceNext, resCheck, limitMessage, - matches, histInput; + matches, histInput, args; function _getPhContent(param) { return getPhContent(param, options); } + function addDisplay(text) + { options.display += (/[^\r\n]$/.test(options.display) ? '\n' : '') + text; } options.limitSrc = options.limit; options.displaySrc = options.display; @@ -677,14 +696,15 @@ function getValidLine(options, preCheck) { forceNext = false; limitMessage = ''; + if (options.defaultInput && !res) { res = options.defaultInput; } + 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'; + addDisplay(histInput + '\n'); if (!forceNext) { // Loop may break options.displayOnly = true; _readlineSync(options); @@ -695,11 +715,34 @@ function getValidLine(options, preCheck) { } } + if (options.cd && res) { + args = parseCL(res); + switch (args[0].toLowerCase()) { + case 'cd': + if (args[1]) { + try { + process.chdir(args[1].trim() === '~' ? ( + IS_WIN ? (process.env.HOMEDRIVE || '') + (process.env.HOMEPATH || '') : + process.env.HOME || '') : args[1]); + } catch (e) { + addDisplay(e + ''); + } + } + forceNext = true; + break; + case 'pwd': + addDisplay(process.cwd()); + forceNext = true; + break; + } + } + 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; } @@ -707,9 +750,8 @@ function getValidLine(options, preCheck) { { limitMessage = replacePlaceholder(options.limitMessage, _getPhContent); } } - options.display += (/[^\n]$/.test(options.display) ? '\n' : '') + - (limitMessage ? limitMessage + '\n' : '') + - replacePlaceholder(options.displaySrc + '', _getPhContent); + addDisplay((limitMessage ? limitMessage + '\n' : '') + + replacePlaceholder(options.displaySrc + '', _getPhContent)); } return toBool(res, options); } @@ -823,22 +865,26 @@ exports.questionNewPassword = function(query, options) { param === 'length' ? min + '...' + max : null; } }), - // added: charlist, min, max, confirm - charlist, min, max, confirm, limit, resCharlist, limitMessage, res1, res2; + // added: charlist, min, max, confirmMessage, unmatchMessage + charlist, min, max, confirmMessage, unmatchMessage, + limit, resCharlist, limitMessage, res1, res2; + options = options || {}; charlist = replacePlaceholder( - options && options.charlist ? options.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; } + options.charlist ? options.charlist + '' : '${!-~}', getPhCharlist); + if (isNaN(min = parseInt(options.min, 10)) || typeof min !== 'number') { min = 12; } + if (isNaN(max = parseInt(options.max, 10)) || typeof max !== 'number') { max = 24; } limit = new RegExp('^[' + charlist.replace(/[^A-Za-z0-9_ ]/g, '\\$&') + ']{' + min + ',' + max + '}$'); resCharlist = array2charlist([charlist], readOptions.caseSensitive, true); resCharlist.text = joinChunks(resCharlist.values, resCharlist.suppressed); /* jshint eqnull:true */ - confirm = options && options.confirm != null ? options.confirm : + confirmMessage = options.confirmMessage != null ? options.confirmMessage : 'Reinput same one to confirm it :'; + unmatchMessage = options.unmatchMessage != null ? options.unmatchMessage : + 'It differs from first one.' + + ' Hit only Enter key if you want to retry from first one.'; /* jshint eqnull:false */ /* jshint eqnull:true */ @@ -852,9 +898,8 @@ exports.questionNewPassword = function(query, options) { 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(confirm, readOptions); + readOptions.limitMessage = unmatchMessage; + res2 = exports.question(confirmMessage, readOptions); } return res1; @@ -887,38 +932,85 @@ exports.questionFloat = function(query, options) { exports.questionPath = function(query, options) { var readOptions = margeOptions({ // -------- default - limitMessage: 'Input valid path, please.${( Min:)minSize}${( Max:)maxSize}', + hideEchoBack: false, + limitMessage: '${error(\n)}Input valid path, please.' + + '${( Min:)minSize}${( Max:)maxSize}', history: true, cd: true }, options, { // -------- forced keepWhitespace: false, limit: function(value) { - var stat; - if (!fs.existsSync(value)) { return; } - validPath = fs.realpathSync(value); - if (options) { - 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; } + var exists, stat, res, pathUtil = require('path'); + // mkdir -p + function mkdirParents(dirPath) { + dirPath.split(/\/|\\/).reduce(function(parents, dir) { + var path = pathUtil.resolve((parents += dir + pathUtil.sep)); + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + } else if (!fs.statSync(path).isDirectory()) { + throw new Error('Non directory already exists: ' + path); + } + return parents; + }, ''); + } + + try { + exists = fs.existsSync(value); + validPath = exists ? fs.realpathSync(value) : pathUtil.resolve(value); + // options.exists default: true, not-bool: no-check + if (!options.hasOwnProperty('exists') && !exists || + typeof options.exists === 'boolean' && options.exists !== exists) { + error = (exists ? 'Already exists' : 'No such file or directory') + + ': ' + validPath; + return; } - if (typeof options.validate === 'function' && !options.validate(validPath)) - { return; } + if (!exists && options.create) { + if (options.isDirectory) { + mkdirParents(validPath); + } else { + mkdirParents(pathUtil.dirname(validPath)); + fs.closeSync(fs.openSync(validPath, 'w')); // touch + } + validPath = fs.realpathSync(validPath); + } + if (exists && (options.minSize || options.maxSize || + options.isFile || options.isDirectory)) { + stat = fs.statSync(validPath); + // type check first (directory has zero size) + if (options.isFile && !stat.isFile()) { + error = 'Not file: ' + validPath; + return; + } else if (options.isDirectory && !stat.isDirectory()) { + error = 'Not directory: ' + validPath; + return; + } else if (options.minSize && stat.size < +options.minSize || + options.maxSize && stat.size > +options.maxSize) { + error = 'Size ' + stat.size +' is out of range: ' + validPath; + return; + } + } + if (typeof options.validate === 'function' && + (res = options.validate(validPath)) !== true) { + error = res + ''; + return; + } + } catch (e) { + error = e + ''; + return; } return true; }, // trueValue, falseValue are don't work. phContent: function(param) { - return param !== 'minSize' && param !== 'maxSize' ? null : - options && options.hasOwnProperty(param) ? options[param] + '' : ''; + return param === 'error' ? error : + param !== 'minSize' && param !== 'maxSize' ? null : + options.hasOwnProperty(param) ? options[param] + '' : ''; } }), - // added: minSize, maxSize, isFile, isDirectory, validate - validPath; + // added: exists, create, minSize, maxSize, isFile, isDirectory, validate + validPath, error = ''; + options = options || {}; /* jshint eqnull:true */ if (query == null) { query = 'Input path (you can "cd" and "pwd") :'; } @@ -931,6 +1023,7 @@ exports.questionPath = function(query, options) { exports.promptSimShell = function(options) { return exports.prompt(margeOptions({ // -------- default + hideEchoBack: false, history: true }, options, { // -------- forced @@ -946,7 +1039,26 @@ exports.promptSimShell = function(options) { })); }; -exports.keyInYN = function(query, options) { +exports.promptCL = function(options) { + return exports.prompt(margeOptions({ + // -------- default + hideEchoBack: false, + history: true + }, options, { + // -------- forced + prompt: (function() { + return IS_WIN ? + '${cwd}>' : + // 'user@host:cwd$ ' + (process.env.USER || '') + + (process.env.HOSTNAME ? + '@' + process.env.HOSTNAME.replace(/\..*$/, '') : '') + + ':${cwdHome}$ '; + })() + })); +}; + +function _keyInYN(query, options, limit) { var res; /* jshint eqnull:true */ if (query == null) { query = 'Are you sure? :'; } @@ -956,13 +1068,17 @@ exports.keyInYN = function(query, options) { res = exports.keyIn(query, margeOptions(options, { // -------- forced hideEchoBack: false, - limit: options && options.strict ? 'yn' : null, + limit: limit, trueValue: 'y', falseValue: 'n', caseSensitive: false })); + // added: guide return typeof res === 'boolean' ? 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) { /* jshint eqnull:true */ @@ -970,11 +1086,15 @@ exports.keyInPause = function(query, options) { /* jshint eqnull:false */ if ((!options || options.guide !== false) && (query += '')) { query = query.replace(/\s+$/, '') + ' (Hit any key)'; } - exports.keyIn(query, margeOptions(options, { + exports.keyIn(query, margeOptions({ + // -------- default + limit: null + }, options, { // -------- forced hideEchoBack: true, mask: '' })); + // added: guide return; }; @@ -989,6 +1109,7 @@ exports.keyInSelect = function(query, items, options) { caseSensitive: false // limit (by items) }), + // added: guide, cancel keylist = '', key2i = {}, charCode = 49 /* '1' */, display = '\n'; if (!Array.isArray(items) || items.length > 35) { throw '`items` must be Array (max length: 35).'; }