├── .gitignore ├── History.md ├── Makefile ├── Readme.md ├── lib ├── index.js └── types.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.1.3 / 2016-01-07 3 | ================== 4 | 5 | * package: upgrade `cli-prompt` (#10, @chemdemo) 6 | 7 | 1.1.2 - March 29, 2015 8 | ---------------------- 9 | * remove dumb `read` leftover 10 | 11 | 1.1.1 - March 29, 2015 12 | ---------------------- 13 | * swap back to `cli-prompt` for prompting 14 | * fix `prefix` option handling 15 | * tweak `separator` option to be fully customizable 16 | 17 | 1.1.0 - March 29, 2015 18 | ---------------------- 19 | * add `prefix` option 20 | * swap to `read` for prompting 21 | 22 | 1.0.0 - March 28, 2015 23 | ---------------------- 24 | * change `list` to `array` for consistency 25 | 26 | 0.5.1 - March 27, 2015 27 | ---------------------- 28 | * fix bug for required strings 29 | 30 | 0.5.0 - October 4, 2014 31 | ----------------------- 32 | * add list support 33 | 34 | 0.4.1 - May 28, 2014 35 | -------------------- 36 | * fix padding bug 37 | 38 | 0.4.0 - April 29, 2014 39 | ---------------------- 40 | * change `pad` option to a number 41 | 42 | 0.3.0 - April 29, 2014 43 | ---------------------- 44 | * fix `pad` option 45 | * add `hint` to schema for custom hints 46 | 47 | 0.2.0 - April 24, 2014 48 | ---------------------- 49 | * add `default` to schema 50 | * add `required` to schema 51 | 52 | 0.1.0 - March 27, 2013 53 | ---------------------- 54 | * add `pad` option 55 | * add `password` type 56 | * fix `color` option to check if one exists 57 | 58 | 0.0.3 - March 11, 2013 59 | ---------------------- 60 | * remove ? from boolean asks 61 | 62 | 0.0.2 - March 9, 2013 63 | --------------------- 64 | * remove debugger call :) 65 | 66 | 0.0.1 - March 8, 2013 67 | --------------------- 68 | :sparkles: -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | mocha = node_modules/.bin/mocha --reporter spec 3 | 4 | node_modules: package.json 5 | @npm install 6 | 7 | test: node_modules 8 | @$(mocha) 9 | 10 | test-debug: node_modules 11 | @$(mocha) debug 12 | 13 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # prompt-for 3 | 4 | Prompt the user for a series of answers. 5 | 6 | ## Installation 7 | 8 | $ npm install prompt-for 9 | 10 | ## Example 11 | 12 | ```js 13 | var prompt = require('prompt-for'); 14 | 15 | var schema = { 16 | name: 'string', 17 | siblings: 'number', 18 | birthday: 'date', 19 | deceased: 'boolean', 20 | secret: 'password' 21 | }; 22 | 23 | prompt(schema, function(err, answers){ 24 | assert(answers.name == 'Ian'); 25 | assert(answers.siblings == 2); 26 | assert(answers.birthday.getTime() == 1343260800000); 27 | assert(answers.deceased == false); 28 | assert(answers.secret == '1234'); 29 | }); 30 | ``` 31 | 32 | And if you're being lazy... 33 | 34 | ```js 35 | prompt(['name', 'website'], function(err, answers){ 36 | assert(answers.name == 'Ian'); 37 | assert(answers.website == 'ianstormtaylor.com'); 38 | }); 39 | ``` 40 | 41 | Or even... 42 | 43 | ```js 44 | prompt('name', function(err, answers){ 45 | assert(answers.name == 'Ian'); 46 | }); 47 | ``` 48 | 49 | ## Options 50 | 51 | Define or overwrite default values... 52 | 53 | * Default `boolean` value is **false** 54 | * Default `date` value is **now** 55 | 56 | ```js 57 | var prompt = require('prompt-for'); 58 | 59 | var schema = { 60 | name: {type:'string', default:'Ian'}, 61 | siblings: {type:'number', default:42}, 62 | birthday: {type:'date', default:'yesterday'}, 63 | deceased: {type:'boolean', default:true}, 64 | secret: {type:'password', default:'1234'} 65 | }; 66 | 67 | prompt(schema, function(err, answers){ 68 | assert(answers.name == 'Ian'); 69 | // ... 70 | }); 71 | ``` 72 | 73 | Disable required for `string` and `number`... 74 | 75 | By default, empty or incorrect answers when asked a string or a number, will be asked again. Set `required` to **false** allows you to skip the question. 76 | 77 | ```js 78 | var prompt = require('prompt-for'); 79 | 80 | var schema = { 81 | name: {type:'string', required:false}, 82 | siblings: {type:'number', required:false} 83 | }; 84 | 85 | prompt(schema, function(err, answers){ 86 | assert(answers.name == null); 87 | assert(answers.number == null); 88 | }); 89 | ``` 90 | 91 | ## API 92 | 93 | #### prompt(schema, [options], fn) 94 | 95 | Prompt the user with the given `schema` and optional `options`, then callback with `fn(err, answers)`. Options default to: 96 | 97 | { 98 | color: null, 99 | pad: true, 100 | prefix: '', 101 | separator: ': ' 102 | } 103 | 104 | ## License 105 | 106 | MIT -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | var debug = require('debug')('prompt-for'); 3 | var each = require('async').eachSeries; 4 | var max = require('max-component'); 5 | var pad = require('pad-component').left; 6 | var type = require('component-type'); 7 | var types = require('./types'); 8 | 9 | /** 10 | * Expose `promptFor`. 11 | */ 12 | 13 | module.exports = promptFor; 14 | 15 | /** 16 | * Prompt for a `schema` of responses. 17 | * 18 | * @param {Object or Array or String} schema 19 | * @param {Object} options (optional) 20 | * @property {String} color 21 | * @property {Number} pad 22 | * @param {Function} fn 23 | */ 24 | 25 | function promptFor(schema, options, fn){ 26 | if ('function' == typeof options) fn = options, options = null; 27 | options = options || {}; 28 | schema = normalize(schema, options); 29 | 30 | var questions = Object.keys(schema); 31 | var answers = {}; 32 | 33 | each(questions, ask, function(err){ 34 | if (err) return fn(err); 35 | debug('answered: %o', answers); 36 | fn(null, answers); 37 | }); 38 | 39 | function ask(key, done){ 40 | debug('asking for "%s"', key); 41 | var obj = schema[key]; 42 | var type = obj.type; 43 | var fn = types[type]; 44 | if (!fn) return done(new Error('Unrecognized type: "' + type + '".')); 45 | 46 | fn(obj, options, function(val){ 47 | debug('answered "%s" with "%s"', key, val); 48 | if (null == val && null != obj.default) val = obj.default; 49 | if (null == val && obj.required) return ask(key, done); 50 | answers[key] = val; 51 | done(); 52 | }); 53 | } 54 | } 55 | 56 | /** 57 | * Normalize a `schema`. 58 | * 59 | * @param {Object or Array or String} schema 60 | * @param {Object} options 61 | * @return {Object} 62 | */ 63 | 64 | function normalize(schema, options){ 65 | if ('string' == type(schema)) { 66 | var key = schema; 67 | schema = {}; 68 | schema[key] = {}; 69 | } 70 | 71 | if ('array' == type(schema)) { 72 | var arr = schema; 73 | schema = {}; 74 | arr.forEach(function(key){ 75 | schema[key] = {}; 76 | }); 77 | } 78 | 79 | if ('object' == type(schema)) { 80 | Object.keys(schema).forEach(function(key){ 81 | var val = schema[key]; 82 | if ('string' != type(val)) return; 83 | schema[key] = {}; 84 | schema[key].type = val; 85 | }); 86 | } 87 | 88 | var keys = Object.keys(schema); 89 | var width = max(keys, 'length') + (options.pad ? options.pad : 0); 90 | var padding = options.pad ? new Array(options.pad + 1).join(' ') : ''; 91 | 92 | keys.forEach(function(key){ 93 | var obj = schema[key]; 94 | obj.type = obj.type || 'string'; 95 | obj.required = !! obj.required; 96 | if ('boolean' == obj.type) obj.default = obj.default || false; 97 | if ('date' == obj.type) obj.default = obj.default || 'now'; 98 | 99 | if (obj.label) { 100 | if (options.pad) obj.label = padding + obj.label; 101 | } else { 102 | obj.label = key; 103 | if (options.pad) obj.label = pad(obj.label, width); 104 | } 105 | }); 106 | 107 | return schema; 108 | } -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | 2 | var chalk = require('chalk'); 3 | var date = require('date.js'); 4 | var fmt = require('util').format; 5 | var colors = chalk.styles; 6 | var prompt = require('cli-prompt'); 7 | 8 | /** 9 | * Prompt for a string for a given `schema` and `options`. 10 | * 11 | * @param {Object} schema 12 | * @param {Object} options 13 | * @param {Function} fn 14 | */ 15 | 16 | exports.string = function(schema, options, fn){ 17 | var msg = format(schema, options); 18 | prompt(msg, function(val){ 19 | if ('' === val && schema.required) return fn(); 20 | return fn(val); 21 | }); 22 | }; 23 | 24 | /** 25 | * Prompt for a password for a given `schema` and `options`. 26 | * 27 | * @param {Object} schema 28 | * @param {Object} options 29 | * @param {Function} fn 30 | */ 31 | 32 | exports.password = function(schema, options, fn){ 33 | var msg = format(schema, options); 34 | prompt.password(msg, function(val){ 35 | if ('' === val && schema.required) return fn(); 36 | return fn(val); 37 | }); 38 | }; 39 | 40 | /** 41 | * Prompt for a number for a given `schema` and `options`. 42 | * 43 | * @param {Object} schema 44 | * @param {Object} options 45 | * @param {Function} fn 46 | */ 47 | 48 | exports.number = function(schema, options, fn){ 49 | var msg = format(schema, options); 50 | prompt(msg, function(val){ 51 | val = parseFloat(val, 10); 52 | if (isNaN(val)) return fn(); 53 | fn(val); 54 | }); 55 | }; 56 | 57 | /** 58 | * Prompt for a boolean for a given `schema` and `options`. 59 | * 60 | * @param {Object} schema 61 | * @param {Object} options 62 | * @param {Function} fn 63 | */ 64 | 65 | exports.boolean = function(schema, options, fn){ 66 | var msg = format(schema, options); 67 | prompt(msg, function(val){ 68 | if ('' == val) return fn(); 69 | fn(/^(y|yes|true)$/i.test(val)); 70 | }); 71 | }; 72 | 73 | /** 74 | * Prompt for a date for a given `schema` and `options`. 75 | * 76 | * @param {Object} schema 77 | * @param {Object} options 78 | * @param {Function} fn 79 | */ 80 | 81 | exports.date = function(schema, options, fn){ 82 | var msg = format(schema, options); 83 | prompt(msg, function(val){ 84 | var ret = new Date(val); 85 | if (invalid(ret)) ret = date(val); 86 | return invalid(ret) ? fn() : fn(ret); 87 | }); 88 | }; 89 | 90 | /** 91 | * Prompt for an array of items for a given `schema` and `options`. 92 | * 93 | * @param {Object} schema 94 | * @param {Object} options 95 | * @param {Function} fn 96 | */ 97 | 98 | exports.array = function(schema, options, fn){ 99 | var msg = format(schema, options); 100 | prompt(msg, function(val){ 101 | var ret = val.split(/ *, */g); 102 | if (!(ret instanceof Array)) ret = []; 103 | return fn(ret); 104 | }); 105 | }; 106 | 107 | /** 108 | * Format the prompt message. 109 | * 110 | * @param {Object} schema 111 | * @param {Object} options 112 | */ 113 | 114 | function format(schema, options){ 115 | var color = options.color; 116 | var sep = options.separator || ': '; 117 | var prefix = options.prefix || ''; 118 | var label = color in colors ? chalk[color](schema.label) : schema.label; 119 | var help = hint(schema); 120 | var h = help ? chalk.gray(help) + ' ' : ''; 121 | return fmt('%s%s%s%s', prefix, label, sep, h); 122 | } 123 | 124 | /** 125 | * Format the default hint for a `schema`. 126 | * 127 | * @param {Object} schema 128 | * @return {String} 129 | */ 130 | 131 | function hint(schema){ 132 | if (schema.hint) return schema.hint; 133 | var def = schema.default; 134 | switch (schema.type) { 135 | case 'boolean': return def ? '(Y/n)' : '(y/N)'; 136 | case 'date': if (!def) return '(now)'; 137 | default: return def ? fmt('(%s)', def) : ''; 138 | } 139 | } 140 | 141 | /** 142 | * Test whether a date is invalid. 143 | * 144 | * @param {Date} date 145 | * @return {Boolean} 146 | */ 147 | 148 | function invalid(date){ 149 | return isNaN(date.getTime()); 150 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prompt-for", 3 | "description": "Prompt the user for a series of answers.", 4 | "repository": "git://github.com/segmentio/prompt-for.git", 5 | "version": "1.1.3", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "dependencies": { 9 | "async": "^0.7.0", 10 | "chalk": "^0.4.0", 11 | "cli-prompt": "^0.4.2", 12 | "component-type": "^1.0.0", 13 | "date.js": "^0.2.0", 14 | "debug": "^0.8.0", 15 | "extend": "^1.2.1", 16 | "max-component": "^1.0.0", 17 | "pad-component": "0.0.1" 18 | }, 19 | "devDependencies": { 20 | "mocha": "1.x" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require('assert'); 3 | var prompt = require('..'); 4 | 5 | describe('prompt-for', function(){ 6 | it('should expose a function', function(){ 7 | assert.equal(typeof prompt, 'function'); 8 | }); 9 | 10 | it('should prompt for a schema', function(done){ 11 | prompt({ one: 'string', two: 'string' }, function(err, answers){ 12 | if (err) return done(err); 13 | assert.equal(answers.one, 'one'); 14 | assert.equal(answers.two, 'two'); 15 | done(); 16 | }); 17 | answer('one'); 18 | answer('two'); 19 | }); 20 | 21 | it('should accept an array shorthand', function(done){ 22 | prompt(['one', 'two'], function(err, answers){ 23 | if (err) return done(err); 24 | assert.equal(answers.one, 'one'); 25 | assert.equal(answers.two, 'two'); 26 | done(); 27 | }); 28 | answer('one'); 29 | answer('two'); 30 | }); 31 | 32 | it('should accept a string shorthand', function(done){ 33 | prompt('one', function(err, answers){ 34 | if (err) return done(err); 35 | assert.equal(answers.one, 'one'); 36 | done(); 37 | }); 38 | answer('one'); 39 | }); 40 | 41 | it('should coerce numbers', function(done){ 42 | prompt({ number: 'number' }, function(err, answers){ 43 | if (err) return done(err); 44 | assert.equal(answers.number, 42); 45 | done(); 46 | }); 47 | answer('42'); 48 | }); 49 | 50 | it('should coerce booleans', function(done){ 51 | prompt({ boolean: 'boolean' }, function(err, answers){ 52 | if (err) return done(err); 53 | assert.equal(answers.boolean, true); 54 | done(); 55 | }); 56 | answer('yes'); 57 | }); 58 | 59 | it('should coerce dates', function(done){ 60 | prompt({ date: 'date' }, function(err, answers){ 61 | if (err) return done(err); 62 | assert.equal(answers.date.getTime(), 1343260800000); 63 | done(); 64 | }); 65 | answer('2012-07-26'); 66 | }); 67 | 68 | it('should coerce arrays', function(done){ 69 | prompt({ array: 'array' }, function(err, answers){ 70 | if (err) return done(err); 71 | assert(answers.array instanceof Array); 72 | assert.equal(answers.array[0], 'one'); 73 | assert.equal(answers.array[1], 'two'); 74 | assert.equal(answers.array[2], 'three'); 75 | done(); 76 | }); 77 | answer('one, two, three'); 78 | }); 79 | 80 | it('should apply defaults', function(done){ 81 | prompt({ number: { type: 'number', default: 42 }}, function(err, answers){ 82 | if (err) return done(err); 83 | assert.equal(answers.number, 42); 84 | done(); 85 | }); 86 | answer(''); 87 | }); 88 | 89 | it('should wait for required values', function(done){ 90 | prompt({ string: { required: true }}, function(err, answers){ 91 | if (err) return done(err); 92 | assert.equal(answers.string, '42'); 93 | done(); 94 | }); 95 | answer(''); 96 | answer('42'); 97 | }); 98 | 99 | it('should use a default for required values', function(done){ 100 | prompt({ number: { type: 'number', default: 42, required: true }}, function(err, answers){ 101 | if (err) return done(err); 102 | assert.equal(answers.number, 42); 103 | done(); 104 | }); 105 | answer(''); 106 | }); 107 | }); 108 | 109 | /** 110 | * Write an answer `str` to stdin. 111 | * 112 | * @param {String} str 113 | * @param {Function} fn 114 | */ 115 | 116 | function answer(str){ 117 | str.split('').forEach(press); 118 | press('', { name: 'enter' }); 119 | } 120 | 121 | /** 122 | * Trigger a keypress on stdin with `c` and `key`. 123 | * 124 | * @param {String} c 125 | * @param {Object} key 126 | */ 127 | 128 | function press(c, key){ 129 | process.stdin.emit('keypress', c, key); 130 | } --------------------------------------------------------------------------------