Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive mode #286

Closed
gimenete opened this issue Oct 28, 2015 · 31 comments
Closed

Interactive mode #286

gimenete opened this issue Oct 28, 2015 · 31 comments

Comments

@gimenete
Copy link

It would be great to have support for an interactive mode. I've written some code here with an example

gimenete@0d059ee

If the idea is accepted I could finish it, make tests and create a PR.

@nexdrew
Copy link
Member

nexdrew commented Oct 28, 2015

Interesting idea. Thanks, @gimenete!

One note on your implementation: I wouldn't call parseArgs() from the new .interactive() API method. Instead, I would move most of that logic to the parseArgs() function itself.

Most API methods (i.e. ones that define options, commands, or usage text) are just synchronous, chainable configuration steps. Arg parsing should be reserved for the .argv terminal getter method or the .parse() method. Defining the interactive option via .interactive() should be a chainable configuration step (should return self) that is separate from parsing args, such that the following should be identical:

yargs.alias('i', 'interactive').interactive('i', fn)

yargs.interactive('i', fn).alias('i', 'interactive')

I really appreciate the sample code, but if we do this, we'll want to make the "interactive" logic as robust as possible, which could get complicated.

I think adding interactivity is a really cool idea, but it may be better accomplished by frameworks like Vorpal. I don't know. Maybe a good candidate for yargs 4?

@bcoe What do you think?

@gimenete
Copy link
Author

Thanks @nexdrew for your comments

The main reason to make interactive() receive a callback and not make it synchronous is because if the interactive option is present it will use the readline module which has an async API.

So my idea is: in case you want to support the interactive mode call interactive(opt, callback) and you will receive the argv variable in the callback no matter if the user chose the interactive mode or not.

Another idea would be to create a different npm module (yargs-interactive) and wrap all the yargs API adding the interactive option. However it's difficult to make it this way since when a demanded option is not present yargs will print the usage and will exit the process.

@nexdrew
Copy link
Member

nexdrew commented Oct 28, 2015

@gimenete To get around the "validation fails -> exit process" problem, try using .fail(fn) to specify your own failure handler. Alternatively, you could also try using .showHelpOnFail(false) and .exitProcess(false) together, in which case yargs will throw an Error on validation failure instead of showing help content and exiting the process.

@bcoe
Copy link
Member

bcoe commented Oct 28, 2015

I like this idea, but one thought I have is perhaps we could pull the prompt handler into its own module -- it seems like enough logic would be added that it would start to muddle index.js a bit. One other thought, do we pull in inquirer rather than re-inventing the wheel?

@bcoe
Copy link
Member

bcoe commented Oct 28, 2015

This could also be an interesting option, to avoid the async behavior:

https://github.com/anseki/readline-sync

@gimenete
Copy link
Author

oh, inquirer looks great if we are ok making interactive() async. I don't mind having an async function. It's only one and it's optional. I think implementing it asynchronously has more pros than cons.

I can try to find some free time to move my changes to another file (interactive.js) and start using inquirer on them.

@gimenete
Copy link
Author

ok, so I moved the interactive code to a separate file and I started using inquirer:

master...gimenete:feature/interactive-mode

I've implemented support for regular inputs, choices (adding the ability to select multiple elements) and booleans.

@bcoe
Copy link
Member

bcoe commented Oct 29, 2015

@gimenete I'm on my honeymoon, so my turn around is a bit slow on pull requests -- but I like where this is going, curious to get @nexdrew's thoughts, especially on async vs. sync mode.

@nexdrew
Copy link
Member

nexdrew commented Oct 29, 2015

My 2¢: I like the idea of interactivity, but I don't like changing how yargs works (sync -> async) just to support it. Personally, I would prefer a synchronous configuration step and a separate (possibly asynchronous) terminal parsing/interactive step, if possible.

I haven't had a chance to play with your branch yet. Hopefully I can make some time soon-ish.

In the meantime, what do you think about making this a separate module, possibly wrapping yargs?

@nexdrew
Copy link
Member

nexdrew commented Oct 29, 2015

Ok, so I just took a few minutes to play with your example. First impression is pretty cool! I still think we should explore other options as well and possibly clean up the API some. I'll try to do some exploration over the next few days.

@gimenete
Copy link
Author

If we don't really want an async version then we will need to not use inquirer and create something from scratch with readline-sync.

Nevertheless note that this interactive mode is totally optional. If somebody doesn't need/want an interactive mode he will keep using the yargs.argv version.

Doing a separate module is another option, but I would prefer having this functionality on yargs so more people will use it, and I think it's simpler to implement inside yargs because all the information provided by the user is easily accesible. Thinking on how to implement it in a separate module I would do something like:

var yargs = require('yargs')()

yargs.interactive = function (key, fn) {
  yargs.boolean(key)
  yargs.fail(function(fn) {
    // I receive a message but I should parse it?
  })
  var argv = yargs.argv
  // nothing is set in argv apparently but we should do:
  if (!argv[key]) return fn(argv)
  // how to check which arguments failed?
  // interactive mode here
  // fn(argv)
}

module.exports = yargs

Should be possible but the main issue is that the argv variable is not populated if any of the checks fails, I think. Then we will need a way to introspect the missing options and their types.

PS: @bcoe congrats for the wedding and have a great honeymoon!

@nexdrew
Copy link
Member

nexdrew commented Oct 29, 2015

Just for the record, I'm not against an async version of parsing for interactive mode, but I think it should be separate from configuring an option to represent that mode. That way, consumers can use the typical .alias() method for the interactive flag, like they can do for help and version options, before any parsing is done.

Perhaps we start by introducing an async version of parsing first (accepting a callback or returning a promise if no callback is given) as one PR, so we make sure we get the API right without interactive mode, and then we follow that up by adding interactive mode (which will imply use of async parsing).

What do you think?

@gimenete
Copy link
Author

Do you mean doing something like this?

yargs
  .usage('This is my awesome program\n\nUsage: $0 [options]')
  .string('x', 'y')
  .alias('i', 'interactive')
  .interactive('i') // implement the interactive option after the async API
  .parse(function(argv) {
    // first implement this new async API
  })

We can call it parse or whatever you prefer. But not argv since it will be backwards incompatible.

@nexdrew
Copy link
Member

nexdrew commented Oct 29, 2015

Well, there's already a .parse(args) method, so we can't call it that 😃 (unless we want to refactor it to support async mode, which is probably not a good idea). But yes, that's exactly what I mean.

@gimenete
Copy link
Author

haha, ok. Another suggestion for the name?

I'll change my branch to match this API design.

@nexdrew
Copy link
Member

nexdrew commented Oct 29, 2015

No great ideas. I hate to say it (because it seems like an anti-pattern), but .parseAsync() makes the most sense to me.

@gimenete
Copy link
Author

gimenete commented Nov 9, 2015

I've just added the parseAsync method and .interactive() no longer receives a callback. I've added some testing as well. Check it out and let me know if we are in the same page and please check if all my assumptions are ok. For example if I'm calculating correctly the parameters that are not already set via command line arguments.

@bcoe
Copy link
Member

bcoe commented Nov 14, 2015

@gimenete haven't forgotten about this slick feature; I'm just getting back in the swing of things post vacation, and am working on catching up on several other OSS projects I maintain.

I'm happy with whatever API you and @nexdrew come to for this, Andrew has smart thoughtful opinions on this sort of thing 👍

@bcoe
Copy link
Member

bcoe commented Jan 9, 2016

@gimenete I still love this idea, one thought I had as we move towards yargs@4.x is that an async mode would be nice for a few reasons:

  • having interactive arguments.
  • async commands.
  • async configuration loading.

Long story short, @gimenete I would love to get your feedback as we start chunking up work for the next major release of yargs.

@bcoe bcoe added the 4.x label Jan 9, 2016
@gimenete
Copy link
Author

@bcoe give me a couple of days and I'll review again my branch and with the other things you are mentioning :)

@RWOverdijk
Copy link

I'm using inquirer for that currently. Config of options, define the command and define the inquiries. Does that help?

@gimenete
Copy link
Author

@bcoe @nexdrew I've pushed new code. Sorry for not responding earlier. See all the changes here master...gimenete:feature/interactive-mode

There is one decision that I made that I don't know if you agree with: you can specify arguments both in the command line (as always) and in the interactive mode if it is present and here master...gimenete:feature/interactive-mode#diff-b72d3f57671eb1c20676ce167bdeac75R6 is where I calculate the missing options. I have found only a problem with this: I always get true/false for boolean values. I never get null or undefined so it never asks you for boolean options

This should be all regarding my initial proposal. But you mention that it would be nice to have also async commands and async configuration. I don't know what you mean with async configuration but for async commands I guess you mean something like this:

.options({
    input: {
      alias: 'in',
      description: '<filename> Input file name',
      requiresArg: true,
      required: true,
      validate: function(value, callback) { fs.exists(value, callback) }
    }
})

@bcoe
Copy link
Member

bcoe commented Jan 29, 2016

@gimenete I really like this work 👍 one thought comes to mind. Now that we've split yargs into an organization, perhaps we could make interactive yargs a new project under the top level organization, rather than adding this functionality directly into yargs? Here's why I think this could be cool:

  • I'd be less concerned about pulling in other helper modules for reading arguments ... I've been contributing a tiny bit to @SBoudrias' inquirer. It's really slick, and is already a close API to yargs' -- perhaps as we move yargs forward we could make the two APIs line up even more?
  • If you pull this into its own module, we wouldn't have to worry about namespace collision on the method .parse() ... we could just override it. We could also potentially modify the default behavior of the property method argv.

Perhaps we could call the project something like yargs-prompt? Thoughts, feedback, objections:

CC: @SBoudrias, @gimenete, @nexdrew

@SBoudrias
Copy link

FWIW, that sounds a lot like what we're planning on doing in the next iteration of the Yeoman generator api yeoman/generator#894

@pleerock
Copy link

any news here?

@gimenete
Copy link
Author

gimenete commented Sep 16, 2016

Sorry, I wanted this for a project that needed many command line options, so it was interesting to be able to use an interactive mode If you didn't (want to) remember all, or you could just pass all if knew all or you were scripting the cli app. But I finally ended up creating a GUI and since then I've just used inquirer or yargs depending on the needs.

I still think that it would be great to have the ability to select an interactive mode though.

@bcoe
Copy link
Member

bcoe commented Sep 19, 2016

@pleerock @gimenete what I'd like, would be a recipe for using yargs in a non CLI environment; which would better support chat-bots, interactive terminals, etc.

we have one breaking change already queued up for 6.x, maybe we could make interactive yargs a goal as part of this release.

@timwis
Copy link

timwis commented Oct 9, 2016

I was looking for something like this today. I built an app that logs in to your online banking to fetch your balance, and I didn't want to pass my bank password to my terminal (because it gets logged in terminal history), and I didn't want to store it on my filesystem. So I wanted the password arg to be prompted. I ended up using prompt, which works fine, but I have some redundancy.

It would be great to be able to specify .option('password', { alias: 'p', prompt: true }) and if password isn't passed as a CLI arg, yargs would just prompt for it.

@nanovazquez
Copy link

nanovazquez commented Feb 8, 2018

A couple of months ago, I was in need of something like this, and I created: yargs-interactive. This tool helps you to implement, using the same code, a CLI tool with interactive support, but also non-interactive and/or mixed mode. It helps my team to develop CLI libraries used by both devs and CI tools.

I'd like to revive this issue because I'm all about enhancing yargs to do the same thing, or at least at some level. In a nutshell, we need a tool that does what yargs currently does (parse arguments) and:

  • Supports a way to prompt for the arguments (full-interactive)
  • Supports a way to prompt for specific arguments (mixed-mode)

Before starting making changes to yargs, there are some things I'd like to ask the team about this new interactive mode:

  1. Would you like to have this in yargs or is it something you consider as out of the scope?
  2. If so, would you prefer to have the code inside yargs or are you willing to use a dependency for this? (like inquirer).
  3. What prompt types do you want to support? Tools like inquirer support a wide variety of prompts, like checkboxes, lists, confirm, etc. Given that this is not the primary usage of yargs, we can just support a single input type, and that's it.

Thanks!

@tagazok
Copy link

tagazok commented Jul 4, 2018

Single Input type is perfect. In my case I would mostly use it to ask for confirmation (Y/n questions).

@bcoe
Copy link
Member

bcoe commented Nov 9, 2019

This feature is beyond the scope of yargs, I think it would be better to direct folks to use tools like inquirerthat is designed for user input.

What I'd definitely be open to would be us adding a section to our docs with an example of how you might use inquirer and yargs in conjunction.

@bcoe bcoe closed this as completed Nov 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants