Deno: Unable to create --help option in the user's script.

Created on 20 Jan 2019  路  13Comments  路  Source: denoland/deno

I started creating a CLI tool in deno like the below:

#!/usr/bin/env deno --allow-run
import { parse } from "https://deno.land/x/[email protected]/mod.ts"
import { exit, args } from "deno"

const main = async (opts) => {
  if (opts.help) {
    console.log(`Usage: my-command`)
    exit(0)
  }

  console.log('something')
}

main(parse(args.slice(1)))

When I pass --help option to this script, deno's help message was printed like the below:

$ deno example.ts 
something
$ deno example.ts --help
Usage: deno script.ts 

Options:
        --allow-write   Allow file system write access.
        --allow-net     Allow network access.
        --allow-env     Allow environment access.
        --allow-run     Allow running subprocesses.
    -A, --allow-all     Allow all permissions
...

This means my tool is unable to have --help option and this is inconvenient.

Most helpful comment

In Node, any flags/arguments that come after the user's script _belong_ to the user's script.

# foo's help is shown, assuming foo.js handles `--help` in some way
$ node foo.js --help
# Node's help is shown and foo.js is ignored
$ node --help foo.js

That works pretty well in my experience and I would say Deno should behave the same.

All 13 comments

How do other CLI tools handle this, like Node.js?

@kitsonk @kt3k I believe this is what we are expecting here: https://docs.rs/getopts/0.2.18/getopts/enum.ParsingStyle.html#variant.StopAtFirstFree

--help is special: I believe some of our customized code (to disambiguate help for deno and for v8) makes things a bit more complicated...

A proposal about flags:
Mixing deno and v8 flags is quite confusing. Since v8 flags are used less often than other deno flags, we could choose to prefix all v8 flags with v8, e.g. --expose-gc becomes --v8-expose-gc.

In Node, any flags/arguments that come after the user's script _belong_ to the user's script.

# foo's help is shown, assuming foo.js handles `--help` in some way
$ node foo.js --help
# Node's help is shown and foo.js is ignored
$ node --help foo.js

That works pretty well in my experience and I would say Deno should behave the same.

@sholladay Yeah that corresponds to StopAtFirstFree parsing style. Other flags at this moment follows this design well, only that --help is processed in a slightly different and problematic way: there is a collision between deno's help flag and that of v8.

For others who want to look into the problem: Node implements a much more complicated custom resolution logic themselves.
The problem we have here is that we want to distinguish v8 specific flags from deno ones: currently we first forward possible flags to v8 in v8_set_flags() and let v8 to remove ones it recognizes, and then we process with getopts in set_recognized_flags(). A few conflicts rises during this process:

  1. If we let v8 to select the ones it likes first, v8 has no idea about first free arguments (in our case, should be the filename to run) and would just consume flags that also comes after it: e.g. deno example.ts --gc-stats and you'll see v8 specific error message.
  2. If we let getopts to select the ones it likes first, we can set ParsingStyle::StopAtFirstFree to make it stop parsing when meeting with the first free arg (which is the filename) and happily forward the remaining flags after this free arg to the user. However, this means it would not recognize v8 specific flags occurring before the first free arg, unless we explicitly add them as options on our getopts::Options instance.

    • Node's custom solution seems follow this pattern, only forward to v8 if its own option parser does not recognize the name

As a side note, the current custom solution of Node seems not perfect either: for example, Node would display v8 error if you do node --gc-stats 1 example.js instead of node --gc-stats=1 example.js. However, since deno currently forward the args first to v8, so deno --gc-stats 1 example.ts works just fine.

In Node, any flags/arguments that come after the user's script belong to the user's script.

I'm against this for the following reasons:

  1. It's simpler if all flags are passed through. I think knowing that flags after the script belong to the script is just an extra thing people need to know.
  2. It's an ergonomic issue. I want to be able to type "[up key] -A" in the shell. That is append to the _end_ of the shell invocation flags.

I think we can satisfy my desires and support --help in user scripts with some one-off logic: if a script is specified in the args, pass the --help arg through.
Maybe this custom logic ends up being more complex for users than just doing things as Node does them (or using --). I'm willing to concede that if we continue to run into issues - but at the moment I'd like to try a bit harder to support CLI args in any position.

@kevinkassimo regarding V8 flags - I think it's a separate issue?

@ry My only concern about this design is that it would render attempts to use shebang that hides away deno broken... considering especially flags like -A, -D, etc. common. Breaks transparency.
(sidenote: we currently allow extensionless script with shebang run just fine with deno using .mime files)

V8 flags sound indeed like a separate issue. I brought it up here simply because our current logic of handling it seems "weird" and causes troubles if I want to change the flag code even a little bit.

My only concern about this design is that it would render attempts to use shebang that hides away deno broken... considering especially flags like -A, -D, etc. common. Breaks transparency.

@kevinkassimo I don't think I follow. Can you give an example?

@ry consider a script shebang (no extension):

#!/usr/bin/env deno
import * as deno from "deno";
console.log(deno.args)

with shebang.mime:

text/typescript

Run ./shebang -A, emits:

[ "./shebang" ]

-A is gone due to our flag processing logic. Would surprise users.

how about using --? that would potentially solve the issue when it matters (i.e. shebang case)

#!/usr/bin/env deno --allow-all --

the above shebang would pass specific arguments to deno while passing everything else to the program, and it's optional so users wanting to add deno arguments at the end would be able to do so.

Edit:
For this to work, the first argument would need to come after the --:

 deno -A -- program.ts -A

would pass -A to both the program and to Deno for example

This should be possible now with the clap-based flag parsing.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ry picture ry  路  3Comments

kitsonk picture kitsonk  路  3Comments

metakeule picture metakeule  路  3Comments

ry picture ry  路  3Comments

ry picture ry  路  3Comments