├── .gitignore ├── .travis.yml ├── Makefile ├── examples ├── password.js ├── name.js └── multi.js ├── test ├── common.js ├── windows.js ├── password.js ├── multi.js └── basic.js ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - '0.10' 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @./node_modules/.bin/mocha \ 3 | --reporter spec \ 4 | --require test/common.js \ 5 | --bail 6 | 7 | .PHONY: test -------------------------------------------------------------------------------- /examples/password.js: -------------------------------------------------------------------------------- 1 | var prompt = require('../'); 2 | 3 | prompt.password('tell me a secret: ', function (val) { 4 | console.log(val); 5 | }); 6 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | assert = require('assert'); 2 | util = require('util'); 3 | resolve = require('path').resolve; 4 | fs = require('fs'); 5 | prompt = require('../'); 6 | suppose = require('suppose'); 7 | -------------------------------------------------------------------------------- /examples/name.js: -------------------------------------------------------------------------------- 1 | var prompt = require('../'); 2 | 3 | prompt('enter your first name: ', function (val) { 4 | var first = val; 5 | prompt('and your last name: ', function (val) { 6 | console.log('hi, ' + first + ' ' + val + '!'); 7 | }, function (err) { 8 | console.error('unable to read last name: ' + err); 9 | }); 10 | }, function (err) { 11 | console.error('unable to read first name: ' + err); 12 | }); 13 | -------------------------------------------------------------------------------- /test/windows.js: -------------------------------------------------------------------------------- 1 | describe('windows CRLF', function () { 2 | it('works', function (done) { 3 | suppose('node', [resolve(__dirname, '../examples/name.js')]) 4 | .when('enter your first name: ').respond('carlos\r\n') 5 | .when('and your last name: ').respond('rodriguez\r\n') 6 | .when('hi, carlos rodriguez!\n').respond("'sup!") 7 | .on('error', assert.ifError) 8 | .end(function (code) { 9 | assert(!code); 10 | done(); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/password.js: -------------------------------------------------------------------------------- 1 | describe('password', function () { 2 | it('works', function (done) { 3 | var options = { 4 | //optional writeable output stream 5 | //debug: fs.createWriteStream('/tmp/debug.txt') 6 | }; 7 | suppose('node', [resolve(__dirname, '../examples/password.js')], options) 8 | .when('tell me a secret: ').respond('earthworn\bm jim\n') 9 | .when('earthworm jim\n').respond('hey') 10 | .on('error', assert.ifError) 11 | .end(function (code) { 12 | assert(!code); 13 | done(); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/multi.js: -------------------------------------------------------------------------------- 1 | var prompt = require('../'); 2 | 3 | prompt.multi([ 4 | { 5 | key: 'username', 6 | default: 'john_doe' 7 | }, 8 | { 9 | label: 'password (must be at least 5 characters)', 10 | key: 'password', 11 | type: 'password', 12 | validate: function (val) { 13 | if (val.length < 5) throw new Error('password must be at least 5 characters long'); 14 | } 15 | }, 16 | { 17 | label: 'number of pets', 18 | key: 'pets', 19 | type: 'number', 20 | default: function () { 21 | return this.password.length; 22 | } 23 | }, 24 | { 25 | label: 'is this ok?', 26 | type: 'boolean' 27 | } 28 | ], console.log); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Carlos Rodriguez (http://s8f.org/)", 3 | "name": "cli-prompt", 4 | "description": "A tiny CLI prompter", 5 | "version": "0.6.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/carlos8f/node-cli-prompt.git" 9 | }, 10 | "homepage": "https://github.com/carlos8f/node-cli-prompt", 11 | "keywords": [ 12 | "prompt", 13 | "cli", 14 | "readline", 15 | "input", 16 | "terminal", 17 | "console", 18 | "wizard" 19 | ], 20 | "dependencies": { 21 | "keypress": "~0.2.1" 22 | }, 23 | "devDependencies": { 24 | "mocha": "*", 25 | "suppose": "git+https://github.com/carlos8f/node-suppose.git" 26 | }, 27 | "scripts": { 28 | "test": "make test" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/carlos8f/node-cli-prompt/issues" 32 | }, 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /test/multi.js: -------------------------------------------------------------------------------- 1 | describe('multi', function () { 2 | it('works', function (done) { 3 | var options = { 4 | //optional writeable output stream 5 | //debug: fs.createWriteStream('/tmp/debug.txt') 6 | }; 7 | suppose('node', [resolve(__dirname, '../examples/multi.js')], options) 8 | .when('username: (john_doe) ').respond('\n') 9 | .when('password (must be at least 5 characters): ').respond('asdfff\b\b\n') 10 | .when('password must be at least 5 characters long\npassword (must be at least 5 characters): ').respond('asdfff\n') 11 | .when('number of pets: (6) ').respond('eight\n') 12 | .when('number of pets: (6) ').respond('8\n') 13 | .when('is this ok?: (y/n) ').respond('okay\n') 14 | .when('is this ok?: (y/n) ').respond('yes\n') 15 | .when("{ username: 'john_doe', password: 'asdfff', pets: 8 }\n").respond('great') 16 | .on('error', assert.ifError) 17 | .end(function (code) { 18 | assert(!code); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | describe('basic test', function () { 2 | it('works', function (done) { 3 | suppose('node', [resolve(__dirname, '../examples/name.js')]) 4 | .when('enter your first name: ').respond('carliz\b\bos 8\n') 5 | .when('and your last name: ').respond('rodriguez\n') 6 | .when('hi, carlos 8 rodriguez!\n').respond("'sup!") 7 | .on('error', assert.ifError) 8 | .end(function (code) { 9 | assert(!code); 10 | done(); 11 | }); 12 | }); 13 | 14 | it('calls onError for premature end', function (done) { 15 | var gotError = false; 16 | var chunks = [] 17 | suppose('node', [resolve(__dirname, '../examples/name.js')]) 18 | // Output to stderr is wrapped into an Error 19 | .end(function (code) { 20 | assert(!code); 21 | var stderr = Buffer.concat(chunks).toString('utf8') 22 | assert(stderr.match(/unable to read first name: Error: stdin has ended\n/)) 23 | done(); 24 | }) 25 | .stderr.on('data', function (chunk) { 26 | chunks.push(chunk) 27 | }) 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cli-prompt 2 | ========== 3 | 4 | A tiny CLI prompter 5 | 6 | [![build status](https://secure.travis-ci.org/carlos8f/node-cli-prompt.png)](http://travis-ci.org/carlos8f/node-cli-prompt) 7 | 8 | Install 9 | ------- 10 | 11 | ```javascript 12 | $ npm install --save cli-prompt 13 | ``` 14 | 15 | Usage 16 | ----- 17 | 18 | ### `prompt(message, onValue, [onError])` 19 | 20 | - `message`: text prompt for the user 21 | - `onValue`: function to be called (after user hits enter/return) with the entered text 22 | - `onError`: optional function to receive an error, if STDIN has already ended, for example. 23 | 24 | Note: cli-prompt will not work if STDIN has ended. If provided, onError will be called in this case. 25 | 26 | Example 27 | ------- 28 | 29 | ```js 30 | var prompt = require('cli-prompt'); 31 | 32 | prompt('enter your first name: ', function (val) { 33 | var first = val; 34 | prompt('and your last name: ', function (val) { 35 | console.log('hi, ' + first + ' ' + val + '!'); 36 | }, function (err) { 37 | console.error('unable to read last name: ' + err); 38 | }); 39 | }, function (err) { 40 | console.error('unable to read first name: ' + err); 41 | }); 42 | ``` 43 | 44 | ### Password/hidden input 45 | 46 | ```js 47 | var prompt = require('cli-prompt'); 48 | 49 | prompt.password('tell me a secret: ', console.log, console.error); 50 | ``` 51 | 52 | ### Multiple questions 53 | 54 | ```js 55 | var prompt = require('cli-prompt'); 56 | 57 | prompt.multi([ 58 | { 59 | key: 'username', 60 | default: 'john_doe' 61 | }, 62 | { 63 | label: 'password (must be at least 5 characters)', 64 | key: 'password', 65 | type: 'password', 66 | validate: function (val) { 67 | if (val.length < 5) throw new Error('password must be at least 5 characters long'); 68 | } 69 | }, 70 | { 71 | label: 'number of pets', 72 | key: 'pets', 73 | type: 'number', 74 | default: function () { 75 | return this.password.length; 76 | } 77 | }, 78 | { 79 | label: 'is this ok?', 80 | type: 'boolean' 81 | } 82 | ], console.log, console.error); 83 | ``` 84 | 85 | --- 86 | 87 | ### Thanks 88 | 89 | - Thanks to @kevinoid for implementing STDIN end/error detection. 90 | - Thanks to @mm-gmbd and @apieceofbart for contributing Windows support patches. 91 | 92 | - - - 93 | 94 | ### Developed by [Terra Eclipse](http://www.terraeclipse.com) 95 | Terra Eclipse, Inc. is a nationally recognized political technology and 96 | strategy firm located in Aptos, CA and Washington, D.C. 97 | 98 | - - - 99 | 100 | ### License: MIT 101 | 102 | - Copyright (C) 2012 Carlos Rodriguez (http://s8f.org/) 103 | - Copyright (C) 2012 Terra Eclipse, Inc. (http://www.terraeclipse.com/) 104 | 105 | Permission is hereby granted, free of charge, to any person obtaining a copy 106 | of this software and associated documentation files (the "Software"), to deal 107 | in the Software without restriction, including without limitation the rights 108 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 109 | copies of the Software, and to permit persons to whom the Software is furnished 110 | to do so, subject to the following conditions: 111 | 112 | The above copyright notice and this permission notice shall be included in 113 | all copies or substantial portions of the Software. 114 | 115 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 116 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 117 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 118 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 119 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 120 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 121 | SOFTWARE. 122 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var tty = require('tty') 2 | , keypress = require('keypress') 3 | , stdinEnded = false 4 | 5 | process.stdin.once('end', function() { stdinEnded = true; }); 6 | 7 | function prompt (message, hideInput, onValue, onError) { 8 | if (typeof hideInput === 'function') { 9 | onError = onValue; 10 | onValue = hideInput; 11 | hideInput = false; 12 | } 13 | 14 | keypress(process.stdin); 15 | 16 | function setRawMode(mode) { 17 | if (process.stdin.setRawMode) { 18 | process.stdin.setRawMode(mode); 19 | } 20 | else if (process.stderr.isTTY) { 21 | tty.setRawMode(mode); 22 | } 23 | } 24 | if (hideInput) setRawMode(true); 25 | 26 | if (message !== null) process.stderr.write(message); 27 | 28 | var gotInput = false; 29 | 30 | function done (err) { 31 | process.stdin.removeListener('keypress', listen); 32 | process.stdin.removeListener('error', done); 33 | process.stdin.removeListener('end', done); 34 | process.stdin.pause(); 35 | if (hideInput) { 36 | setRawMode(false); 37 | console.error(); 38 | } 39 | if (err && onError) { 40 | onError(err); 41 | } else if (!gotInput && onError) { 42 | onError(new Error('stdin has ended')); 43 | } else { 44 | onValue(line, function () {}); // for backwards-compatibility, fake end() callback 45 | } 46 | } 47 | 48 | if (stdinEnded) { 49 | process.nextTick(done); 50 | return; 51 | } 52 | 53 | function listen (c, key) { 54 | gotInput = true; 55 | if (key) { 56 | if (key.ctrl && key.name === 'c') { 57 | process.exit(); 58 | } 59 | else if (key.name === 'return'){ 60 | if (hideInput == true){ 61 | done(); 62 | } 63 | return; 64 | } else if (key.name === 'enter' || key.sequence === '\r\n') { 65 | line = line.trim(); 66 | done(); 67 | return; 68 | } 69 | if (key.name === 'backspace') line = line.slice(0, -1); 70 | } 71 | if (!key || key.name !== 'backspace') line += c; 72 | } 73 | 74 | var line = ''; 75 | process.stdin.on('keypress', listen) 76 | 77 | if (onError) { 78 | process.stdin 79 | .once('error', done) 80 | .once('end', done); 81 | } 82 | 83 | process.stdin.resume(); 84 | } 85 | module.exports = prompt; 86 | 87 | function password (message, onValue, onError) { 88 | prompt(message, true, function (val) { 89 | // password is required 90 | if (!val) password(message, onValue, onError); 91 | else onValue(val, function () {}); // for backwards-compatibility, fake end() callback 92 | }, onError); 93 | } 94 | module.exports.password = password; 95 | 96 | function multi (questions, onValue, onError) { 97 | var idx = 0, ret = {}; 98 | (function ask () { 99 | var q = questions[idx++]; 100 | if (typeof q === 'string') { 101 | q = {key: q}; 102 | } 103 | function record (val) { 104 | function retry () { 105 | idx--; 106 | ask(); 107 | } 108 | if (q.required && typeof q.default === 'undefined' && !val) return retry(); 109 | if (!val && typeof q.default !== 'undefined') { 110 | val = def; 111 | } 112 | if (q.validate) { 113 | try { 114 | var ok = q.validate.call(ret, val); 115 | } 116 | catch (e) { 117 | console.error(e.message); 118 | return retry(); 119 | } 120 | if (ok === false) return retry(); 121 | } 122 | if (q.type === 'number') { 123 | val = Number(val); 124 | if (Number.isNaN(val)) return retry(); 125 | } 126 | else if (q.type === 'boolean') val = val.match(/^(yes|ok|true|y)$/i) ? true : false; 127 | if (typeof q.key !== 'undefined') ret[q.key] = val; 128 | if (questions[idx]) ask(); 129 | else onValue(ret); 130 | } 131 | var label = (q.label || q.key) + ': '; 132 | if (q.default) { 133 | var def = (typeof q.default === 'function') ? val = q.default.call(ret) : q.default; 134 | label += '(' + def + ') '; 135 | } 136 | else if (q.type === 'boolean') { 137 | label += '(y/n) '; 138 | q.validate = function (val){ 139 | if (!val.match(/^(yes|ok|true|y|no|false|n)$/i)) return false; 140 | }; 141 | } 142 | prompt(label, q.type === 'password', record, onError); 143 | })(); 144 | } 145 | module.exports.multi = multi; 146 | --------------------------------------------------------------------------------