c-blake / cligen

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

How do I make global options? #198

Closed bandithedoge closed 3 years ago

bandithedoge commented 3 years ago

I have a CLI program with multiple subcommands but there are some options I'd like to share between them, like config path. Setting them for every single subcommand would be pretty dumb so is there a cleaner way?

This is my code right now.

import cligen
include cligen/mergeCfgEnv
dispatchMulti([project, help = { "pkgList": "List of packages", }],
              [search]
              )
c-blake commented 3 years ago

The cligen multicommand setup does not really have global options (other than help). AFAIK, there isn't really a "standard" for multi-commands/global options out in the world, though there are a few common-ish frameworks.

What I might suggest as a workaround would be an environment variable or three. Many programs use these to direct programs a config file. It's just import os; let foo = getenv("BAR", "mydefault") in Nim. It's probably a pretty close semantic match from a "globalness" point of view.

c-blake commented 3 years ago

Oh, also many are unaware that most shells (e.g. posix) let you prefix a command with an assignment to override the environment for it as in varNm=val myCommand.

bandithedoge commented 3 years ago

Thank you for your help, I'm going to try using environment variables.

c-blake commented 3 years ago

I should say that the way I implement multi-commands/sub-commands does have this kind of surrogate "multi" slot you can use to set overall metadata like the top level usage (table of subcommands) and that sort of thing in dispatchMulti. It might not be impossible to add a new compile-time parameter there to define "global" options. If you want to try, you can, but that code is kind of tricky. PRs are not unwelcome - just hard. For now I am closing this issue, though, since you probably have a solution.

You can look at https://github.com/c-blake/procs (at the very bottom of procs.nim) for some more techniques, and look at its "shared config file" (whose default location is env-var overidable) where I just do sections for each subcommand.

bandithedoge commented 3 years ago

I'll definitely take a look at that, thanks.

binaryben commented 1 year ago

PRs are not unwelcome - just hard.

Out of interest, how hard are you imaging this would be? I could benefit from a global flag option for things like -vvv to set logging level. Config files can be set with an environment flag and I'll use that for now, but that would also be nice to have as a global flag - it's a convention I use frequently with various CLI tools I use

c-blake commented 1 year ago

There are a few complexities. I would say the worst one is "binding". So, say you have a verbosity integer level in your top-level dispatcher. What does that map to in your Nim code? I.e., where do we store the output of whatever we parse. All the other bindings are user-visible/user-specified parameters, but that multi-dispatcher is library-internal

So, I'm not sure what would be nicest. We could have that higher level dispatcher take a "binding alist", basically another Table-like {paramname: variable} thing. I think this was discussed in another issue..Let me look.

c-blake commented 1 year ago

Here it is: https://github.com/c-blake/cligen/issues/128

The command-line invocation V=3 mainCmd subCmd is not really much more verbose than mainCmd -vvv subCmd. It is true that then if your code launches any subprocesses they will inherit the "V" variable unless you delEnv("V") right after you read it. And I realize there are other CLI toolkits out there which decided to make sense of mainCmd -vvv subCmd. I'm not sure any has truly dominant mindshare in the universe of all CLIs, though. (I.e. is the one to copy.)

Also, you can do something like test/SemiAutoMulti.nim and I am amenable to making the work needed there even less, but stopping short of the full automation of dispatchMulti.

binaryben commented 1 year ago

I have been wondering about the best place to store state of common options as well. I come from a Node background mainly where object oriented classes feature more heavily. The library I like for CLI gen in Node is Oclif (n.b., compiled CLIs is one of the main reasons I went looking beyond Node because I didn't want to depend on having a language installed). Oclif lets you can create a base command class which defines any global options.

Unfortunately that doesn't seem to be an option here if I am understanding Nim well enough - and actually, it would be entirely counterintuitive to the way cligen works as well if I am understanding the philosophy behind the library.

The best idea I have come up with is allow the procedures called by the dispatcher to take in a shared object as an argument. It would work well for the way I am using cligen as the procedures that actually run the commands are specific to the CLI only, and call the underlying library. Even though it would work for the way I am using cligen though, I still don't really like how tightly it couples those procedures with the CLI side of things.

The other option I've considered which is very much a hack is to parse the args before calling the dispatcher, and setting temporary environment variables ... i.e. $SSK_INTERNAL_VERBOSE_LEVEL. Make sure the internal envs are cleared at the start of every CLI run and then set appropriately

binaryben commented 1 year ago

Just to add some clarity:

And I realize there are other CLI toolkits out there which decided to make sense of mainCmd -vvv subCmd.

I'm not a particular fan of options needing to be in a specific order to have greater meaning when invoking a multi command CLI - -vvv should be able to go anywhere imho, or even being required to go after the subcommand is fine.

Setting them for every single subcommand would be pretty dumb so is there a cleaner way?

It's this point from the OP I am also trying to avoid :)

c-blake commented 1 year ago

Another complexity is that cligen the compile-time lib needs the type as well as the names to route the appropriate argcvt.

The "reaching up the tree" is kind of tricky/risky, but "broadcasting down the tree" is easier. So, e.g., in https://github.com/c-blake/procs/blob/master/configs/cb0/config I have an "include" syntax which works fine assuming each subcommand takes the same options with the same types & semantics. So, if they all have a -v, etc.

Anyway, I think that ultimately - much like the shared object your Oclif uses - some kind of shared declaration state/scope for the outer command is almost fundamental to the common conceptualization of the problem. But for cligen to do that it would have to at least have the user propagate that info into the library and the user may have to use a very stylized setup.

It is probably cleanest to aggressively add code to make test/SemiAutoMulti.nim very straightforward. Then arbitrary logic, nesting, subscopes, etc. can then all be wedged in as needed. The CL-author (there are always >=3 roles CL-metalibrary, CL-author, CL-user) would probably still be forced to make -vvv come before the subcommand, or at least before the part "forwarded on". Sometimes people want to immitate the exact syntax of other things. There may still be limits. I think besides Oclif that there are popular Go libs for this. I mean, it took forever for GNU --long-opts to even catch on and they aren't universal.

c-blake commented 1 year ago

BTW, I do like your shared object idea and you could maybe do your own argcvt with some sub-syntax. The options would all still come after the mainCmd subCmd, though.

I mean, if you just want the special "repeat => levels" thing of -vvv shared then you could just use a distinct int and a shared parent scope argParse & argHelp. Then each subcommand just has to take a .., verb=Repeater(1)).

But note that the default verbosity level as well as whether it even makes sense at all might vary across subCmds or not even make sense for some, like how color schemes make sense in my procs display and procs scrollsy but not my procs find. Sometimes, it's hard to "have it all". :-/

binaryben commented 1 year ago

Just woke up and re-reading

Also, you can do something like test/SemiAutoMulti.nim

Interesting, sorry I didn't look much at that file when looking through the test cases. So that means my assumption that we can't have what amounts to a base command is wrong? I take it that is what proc multi(subcommand) is?

c-blake commented 1 year ago

Presactly! First, I apologize for the length of this, but I have a sense that your are right at the cusp of fully grokking the problem. multi is a fully synthetic/generated proc not much under CLauthor control. You can (maybe!) get some sense of what is happening by compiling some small test skeleton with -d:printDispatchMultiGen -d:printDispatchDG -d:printDispatchMulti and re-directing the stderr to some file to look at.

I just synthesize/generate a proc to do subcommand-dispatching and call the regular dispatchGen on that new proc, but that acts as the "global layer". At first I thought this was elegant re-use, but now I am not sure (in retrospect) even having dispatchMulti at all was the best design decision and there are some weirdnesses at sub-sub-command levels.

Scare quotes are there since really it can maybe be nested multiple times...like cmd sub1 sub2 .. and thencmd -v sub1 -v sub2 -v might happen and, well, what are the semantics of that? Are they all bumping a global or are there 3 variables? The World At Large probably does not have any standard answer for us to even copy and I could see people wanting different things in different cases. People just say "global" options, but it's really a whole nested hierarchy and people seem to have a lot of varying ideas about how help and "more" global flags should work.

But that global layer is not 100% not under CLauthor control! Right now you can add a --version/-V type global flag that prints the version string and exits. That is all pseudo-hard-coded into cligen, though (including a VersionOnly exception). test/Version.nim has example code for a single-dispatch. Since --version just raises a VersionOnly exception to print a string & quit, it is A) more fully committed to a "CLI app" as opposed to library usage, and B) there just are no questions about "compositionality" like cmd -v sub1 -v.

For the version string I use this field version in ClCfg which is a parameter to the various dispatch things, but also has a default global variable. So, you can just say const nimbleFile = staticRead "procs.nimble"; clCfg.version = nimbleFile.fromNimble "version" once and then get the same string at the global level and all local levels. But the user code does not need to access the version string while it probably would need to access a log level, and activity itself (quit) is short-circuiting. So, it was kind of easy to hard code.

The simplest way to get what you want might be to demand CLauthors wanting this use a new distinct int Log-Level type (e.g. cligen.LogLevel) and to override the argcvt overload for it in order to set a "more global" variable under their control. Then the generated multi just needs to optionally grow a new parameter with user specified names. But even this is thornier than it might first seem. I can already see people requesting its "opposite", and a "pin" one as well as a "bump up" one - so, -q, -v, --level. One also has to think about how it would interact with logging APIs like std/logging or https://github.com/status-im/nim-chronicles setting their level. So, it feels like a slippery slope to hard coding a lot of things that then only the requester (and maybe myself) would understand/agree on.

This maybe makes more sense of me harping on "make test/SemiAutoMulti.nim more dirt simple" as the best answer. It would allow all manner of -vvv -qq --level and nesting, reference to any "imported" types from logging APIs, and Nim scopes/general code structuring/variable access afford a lot of organizational power. To be honest even that solution might still need some kind of new power/convenience for dispatched-to procs under some composition ideas. I think a distinct int type and custom argParse and subcmd-level indexed seq (or seq of seqs to cover cmd -v sub1 -v -q:2 sub2) might all be able to work, but it gets messy fast. It would also make users wanting fancy sub-sub-... command syntaxes more intimately aware of how they are really creating a new programming language within a token vector..(as options themselves already are...).