2 * prompt.js: Simple prompt for prompting information from the command line
4 * (C) 2010, Nodejitsu Inc.
8 var events = require('events'),
9 async = require('async'),
10 colors = require('colors'),
11 winston = require('winston'),
15 // ### @private function capitalize (str)
16 // #### str {string} String to capitalize
17 // Capitalizes the string supplied.
19 function capitalize(str) {
20 return str.charAt(0).toUpperCase() + str.slice(1);
24 // Expose version using `pkginfo`
26 require('pkginfo')(module, 'version');
28 var stdin, stdout, history = [];
29 var prompt = module.exports = Object.create(events.EventEmitter.prototype);
30 var logger = prompt.logger = new winston.Logger({
31 transports: [new (winston.transports.Console)()]
34 prompt.started = false;
35 prompt.paused = false;
36 prompt.allowEmpty = false;
37 prompt.message = 'prompt';
38 prompt.delimiter = ': ';
41 // Create an empty object for the properties
44 prompt.properties = {};
47 // Setup the default winston logger to use
48 // the `cli` levels and colors.
53 // ### function start (options)
54 // #### @options {Object} **Optional** Options to consume by prompt
55 // Starts the prompt by listening to the appropriate events on `options.stdin`
56 // and `options.stdout`. If no streams are supplied, then `process.stdin`
57 // and `process.stdout` are used, respectively.
59 prompt.start = function (options) {
64 options = options || {};
65 stdin = options.stdin || process.openStdin();
66 stdout = options.stdout || process.stdout;
69 // By default: Remeber the last `10` prompt property /
70 // answer pairs and don't allow empty responses globally.
72 prompt.memory = options.memory || 10;
73 prompt.allowEmpty = options.allowEmpty || false;
74 prompt.message = options.message || prompt.message;
75 prompt.delimiter = options.delimiter || prompt.delimiter;
77 if (process.platform !== 'win32') {
78 // windows falls apart trying to deal with SIGINT
79 process.on('SIGINT', function () {
86 prompt.started = true;
91 // ### function pause ()
92 // Pauses input coming in from stdin
94 prompt.pause = function () {
95 if (!prompt.started || prompt.paused) {
100 prompt.emit('pause');
101 prompt.paused = true;
106 // ### function resume ()
107 // Resumes input coming in from stdin
109 prompt.resume = function () {
110 if (!prompt.started || !prompt.paused) {
115 prompt.emit('resume');
116 prompt.paused = false;
121 // ### function history (search)
122 // #### @search {Number|string} Index or property name to find.
123 // Returns the `property:value` pair from within the prompts
126 prompt.history = function (search) {
127 if (typeof search === 'number') {
128 return history[search] || {};
131 var names = history.map(function (pair) {
132 return typeof pair.property === 'string'
134 : pair.property.name;
137 if (~names.indexOf(search)) {
141 return history.filter(function (name) {
142 return typeof pair.property === 'string'
143 ? pair.property === name
144 : pair.property.name === name;
149 // ### function get (msg, [validator,] callback)
150 // #### @msg {Array|Object|string} Set of variables to get input for.
151 // #### @callback {function} Continuation to pass control to when complete.
152 // Gets input from the user via stdin for the specified message(s) `msg`.
154 prompt.get = function (msg, callback) {
155 var vars = !Array.isArray(msg) ? [msg] : msg,
158 vars = vars.map(function (v) {
159 if (typeof v === 'string') {
163 return prompt.properties[v] || v;
166 function get(target, next) {
167 prompt.getInput(target, function (err, line) {
172 var name = target.name || target;
178 async.forEachSeries(vars, get, function (err) {
179 return err ? callback(err) : callback(null, result);
186 // ### function getInput (msg, validator, callback)
187 // #### @msg {Object|string} Variable to get input for.
188 // #### @callback {function} Continuation to pass control to when complete.
189 // Gets input from the user via stdin for the specified message `msg`.
191 prompt.getInput = function (prop, callback) {
192 var name = prop.message || prop.name || prop,
193 propName = prop.name || prop,
194 delim = prompt.delimiter,
195 raw = [prompt.message, delim + name.grey, delim.grey],
196 read = prop.hidden ? prompt.readLineHidden : prompt.readLine,
199 if (prompt.override && prompt.override[propName]) {
200 return callback (null, prompt.override[propName])
204 raw.splice(2, -1, ' (' + prop.default + ')');
207 // Calculate the raw length and colorize the prompt
208 length = raw.join('').length;
213 prop.help.forEach(function (line) {
219 prompt.emit('prompt', prop);
221 read.call(null, function (err, line) {
223 return callback(err);
226 if (!line || line === '') {
227 line = prop.default || line;
230 if (!prop.validator && prop.empty !== false) {
231 logger.input(line.yellow);
232 return callback(null, line);
236 validator = prop.validator;
238 function next(valid) {
239 if (arguments.length < 1) {
243 if (prop.empty === false && valid) {
244 valid = line.length > 0;
246 prop.warning = prop.warning || 'You must supply a value.';
251 logger.error('Invalid input for ' + name.grey);
253 logger.error(prop.warning);
256 prompt.emit('invalid', prop, line);
257 return prompt.getInput(prop, callback);
261 // Log the resulting line, append this `property:value`
262 // pair to the history for `prompt` and respond to
265 logger.input(line.yellow);
266 prompt._remember(prop, line);
267 callback(null, line);
271 if (validator.test) {
272 valid = validator.test(line)
274 else if (typeof validator === 'function') {
275 return validator.length < 2
276 ? next(validator(line))
277 : validator(line, next);
280 return callback(new Error('Invalid valiator: ' + typeof validator));
291 // ### function addProperties (obj, properties, callback)
292 // #### @obj {Object} Object to add properties to
293 // #### @properties {Array} List of properties to get values for
294 // #### @callback {function} Continuation to pass control to when complete.
295 // Prompts the user for values each of the `properties` if `obj` does not already
296 // have a value for the property. Responds with the modified object.
298 prompt.addProperties = function (obj, properties, callback) {
299 properties = properties.filter(function (prop) {
300 return typeof obj[prop] === 'undefined';
303 if (properties.length === 0) {
304 return callback(obj);
307 prompt.get(properties, function (err, results) {
309 return callback(err);
312 return callback(null, obj);
315 function putNested (obj, path, value) {
318 while (path.length > 1) {
327 last[path.shift()] = value;
330 Object.keys(results).forEach(function (key) {
331 putNested(obj, key.split('.'), results[key]);
341 // ### function readLine (callback)
342 // #### @callback {function} Continuation to respond to when complete
343 // Gets a single line of input from the user.
345 prompt.readLine = function (callback) {
346 var value = '', buffer = '';
348 stdin.setEncoding('utf8');
349 stdin.on('error', callback);
350 stdin.on('data', function data (chunk) {
351 value += buffer + chunk;
353 value = value.replace(/\r/g, '');
354 if (value.indexOf('\n') !== -1) {
355 if (value !== '\n') {
356 value = value.replace(/^\n+/, '');
359 buffer = value.substr(value.indexOf('\n'));
360 value = value.substr(0, value.indexOf('\n'));
362 stdin.removeListener('data', data);
363 stdin.removeListener('error', callback);
364 value = value.trim();
365 callback(null, value);
373 // ### function readLineHidden (callback)
374 // #### @callback {function} Continuation to respond to when complete
375 // Gets a single line of hidden input (i.e. `rawMode = true`) from the user.
377 prompt.readLineHidden = function (callback) {
381 // Ignore errors from `.setRawMode()` so that `prompt` can
382 // be scripted in child processes.
384 try { tty.setRawMode(true) }
388 stdin.on('error', callback);
389 stdin.on('data', function data (line) {
391 for(var i = 0; i < line.length; i++) {
394 case '\n': case '\r': case '\r\n': case '\u0004':
395 try { tty.setRawMode(false) }
397 stdin.removeListener('data', data);
398 stdin.removeListener('error', callback);
399 value = value.trim();
401 stdout.flush && stdout.flush();
403 return callback(null, value);
404 case '\x7f': case'\x08':
405 value = value.slice(0,-1);
407 case '\u0003': case '\0':
422 // ### @private function _remember (property, value)
423 // #### @property {Object|string} Property that the value is in response to.
424 // #### @value {string} User input captured by `prompt`.
425 // Prepends the `property:value` pair into the private `history` Array
426 // for `prompt` so that it can be accessed later.
428 prompt._remember = function (property, value) {
435 // If the length of the `history` Array
436 // has exceeded the specified length to remember,
437 // `prompt.memory`, truncate it.
439 if (history.length > prompt.memory) {
440 history.splice(prompt.memory, history.length - prompt.memory);