pavlosgi/freecli

Allow options after arguments

rtfpessoa opened this issue · 14 comments

I started trying this in a project but stumbled in an issue.

I do not seem to be able to have options after arguments.

Is this a known limitation?

Example:

cmd("analyse") {
      takesG[AnalyseConfig] {
        O.help -- "help" ::
          O.string - 'd' -- "directory" -~ req -~ des("The directory to be analysed") ::
          string -~ name("toolName") -~ des("The tool to analyse the code")
      } ::
        runs[AnalyseConfig] { config =>
          println(s"Analysing code in ${config.directory} with ${config.toolName}")
        }
    }

If I tun this command with analyse tool --directory /path/to/dir it does not work,
but with analyse --directory /path/to/dir tool it works.

If it is a known limitation, where should I look to fix this? Do you think is feasible or might be hard to achieve?

This is tricky because arguments are positional. So were you to allow arguments to appear anywhere you would have to impose different rules on options.
this is fine analyse --directory /path/to/dir tool
this is fine analyse --directory /path/to/dir --directory
what about this analyse --directory --directory /path/to/dir? if arguments could appear anywhere what does that first directory represent? An argument or an option?

@pavlosgi I think that should not be valid.

If --directory is a flag you could have analyse --directory tool or just analyse tool --directory since you know it has no parameters.

But if it is an option, this analyse --directory tool would not work and would fail with missing argument.

Does it make sense?

analyse --directory --directory /path/to/dir if we say this is invalid, where --directory is an option
analyse arg --directory /path/to/dir but this is valid, aren't we making validation dependent on the option name?

Git is similar
git diff concept.txt concept.txt -p
fatal: bad flag '-p' used after filename

Yes, probably.

But I guess that would be a way to have contextual knowledge.
My objective would be to get something like Ubuntu apt, for example, where you can do apt install docker-ce -y or apt install -y docker-ce. And this is not necessarily better for the CLI developers but it can make the CLI usage much more friendly.

So the parser currently walks the arguments, first parses options and then arguments. You can have a look at that and see if you can modify it in a way that doesn't produce different weird edge cases. I would be more than happy to look at a PR. ConfigParserInterpreter is a good place to start

Ok, nice. I will see if I can do some changes.

If this type of change is made, can we then have multi-level commands with a large mixture of required and optional args? I have 2 levels of commands for most of my CLI before the actual required arguments are listed and then some --blah type flags which can apply to the different levels of the command. Would this handle such a scenario?

@aappddeevv what you mean is having some options or flags in the main command that can be shared by sub commands? For example if you have a --verbose flag you want it accessible in all sub commands and don't want to define it in all of them. At least for me that would be nice.
From my understanding the only shared options are help and version right?

Regarding my investigation, I had a hard time getting the code so I decided to invest some time investigating Free. I still got no time to completely understand how the CLI is wired together but I think that for my usage I might need the Free Monad and not the Applicative. Still I might be able to do something with the state to help that since the dsl introspection is important.

Edit: add more context

Have a look at some more commands and subcommands examples here https://github.com/pavlosgi/freecli/blob/master/core/src/test/scala/freecli/command/CommandHelpTest.scala. What we support is the ability when you run the command to access all parent configuration as well, meaning the type of run in a subcommand is a product containing the parent config type and the subcommand config type.

case class A(a1: Int, a2: Boolean, a3: String, a4: Int) // Parent config
case class B(b1: A, b2: String, b3: String) // Child config containing Parent config

@pavlosgi ahhh, seems like that is possible then. That is a nice example.

This might be of interest @rtfpessoa @aappddeevv 6ce33a5. This is released with 0.1.2

@pavlosgi thanks for the tip.