haxetink / tink_cli

Write command line tools in Haxe
MIT License
43 stars 14 forks source link

Wiki: Subcommands #22

Closed melMass closed 4 years ago

melMass commented 4 years ago

Hi Kevin,

I'm not sure that you are interested in this, or even if it's the right approach, but as I did not think tink_cli was able to do that, here is a small writeup on Subcommands with tink_cli:

See Kevin's answer for the official right approach. 🥇 I just ended a project where I needed to build a CLI with subcommands. Haxe is by far my favorite coding language nowadays I wanted to give tink_cli another try (first discovered the lib a year ago). One of the requirements I had was subcommands with their own flags. Here is how I went about it: ## A `MainCommand` to wire them all The idea is pretty simple, your main command is just used to run your subcommands as commands... An example will be clearer. Let's take this basic example: #### Main.hx ```haxe class Main { static function main() { Cli.process(Sys.args(), new MainCommand()).handle(Main.exit); } } ``` #### MainCommand.hx ```haxe class MainCommand { /** Be more verbose */ public var verbose:Bool=false; public function new() {} @:defaultCommand public function main_command(rest:Rest){ // print the help Sys.println(Cli.getDoc(this)); } } ``` tink_cli sets the **public var** `verbose` as a flag (-v or --verbose) and running the command simply prints the help. Now just like we did we run the main command we can run subcommands, let's add an `info` subcommand: ```haxe class MainCommand { /** Be more verbose */ public var verbose:Bool=false; public function new() {} @:defaultCommand public function main_command(rest:Rest){ // print the help Sys.println(Cli.getDoc(this)); } /** Get the news */ @:command public function info(rest:Rest){ Sys.println("INFO"); } } ``` This will work as expected but the issue is the more complicated your app gets the more separation you want.. The trick to separate commands and flags it to remove the first item from the Sys.args() before handling it: ```haxe class MainCommand { /** Be more verbose */ public var verbose:Bool=false; public function new() {} /** Returns Sys.args without the first arg. */ private function subargs():Array { var fargs = Sys.args(); fargs.remove(fargs[0]); return fargs; } @:defaultCommand public function main_command(rest:Rest){ // print the help Sys.println(Cli.getDoc(this)); } /** Get the news */ @:command public function info(rest:Rest){ Cli.process(subargs(), new InfoCommand()).handle(Cli.exit); } } class InfoCommand { /** Filters fake news */ @:flag("fake") public var verbose:fakeNewsFilter=false; @:defaultCommand public function main_command(rest:Rest){ // print the help Sys.println(Cli.getDoc(this)); } ``` We now have two separate sets of flags for subcommands
kevinresol commented 4 years ago

Subcommand is already supported via @:command public var info:InfoCommand = new InfoCommand();

melMass commented 4 years ago

@kevinresol Thanks !!

Glad to learn that! The only issue I have now is that it does not take the doc comment?
I tried above the meta, above the var declaration and above the class, none worked.

Is there a more advanced doc somewhere?

melMass commented 4 years ago

It doesn't work as expected using your method.

I use to be able to do:

    mycli new project

I'm I missing something ?

kevinresol commented 4 years ago

Just tested and it is working for me (https://github.com/haxetink/tink_cli/commit/25aaa82c763c5840ad54cec623330d1deaed28f1)

Can you give me more details?

melMass commented 4 years ago

Sorry for the delay, I will put together a sample of my issue later today!

melMass commented 4 years ago

The issue I had was that for the project command I rely on the Prompt argument, so there is no Rest and Sys.args is not filtered!

So it is working as expected. Is there a way though to handle both Prompt and Rest.

kevinresol commented 4 years ago

Please post an example

melMass commented 4 years ago
@:command("project") 
public function project_command(?prompt:Prompt,?rest:Rest<String>) {
    trace(rest);
    return prompt.prompt(Simple('Project Name').next( (input) -> {
        trace('Creating project ${input}');
    return Noise;
    });
}
kevinresol commented 4 years ago

and what is the error you are getting?

melMass commented 4 years ago

I used a workaround on the project that needed it, as it's unrelated to the original answer I'll close that one and reopen another one if I face the issue again. Thanks for the support