higgsjs / Higgs

Higgs JavaScript Virtual Machine
875 stars 62 forks source link

Options Parser #118

Open mollymorphic opened 10 years ago

mollymorphic commented 10 years ago

Higgs allows users to pass arguments to scripts by including them after "--", like so:

./higgs script.js -- foo bar

However, this just provides an array of strings (in this case ["foo", "bar"]). Higgs should include a library that makes it easy for scripts to declare what arguments and options they take, which ones are required, etc. The library should then take an argument array like:

["plain arg", "second plain arg", "--opt", "value", "--secondopt", "value", "value"]

and produce an array of the arguments like:

["plain arg", "second plain arg"]

as well as an opts object like

{
    "opt" : "value",
    "secondopt" : ["value", "value"]
}

If all required args/options are not present an error should be thrown and/or a message displayed and the program exited.

A possible enhancement would be to take an optional description of what each option does, and use this to construct a "usage" message to display when no/incorrect args/opts are passed or the "-help" option is encountered.

Another possible enhancement would be to take an optional type for each arg/opt and then convert the argument/opt value to that type before passing it along to the library user, or an optional "converter function" to be applied to the opt/arg before passing it on to the library user.

Feedback for possible extra features, ideas for what the API should look like, and any other general thoughts are very welcome (^_^).

sbstp commented 10 years ago

Well the pocoo guys recently released a pretty awesome CLI API in Python. I think it's one of the cleanest APIs I've seen, but it makes heavy usage of annotations which are not available in JavaScript. One attempt at function annotation I've seen (in JavaScript) was in Angular.js. You can annotate using one of the following methods, array wrapper:

var annotated = [annotation1, annotation2, annotation3, funcToAnnotate]

and properties on function:

funcToAnnotate.annotation1 = "..."
funcToAnnotate.annotation2 = "..."
// etc
function funcToAnnotate() {
}

I think the second approach might be able to produce pretty clean results.

/**
 * ex: ls -l Folder/
 * options: { l: true }
 * text: "Folder/"
 */
function cmd(options, text) {

}
cmd.help = 'List stuff in a folder.'
cmd.options = {
 '-l': {
    help: 'Give more details.'
  }
}
// call the cli API with the args and the annotated function.
cli(args, cmd);

It could also be done with jQuery style chaining around a method, like so:

var cmd = command(function (options, text) {
})
  .help('List stuff in a folder.')
  .option('-l', 'Gives more details', { extra: information })
// or
var cmd = command()
  .help('List stuff in a folder.')
  .option('-l', 'Gives more details', { extra: information })
  .exec(function (options, text) {
  })
// or
var cmd = command()
  .help('List stuff in a folder.')
  .option('-l', 'Gives more details', { extra: information })
{options,text} = cmd.exec(args)
mollymorphic commented 10 years ago

Nice, some good ideas, thanks @SBSTP (^_^).

I think to start with we want something pretty simple; we can always build it up or have another lib later. So, I prefer the latter style over trying to decorate function objects for now. I don't think we need to handle subcommands and stuff either yet,we can just let the user do it.

So to expand on this with a more concrete example, maybe something like this:

function main(args, opts)
{
    // args is an object
    var op = args.op;
    var numbers = args.numbers;
    var initValue = 0;
    var fun;
    // user has to do some validation
    if (op === "plus")
        fun = function(a, b){ return a + b};
    else if (op === "minus")
        fun = function(a, b){ return a - b};
    else
        throw "Expected \'plus\' or \'minus\'";

    // opts is an object
    if ("initValue" in opts)
        initValue = Number(opts.initValue);

    // numbers is an array
    var result = numbers.reduce(fun, initValue);
    print(result);
}

optParser()
    .desc("A program to do something with numbers")
    .arg("op", {
        required : true,
        desc : "The operation to apply."
    })
    // note the use of ... to consume the rest of the args
    .arg("numbers...", {
        required : true,
        desc : "A list of numbers to operate on."
    })
    .opt("initValue", {
        desc : "an initial value."
    })
    .exec(global.arguments, main);

Later we could add more advanced features like being able to provide a validation function or list of good values for arguments, figuring out valid subcommand combos, checking/converting types for arguments.

Whoever wants to tackle this can make the final call on how fancy they want to make it and what names they want to give everything, but it should provide a simple api for simple cases and only get more complex if needed.

maximecb commented 10 years ago

Not sure that I love the chaining notation, but that's not really relevant. You can support that notation without forcing people to use it. Having desc strings seems like a good idea. I'd suggest naming the library "options", not just "opt".

mollymorphic commented 10 years ago

@maximecb yea, should be easy to have it support just passing in an object that describes all the options and everything

yawnt commented 9 years ago

i think higgs itself should not include an option parser, as this sounds too "high level" to me.. i'd rather see higgs with a js core with just barebone functionalities and an officially supported separate package providing higher-level abstractions (akin to ruby's or go's stdlib) :)

maximecb commented 9 years ago

The options parser is going to be in a library.

maximecb commented 9 years ago

I'd like to maybe add "shebang" mode to Higgs as well as command-line mode. I'm guessing this might affect the options parser a bit.

maximecb commented 9 years ago

I added the shebang mode support a little while back: https://github.com/higgsjs/Higgs/commit/165b3f87ba3245f267939293d3e85b99fbe19e47

maximecb commented 9 years ago

Since @SBSTP contributed an options parser, should we close this issue for now, reopen it later?

sbstp commented 9 years ago

There were still a few questions about the API (the error was fixed, but not the API). I could fix that before we close this issue.

maximecb commented 9 years ago

Which questions do you feel still need to be resolved?

sbstp commented 9 years ago

I created the singleton, but the way I created it conflicted with the way module are imported. Every time you'd require('lib/options.js') you'd get a warning because I modify the exports variable. So I'm not sure if I should rewrite the way the singleton is implemented or if we should remove the warning from the require function.

maximecb commented 9 years ago

Why modify the exports variable? The way modules work now, the library will only be loaded/initialized once.

sbstp commented 9 years ago

It was designed to be a constructor, so using it to instantiate an object and assigning it to exports way really easy. It's also a common pattern in the node world. I could easily drop the constructor, though.

maximecb commented 9 years ago

Do eet