Inquirer.js: Auto answering

Created on 21 Mar 2017  路  15Comments  路  Source: SBoudrias/Inquirer.js

Is it possible to automatically answer the question? E.g. when user calls cli app with arguments I want to parse them and depending on their values automatically answer a question when it's asksed.

So I would like to write something like this:

const askName = {
  type: 'input',
  name: 'name',
  message: `What's your name?`,
  // auto also can be string/boolean any other valid type
  auto: session => process.argv.name ? process.argv.name : undefined
};

So if auto's value is undefined, then the answer is prompted to user otherwise the answer automatically sets to returned from callback value.

It's nice to have this ability rather then don't ask the question at all because user can see the answer that was choosen by his actions (in my case by calling cli app with some arguments).

I tried to achieve such behavior from manually calling prompt.ui.rl.emit('line', 'hello') but I can't find sutable callback in inquirer's life cycle where I can do this.

Most helpful comment

I need this too.

I'm building a CLI tool that takes options via yargs. Inquirer is used to show a 'wizard' for any question that isn't already answered by a command line arg.

I can't use default, because that's basically just echoing back what the user has already entered, and is asking them to re-confirm (and if when is false, it gets ignored completely.)

I can't use when(), because that will skip the question entirely and avoid populating the answers hash for further questions, as well as miss out on the validation functions.

What would solve it is an answer field on the question object. Unlike 'default', putting a value there would avoid asking the question - BUT, it would automatically re-ask it if the value fails validation and show the usual error prompt.

So, it's basically a means to skip asking the question, but treat the value given as the answer.

Does that make sense?

All 15 comments

Absolutely. This is a great pattern in Yeoman generators like generator-seth, so I will use that as an example.

Steps:

  1. Get a command line argument.
  2. Dynamically show/hide a prompt based on whether the argument was provided.
  3. Merge the arguments and prompt answers with something like Object.assign().

The linked example uses some Yeoman APIs for dealing with arguments, but could be refactored pretty easily. I would use meow for this in a standalone CLI.

This approach is quite flexible. In your case, it sounds like instead of skipping the prompt when the user has provided an argument, you still want to show the prompt but make their argument the prompt's default value. It would be shown to the user and behave kind of like a confirmation screen. To do that, you would pass the argument to default instead of when.

@sholladay thank you for explanation, but unfortunatelly it's not what I'm looking for.

You was right when you mentioned that my case bit different. I want to skip the confirmation step for the questions with auto property, because when I call my app with some argument which sets default answers on questions I don't want confirm it for the each question. Imagine, that I have 100 of questions. I just want to fill in them once and other day just generate without prompting them once again, even with default values set as for previous time.

Also I don't want return false as when value, because I want to see which answers were set (automatically) for which question.

Have you got the idea?

Maybe would could print yourself the values in the when function:

when: function () {
    if (options.foo) {
        console.log('? My question is:' + chalk.cyan(options.foo));
        return false;
    }
    return true;
}

Have you got the idea?

I think so, we'll see. 馃槃

Let me revisit what you mean by "default" and "automatically answer the question." In Inquirer, the term "default" really means "initial value", similar to defaultValue on an input element in the DOM. If the prompt is not shown due to using when: false, then the default is not used.

I don't want confirm it for the each question

Okay, so it sounds like you don't want to prompt for a question at all if we receive a value for it via a command line argument. This is what when does in my example earlier.

If you do use when: false, then there won't be any answer in the object returned by Inquirer. This is why I use Object.assign() to merge the command line arguments and the Inquirer answers.

I just want to fill in them once and other day just generate without prompting them once again

Ah, okay. This sounds like what store: true does in Yeoman. This in not part of Inquirer, but could be added to your app by using conf to persist the answers. You would attempt to load existing configuration and use those values instead of prompting for them.

I don't want return false as when value, because I want to see which answers were set (automatically) for which question.

So you want to show the user what value is implicitly being used but not let them change it? This sounds like bad UX to me, honestly. And yeah, you can just use console.log() as @SBoudrias described. At that point it's not a prompt, it's just a log.

So, I found the code which generates question output line and as @SBoudrias said and modified code a little:

const PromptBase = require('inquirer/lib/prompts/base');

function autoAnswer(question) {
  // since console.log() returns undefined, we can use such one-liner
  question.when = () => console.log(new PromptBase(askName).getQuestion());

  return question;
}

const askName = {
  type: 'input',
  name: 'name',
  message: `What's your name?`,
  default: 'Anonymous'
};

// now the question is "auto answering" and output remains native
autoAnswer(askName);

What do you think? Is it safe to use internal function from here inquirer/lib/prompts/base ?

But probably it would be better to use other color to let the user know, that particular question was answered not usual way.

I need this too.

I'm building a CLI tool that takes options via yargs. Inquirer is used to show a 'wizard' for any question that isn't already answered by a command line arg.

I can't use default, because that's basically just echoing back what the user has already entered, and is asking them to re-confirm (and if when is false, it gets ignored completely.)

I can't use when(), because that will skip the question entirely and avoid populating the answers hash for further questions, as well as miss out on the validation functions.

What would solve it is an answer field on the question object. Unlike 'default', putting a value there would avoid asking the question - BUT, it would automatically re-ask it if the value fails validation and show the usual error prompt.

So, it's basically a means to skip asking the question, but treat the value given as the answer.

Does that make sense?

I've came with a workaround. Maybe it can help someone.
The idea is that I have two information source : a configuration file (could be whatever object your want) and inquirer.prompt.

  1. I load the configuration file and validate values using question.validate functions.
  2. Depending what's is defined in the configuration file, I set related question.when to false.
  3. I run every question.default on the configuration values and replaces default with output if any. That is because skipping questions doesn't populate the answers as explained above.
    :
// Here prompt is my own.
prompt.init() // init load configuration file
    .then(config => prompt.askCredentials() // asking information
        .then(answers => Object.assign(config, answers)) // merging
    )
/**
 * load configuration file if present
 * make related prompts to be skipped
 *
 * @resolves configuration object
 */
const init = () => fse.readJson('.db-config.json') // todo use constant
    .then((config) => {
        // disable prompts for provided items
        lodash.forEach(config, (value, key) => {
            if (inquiries[key]) {
                // I am so sorry, I have no other choice than to compare to true
                // validate returns true or the error message and a non empty string is considered truthy
                if (typeof (inquiries[key].validate) === 'function' && inquiries[key].validate(config[key]) !== true) {
                    // better store all errors then log them all
                    throw Error(`.db-config.json "${key}": "${value}" ${inquiries[key].validate(config[key])}`);
                }
                inquiries[key].when = false;
            } else {
                // todo more something of a warning
                throw Error(`${inquiries[key]} is defined in .db-config.json but is not a valid configuration item`); // todo use constant
            }
        });
        lodash.forEach(inquiries, (prompt) => {
            if (typeof (prompt.default) === 'function') {
                // if prompt can define a default value from configuration, uses it
                if (prompt.default(config)) prompt.default = prompt.default(config);
            }
        });
        return config;
    })
    .catch((error) => {
        console.error(error);
        return {};
    });

const askCredentials = () => inquirer.prompt([
    inquiries.dbms,
    inquiries.host,
    inquiries.port,
    inquiries.user,
    inquiries.password,
    inquiries.schema
]);

If you want more details, the source is here.

Update: This doesn't work completely because it will return an empty object if none of the questions are asked.


What I ended up doing was a variation on @SBoudrias's comment https://github.com/SBoudrias/Inquirer.js/issues/517#issuecomment-288964496 which sets the answer to an arbitrary value and tells the user that the answer was set to a specific value for a specific question.

const value = 'BAR';

const askName = {
  type: 'input',
  name: 'name',
  message: "What's your name?",
  when(answers) {
    if (skipQuestionAndSetValue) {
      console.log(`${chalk.blue('!')} Setting answer to "${value}" for "${this.message}"`);
      answers.name = value;
      return false;
    }
    return true;
  },
}

cc @likerRr @leebenson does this work for you?

Ah I've found out that an array of questions with whens that always return false just returns an empty object 馃槙

@SBoudrias would you be up for changing this behavior to not discard the mutated answers value? I've opened PR #645 with a failing test.

To demonstrate what I'm looking for, another (simpler) example of what I hope to get working:

// NOTE: Not working currently, don't use :)
var prompts = [
  {
    type: 'confirm',
    name: 'q1',
    message: 'message',
    when: function(answers) {
      answers.q1 = 'foo';
      return false;
    }
  },
  {
    type: 'confirm',
    name: 'q2',
    message: 'message',
    when: function(answers) {
      answers.q2 = 'bar';
      return false;
    }
  }
];

inquirer.prompt(prompts);
// answers: {}

To contrast, if there is at least one question that actually gets asked, the answers array is set correctly

// This works
var prompts = [
  {
    type: 'confirm',
    name: 'q1',
    message: 'message'
  },
  {
    type: 'confirm',
    name: 'q2',
    message: 'message',
    when: function(answers) {
      answers.q2 = 'bar';
      return false;
    }
  }
];

inquirer.prompt(prompts);
// answers: { q1: '<user input>', q2: 'bar' }

I'm not super warm to the idea of allowing users mutate the answers object. That's usually a pretty bad design decision.

Why not just provide defaults? Object.assign({ q2: 'bar' }, answers)?

Defaults would almost work, but they are not great for the following reasons:

  1. They happen outside of the sequence of the prompt. This is sub-ideal because:

    • They are outside of the validation logic
    • The assignment to the final answers object happens at a different time. Because of this, notifying users that a default value has been assigned needs to happen out of sequence (e.g. after the rest of the prompts).
  2. This moves some of the sources of the answers outside of the questions list, which makes it one more place to look. I like to think of prompt() kind of like Array.prototype.reduce(), taking in the questions list and providing answers at the end. I'd like to keep all of the logic within the questions if possible. I'm using (and can envision continuing to use) the when() callback kind of like a callback function to reduce(), except that every question has its own separate callback.


I'm not super warm to the idea of allowing users mutate the answers object. That's usually a pretty bad design decision.

Regarding this, I agree with you from an idealistic perspective (immutability improves comprehensibility and debuggability) and I would greatly prefer a way to achieve my goals without having to mutate answers.

So if there's a different way of achieving something similar (other than modifying the answers outside of the questions objects), I'm all ears. Here's another illustration of the complex sequence I'm trying to do:

Simplified example:

const options = await getArgsOrConfig();
const allCustomers = await getAllCustomers(token);

const prompts = [
  {
    type: 'list',
    name: 'customer',
    message: 'Customer:',
    choices: allCustomers,
    when(answers) {
      if (options.customer) {
        console.log(`${blue('!')} Using "customer" option from arguments or config`);
        answers.customer = options.customer;
      } else if (allCustomers.length === 1) {
        const firstCustomer = allCustomers[0];
        clog(`${blue('!')} Using only authorized customer "${firstCustomer.name}"`);
        answers.customer = firstCustomer.value;
      } else {
        // Prompt for input
        return true;
      }
    },
  },
  {
    type: 'input',
    name: 'git_url',
    message: 'Git URL:',
    validate: input =>
      /(github\.com|bitbucket\.org|gitlab\.com|\.git$)/.test(input) ||
      'Please enter a valid Git URL.',
    when(answers) {
      if (options.git_url) {
        console.log(`${blue('!')} Using "git_url" option from arguments or config`);
        answers.git_url = options.git_url;
      } else {
        // Prompt for input
        return true;
      }
    },
  }
];

inquirer.prompt(prompts);

Full code: https://github.com/amazeeio/lagoon/blob/a4a6fa7f101243a69676f060c6c5d8ddaa403ccd/cli/src/commands/projectCommands/create.js#L123-L222

@SBoudrias Would you be up for alternately extending when() to allow for a different, non-boolean return type? The return value could potentially be either:

Option 1: a value for the current answer

Option 2: a new object to replace answers with (kind of similar API to reduce() callbacks?)

The second option would be more flexible but also allow for more footguns (a question that changes an answer not meant for it).

I think one thing that would help clarify some of the feature requests going on here is if people would make a mockup for how you want it to look and work for the user, then post a recording of it using asciinema.

Anyone here should check out #709 and give their thoughts on it. Cheers 馃嵒

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Ezekiel-DA picture Ezekiel-DA  路  7Comments

fuechter picture fuechter  路  3Comments

mechanical-turk picture mechanical-turk  路  3Comments

aguerrieri picture aguerrieri  路  6Comments

ro31337 picture ro31337  路  5Comments