c-blake / cligen

Nim library to infer/generate command-line-interfaces / option / argument parsing; Docs at
https://c-blake.github.io/cligen/
ISC License
496 stars 23 forks source link

Best way to catch all/most exceptions in a cli tool #180

Closed halloleo closed 3 years ago

halloleo commented 3 years ago

I am currently prettifying the UI of a CLI tool I have written with cligen. It works rather well. :slightly_smiling_face:

For exceptions that bubble up to the main proc I want to show a nice error message instead of the long exception stack. At the moment I use a construct like this

  try:
    dispatch(...)
  except Error as e:
    ...

Do you think this is a good way to do it or does cligen provide some mechanism for this I have just overlooked?

Many thanks for pointers!

c-blake commented 3 years ago

Yes. I think try: dispatch.. is best/fine if you want the exceptions caught only at the CLI level.

If there are many exceptions that can be thrown, it might make for a nicer API entry point to catch them inside the wrapped proc and translate the multiplicity into a few high level ideas for the APi caller to maybe catch or not. Tastes vary.

Many people just use cligen as a way to write API-less CLIs in which case there is no real difference.

halloleo commented 3 years ago

Thanks @c-blake!

Indeed, the try: dispatch... is exactly for the error messages from cligen!

Coming from Python's click it did surprise me that usage errors raise exceptions rather than printing out the error message. But it's OK to handle it in my main.

c-blake commented 3 years ago

I assume you mean "usage errors" being things other than CLI syntax errors like unknown options or unparsable int parameters and the like which should get cligen-internal messages.

It sounds like click is less embracing of the dual API-CLI sense of things. That may let it be more work-saving on the part of CLI authors, but also it may discourage people keeping the API alone part nice. The latter is helpful to preserve clean Nim-access as well as CLI access. Someday someone may be firing up a million calls and really like the lower overhead API, though of course there are many ways to layer a software system. :-)

halloleo commented 3 years ago

You are spot on: Click works with decorators which change the function - and make it in the decorated form unusable as an API function.

c-blake commented 3 years ago

This is somewhat covered in my more MOTIVATION document. Many people will recommend switching away from shell scripts to a "real" programming language (like Nim!) once "complexity" goes "above X". This is a subtle point, usually only understood after seeing whole systems of software evolve over years.

But if some picky CLauthor has layered their system in a way to make that hard you wind up with integration complexity..E.g., someone on the Nim IRC was doing an "inverse cligen" that would take the help output of some program and create an API call to invoke it. But then they need to deal with all the shell quoting rules to invoke as well as parsing (in general) incredibly irregular natural language help messages to infer the API. It's all a mess. Better to just never have made API access hard in the first place.

The way cligen works is just trying to "encourage" this better layering. It doesn't really force it and it is certainly possible to layer well without it. As already granted, I'm sure there are ways to layer the Python so the Click part is just a razor thin wrapper around APIs, do if __name__ == "__main__" guards, etc. But there will always be a temptation to open code something important just into the CLI part.

You aren't the first person to wonder about this. I remember discussing it in the past with at least @kaushalmodi.

halloleo commented 3 years ago

When you talk about "inverse cligen", do you mean the docopt project? A Nim version exists now, but I used it a few years ago in Python. I was a big fan of it, but what steered me away was the fact that the paramter values all end up in a big dict where it is too easy to miss-spell a key when you need to access a value.

c-blake commented 3 years ago

Not docopt (which I am aware of as a kind of antipode to cligen maybe along a "different axis" from what I mean here), but this. I guess it was @haxscramper.

He does not elaborate, but the apparent idea is that CLI invocation syntax can be a mess compared to Nim invocation. So, rather than system("...CLI mess I cannot remember which is checked only at run-time") you would use the "--help" output to generate a Nim API call for the desired functionality, and then call that API instead. It would still be slow like any exec, but more "all Nim, all the time". So, call it "apigen" if you like.

The mechanism/architecture is sort of how Zsh/Bash will parse the output of "--help" to automate doing completion. As per early in the cligen README, I did check that cligen's default help output works with Zsh/Bash in this way (and on a frequent basis I re-check that the Zsh one works just as a CLuser), but you seem to want to stray pretty far from the defaults. So, you should probably double check that your versions work, too. Plus, your fingers might like completion. :-)

Anyway, @haxscramper's apigen idea is not bad, but it is difficult. To get the kind of "unknown option" feedback at compile-time you mention being nice relative to your docopt experience, it would make most sense to set up these wrappers at compile-time. The huge variety of command line syntaxes, however, induces this need for a "database"/big forest of command-specific metadata/logic -- just like the shell completion engines have. Many commands do not even have "--help" at all, and you cannot even "just run the command and see" because that might block reading standard input or try to open (or worse create!) a file named "--help" or some such. So, "coverage" must be "opt in" and somewhat limited.

Indeed, the best design here would actually just parse Zsh/Bash/.. syntax definitions "crowd sourcing" the domain-knowledge complexity to all the shell users. That would mean you have to write almost a "whole shell" at compile-time in Nim (or maybe syntax translator helper programs to move the Zsh|Bash definition "completion sub-syntax" into something Nim could parse at C-T to form an API). This is non-trivial because "being interpreted/dynamic" is almost hard-wired into shell syntax. Andy Chu over at OilShell has been working on a static compiler for Unix shell for years. Chances are good one could get 95% of the way there with 5% of the effort for completion rules alone, though, and just flag unparsable completion rules for manual edit.

Another wrinkle is that any "generic help format" commands in question must be available at compile-time (to run to get the help message to form your API calls). So, you would probably have to declare this "apigen" to only support such common format commands when they are available at compile-time. Maybe not a deal breaker, but a definite limitation that ironically penalizes programs that are most consistent in their help output by being less supported than idiosyncratic programs that need unique TAB completion database entries. This even matters since programs can "grow new options" over time and so only the version of the program available at compile-time is really supported. That could differ from the version on a deployed system. Yikes.

haxscramper commented 3 years ago

I wanted to write a tool, similar to let's say c2nim that generates a static description of the CLI syntax, and then call it by creating objects and calling it. So general workflow would be something like

var cmd = newConvertCmd()
cmd.arg "-"
cmd.channel = "RGB"
cmd.negate = true
cmd.arg "-"
cmd.execShell()

newConvertCmd() would create an object of type ConvertCmd that has all necessary fields (and setters in the form of flag= and option= for correct types).

This can be further wrapped into syntactic sugar, for example

execShell(shCmd(npm, --silent, link, "nan"))

Such object description can be generated by parsing manpages, or --help for commands, and covering most of the different CLI syntax rules will be sufficient, given there is an escape hatch for passing raw unconstrained options to command invocation.

Parsing of completion description for fish/bash/zsh shells is possible, but not relevant for use case that I talked about on IRC. Can be done using something like bash parser. There is no tree-sitter grammar for fish shell, but I don't think it is particularly difficult to write (because language does not have a lot of quirks wrt. to syntax)

haxscramper commented 3 years ago

Main use case - interfacing with more heavyweight CLI applications that might have dozens or even hundreds of switches, subcommands (docker, github CLI, ffmpeg, imagemagic convert, various compilers), without the need to remember all the switches, flags, and not having to constantly resort to string interpolation. Subcommands can be described using additional objects, and nim discriminant allow to model this really nicely.

Additional bonuses include:

c-blake commented 3 years ago

Picking the shell with a completion database whose syntax is cleanest sounds smart, and maybe Fish is that shell. I don't think I misdescribed the idea much as it is a very similar problem to completion, but I do appreciate your elaboration. :-)

As a simple example of problematic dynamism, a command could support some --option only if a certain environment variable is set (like $TERM, say). Supporting that would likely be out of scope/more a documentation issue.

The super complex command use cases are also the ones most vulnerable to compile-/run-time version skew { new features are almost exponential - proportional to existing features :-) }. But I don't want to sound discouraging. As I said, I think it's a good idea!

c-blake commented 3 years ago

(gcc is another good example with 1000s of options these days...)

c-blake commented 3 years ago

{ gcc --help=params | grep -e--param gives like 235 answers alone on gcc-10.2 :-) }