简体   繁体   中英

Create Series of Prompts from Array of Questions in Node.js

I am creating a command line tool in node. After the user enters a specific command I parse through the options they have entered.

> mycommand init --name name --email user@domain.com

I then validate the values of the options. After validation I generate an array of questions that were not answered from the options the user entered and from the entered options that did not pass validation. Once the array has been generated, I loop through it and give a prompt for each question:

    for(var i = 0; i < questions.length; i++) {
        var prop  = questions[i],
            title = messages.prompts.init[prop].question,
            def   = messages.prompts.init[prop].def,
            input = read.createInterface(process.stdin, process.stdout);

        if(messages.prompts.init[prop].hasOwnProperty('format')){
            title = title + ' <' + messages.prompts.init[prop].format + '> '; 
        }

        input.question(title + ' (' + def + ')', function (a) {
            //dosomething(a);
            process.exit();
        });
    }

The problem is, the code loops through the entire array without waiting for the user input and ends with a final prompt displaying a message from the last item in the array. When I try to type a response, each keystroke is multiplied by the number of items in the array. For instance, attempting to type "myname" results in:

> Enter Your Name (none): mmmmyyyynnnnaaaammmmeeee

I have tried multiple node modules including read and prompt and am experienceing the same problem So I must not fully understand how command line tools read user input.

Any insight on how to solve this problem would be appreciated.

Thanks.

So, I think the problem is that it's creating callbacks and the loop continues on so you're getting weird results. Using a variant of the Tiny CLI example, you can create a prompt with a question from the array and watch the line event to get the input, then repeat. Here's a quick example.

var read = require('readline'),
    input = read.createInterface(process.stdin, process.stdout),
    questions = ['test1: ', 'test2: '],
    counter = 0;

input.setPrompt(questions[0]);
input.prompt();

input.on('line', function (a) {
    console.log('answer: ', a);
    counter++;
    if (counter < questions.length) {
        input.setPrompt(questions[counter]);
        input.prompt();
    } else {
        process.exit(0);
    }
});

I wrote some utility methods to help with this. Couple of added gems in here like it automatically writes the last values entered to a hidden file so a user can press enter

```

var async = require('async');
var fileUtils = require('../util/file-util');

var LAST_OPTIONS_FILE='./.last-options-file';


/**
 * The prompt object type that is used by askSeries
 * @typedef {Object} Prompt
 * @property {string}  key - is used as the variable name
 * @property {string}  [default] - is a default value so you can just press enter
 * @property {string}  [format] - validates input against a regex
 * @property {string}  [question] - use to make a more human readable prompt
 * @property {boolean} [confirm] - will force user to confirm the value if it was found on the command line
 * @property {boolean} [forceEnter] - if true, user must enter this value (last value is ignored)
 */


/**
 * Asks user for input for each of the prompts
 *
 * <PRE>
 * Example:
 * askSeries([{key:"customerId"}, {key:"subscriptionId", "default":"blah"}], function(inputs) {
 *    console.log(inputs);
 * });
 * OUTPUT:
 *   customerId: 5
 *   subscriptionId [blah]: 9
 *   [ customerId: '5', subscriptionId: '9' ]
 *
 * askSeries([{key:"customerId", question:"Enter Customer Id", format: /\d+/}, {key:"subscriptionId", "default":"blah"}], function(inputs) {
 *    console.log(inputs);
 * });
 * OUTPUT:
 *   Enter Customer Id: abc
 *   It should match: /\d+/
 *   Enter Customer Id: 123
 *   subscriptionId [blah]:
 *   [ customerId: '123', subscriptionId: 'blah' ]
 * </PRE>
 *
 * @param {Prompt[]} prompts - an array of Prompts which dictate what user should be asked
 * @param {object} [argv] - if any of prompts .keys match argv it sets the default appropriately,
 *                          argv will get all prompt .key values set on it as well
 * @param {function} callback - signature is function(err,params)
 */
exports.askSeries = function(prompts,argv,callback) {
    var input = {};
    var lastVal = {};
    if(typeof argv === 'function') { callback = argv; argv=null; }

    lastVal = fileUtils.readJSONFileSync(LAST_OPTIONS_FILE);
    if( !lastVal ) { lastVal = {}; }

    console.log("LASTVAL", lastVal);


    async.eachSeries(prompts, function(prompt, next) {
        if( !prompt.key ) { callback(new Error("prompt doesn't have required 'key' param")); }
        if( !prompt.confirm && argv && argv[prompt.key] ) {
            input[prompt.key] = argv[prompt.key];
            return next();
        }
        else {

            var defaultVal = prompt.default;
            if( argv && argv[prompt.key] ) { defaultVal = argv[prompt.key]; }
            else if( !prompt.forceEnter && lastVal[prompt.key] ) { defaultVal = lastVal[prompt.key]; }

            exports.ask( prompt.question || prompt.key, prompt.format || /.+|/, defaultVal, function(value) {
                if( !value ) {
                    if( prompt.default ) {
                        value = prompt.default;
                    }
                    if( argv && argv[prompt.key] ) {
                        value = argv[prompt.key];
                    }
                }
                input[prompt.key] = value;
                if( argv ) { argv[key] = value;}
                next();
            });
        }
    }, function(err) {
        try {
            var fileData = JSON.stringify(input);
            fileUtils.writeToFile(LAST_OPTIONS_FILE, fileData );
        }catch(err) { console.log("Unable to save entered values"); }
        callback(err,input);
    });
};

/**
 * Prompts user for input
 *
 * @param {string} question prompt that is displayed to the user
 * @param {string} [format] regex used to validate
 * @param {string} [defaultVal] uses this value if enter is pressed
 * @param callback is invoked with value input as callback(value);
 */
exports.ask = function ask(question, format, defaultVal, callback) {
    var stdin = process.stdin, stdout = process.stdout;

    if( typeof(format) === 'function' ) {
        callback = format;
        format = null;
    }
    if( typeof(defaultVal) === 'function') {
        callback = defaultVal;
        defaultVal = null;
    }

    stdin.resume();

    var prompt = question;
    if( defaultVal ) {
        prompt += " [" + defaultVal + "]";
    }
    prompt += ": ";

    stdout.write(prompt);

    stdin.once('data', function(data) {
        data = data.toString().trim();

        if( !data && defaultVal ) {
            data = defaultVal;
        }

        if (!format || format.test(data)) {
            callback(data);
        } else {
            stdout.write("It should match: "+ format +"\n");
            ask(question, format, callback);
        }
    });
};

/**
 * Prints the usage from the <link Prompt> array.
 * @param {Prompt[]} prompts - the array of prompts that will be asked if not entered on cmd line.
 * @param {string} binaryName - if specified it puts it in the usage as <binaryName> [options]
 * @param {boolean} [dontPrint] - if true, the usage will not be written to console
 */
exports.getUsage = function(prompts, binaryName, dontPrint) {
    if(!binaryName) { binaryName = "program_name";}
    var s = "\nUsage: \n./" + binaryName + " [options]\n\n options:\n";

    console.log(prompts);
    prompts.forEach( function(p) {
        s += "   --" + p.key;
        if(p.question) { s += ": " + p.question;}
        if(p.format) { s += ", format: " + p.format; }
        s += "\n";
    });

    if( !dontPrint ) {
        console.log( s );
    }

    return s;
};fs

```

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM