Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for @Command annotation on methods (in addition to classes) #416

Merged
merged 3 commits into from
Aug 23, 2018

Conversation

illes
Copy link
Contributor

@illes illes commented Jul 16, 2018

allowing @Command annotation on methods cuts the boilerplate in situations where a class would have to be created just to allow integration with picocli.

Consider a simple tail example:

@Command(name="tail")
public class Tail implements Runnable {
    	
    @Option(names="-n", defaultValue="10")
    int n;
    	
    @Parameters()
    File[] files;

    public void run() {
        // ... do your thing
    }
}

versus

    @Command(name="tail")
    void tail(@Option(names="-n", defaultValue="10") int n, File... files) {
        // ... do your thing
    }

The attached commit is a minimal implementation and test cases for this feature.

@kravemir
Copy link

Quite interesting idea... Can it be used like app interface list, app interface <ID> up/down?

@codecov-io
Copy link

codecov-io commented Jul 16, 2018

Codecov Report

Merging #416 into master will decrease coverage by 0.87%.
The diff coverage is 76.02%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master     #416      +/-   ##
============================================
- Coverage     89.23%   88.35%   -0.88%     
- Complexity      264      278      +14     
============================================
  Files             4        4              
  Lines          3566     3702     +136     
  Branches        851      882      +31     
============================================
+ Hits           3182     3271      +89     
- Misses          188      220      +32     
- Partials        196      211      +15
Impacted Files Coverage Δ Complexity Δ
src/main/java/picocli/CommandLine.java 88.23% <76.02%> (-1%) 150 <13> (+14)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 19a3524...a6fd8d4. Read the comment docs.

@illes
Copy link
Contributor Author

illes commented Jul 16, 2018

@kravemir : if it is possible using the master branch, it should be doable with this feature too, as the processing of @Option/@Parameters annotations is the same. Do you happen to have a working example we could try to adapt?

@illes
Copy link
Contributor Author

illes commented Jul 16, 2018

@kravemir : with a bit of tweaking, this example works even without having to use @ParentCommand:

@Command(name="app", subcommands={App.Interface.class})
class App {

    @Command(name="interface")
    static class Interface {

        @Parameters(arity="0..1", description="network interface")
        String iface;

        @Command void list() { /* ... */ }
        @Command void up() { /* use this.iface */ }
        @Command void down() { /* use this.iface */ }

        // ...

Results in:

Usage: app interface [<iface>] [COMMAND]
      [<iface>]   network interface
Commands:
  up
  down
  list

Is this the use case you were thinking about?

@kravemir
Copy link

@illes It's almost what I though about. But, I would like something more like:

Usage: app interface list

Together with:

Usage: app interface [iface] [COMMAND]
      [iface]   (required) network interface
Commands:
  up
  down
  reset
  ...

@illes
Copy link
Contributor Author

illes commented Jul 16, 2018

@kravemir : short answer: no, picocli does not offer that parsing logic, and my proposed changes do not affect parsing

picocli goes something like:

  • for iface to come before the up/down command, it has to be a @Parameter to the parent @Command
  • iface can be either required or not controlled by the compile time constraint arity, hence you cannot decide to make it required based on runtime information (the value of COMMAND determining the subcommand to use for additional parsing)

as a workaround, you could e.g.:

  • throw a (Parameter)Exception in list() if iface is defined, and in up()/down() if it is not
  • create two separate CommandLine instances and chain them
    • when printing usage: print usage for both
    • when executing: try CommandLine.parse(..) on the one with list, if that fails try the other one, before actually executing via run()/call()

@remkop
Copy link
Owner

remkop commented Jul 16, 2018

This looks very interesting! Thanks for the PR!

I need a bit of time to finish up some other things (1 and 2 ) I'm working on, but I'll get back to this end of this week.

@remkop
Copy link
Owner

remkop commented Jul 21, 2018

I've started to take a look at this. I like the idea, and I'd like to evolve this so it can be merged.

I noticed that users need to obtain a Method instance to execute the run convenience methods. I would like to take the burden of reflecting on the class to get a Method handle into the library, so users can just call:

CommandLine.invoke("methodName", MyApp.class, args);

The CommandLine.run method is used for command classes that implement Runnable, so a different name may be better. I used invoke in the example above but am open to suggestions.

Still looking at the code and thinking about this...

remkop added a commit that referenced this pull request Jul 21, 2018
@illes
Copy link
Contributor Author

illes commented Jul 23, 2018

@remkop : Thanks for reviewing. I implemented the name change from run to invoke (also adding an Object return value), and added reflection logic helper CommandLine.getCommandMethods(...) to the library, unit tests have been updated to reflect/cover these changes.

One way to evolve would be to add a boolean flag to @Command that would trigger addMethodSubcommands() automatically.

@remkop
Copy link
Owner

remkop commented Jul 23, 2018

Thanks for updating! I’ll take another look.

By the way, I found this library https://github.com/tomitribe/crest which has a similar style of defining commands with methods.

Not intending to imitate, but it might be useful to see how they approached things or solved certain issues. Not sure if they support subcommands for example. Anyway, their docs may be interesting.

remkop added a commit that referenced this pull request Aug 1, 2018
@remkop remkop added this to the 3.6 milestone Aug 17, 2018
@remkop
Copy link
Owner

remkop commented Aug 19, 2018

Sorry for the wait! This has finally bubbled to the top of my todo list now.

Good point about adding some way to make command methods subcommands of the command class. Like you suggested, one way is to have a addMethodSubcommands boolean on @Command.

A potential alternative way would be to simply always add command methods as subcommands of the command class that contains the method if the class is annotated with @Command.

If the class is not annotated with @Command, the methods annotated with @Command are top-level commands (and the desired command needs to be called from the main method with CommandLine.invoke).

What do you think?

Can I ask a favour: would it be possible to rebase on master? (If that's too much hassle I can do a merge, but I like the cleaner history of a rebase.)

remkop added a commit that referenced this pull request Aug 20, 2018
…-command-methods-v2

# Conflicts:
#	src/test/java/picocli/CommandLineTest.java
remkop added a commit that referenced this pull request Aug 20, 2018
remkop added a commit that referenced this pull request Aug 20, 2018
@illes
Copy link
Contributor Author

illes commented Aug 21, 2018

I like the idea: addMethodSubcommands=true added, incl. javadoc and tests. Rebased on master squashing fixups.

@remkop
Copy link
Owner

remkop commented Aug 21, 2018

Sorry for the late response: traveling with family and have intermittent access to internet and PC.

Thanks for the changes!

@remkop remkop merged commit 44f7f84 into remkop:master Aug 23, 2018
@remkop
Copy link
Owner

remkop commented Aug 23, 2018

@illes I went ahead and merged the PR. Many thanks!

There are a few changes I would like your opinion on:

  • Do we need the @Command(addMethodSubcommands) attribute? I was thinking to add method commands automatically as subcommands if the class has a @Command annotation.
  • Similarly, I was thinking to make the CommandLine#addMethodSubcommands and getCommandMethods methods private.
  • I am not sure about mixing in options/positional params from the containing class @Command into the method commands. I would prefer not to do this.
  • That said, it would be good if command methods can mix in the standard help options. Currently if you annotate a method with @Command(mixinStandardHelpOptions = true), and invoke that method with CommandLine.invoke("method", MyClass.class, "--help") you will get a NullPointerException. The NPE is easily fixed by the mixinStandardHelpOptions still need to be mixed in for command methods.
  • We need documentation for command methods. Could be after 15.1 Annotated Methods or perhaps a separate page. Users probably are interested in a recommendation on when to use which annotation style.

I have some work in progress to fix the NPE for methods with @Command(mixinStandardHelpOptions = true) and will push that soon.

remkop added a commit that referenced this pull request Aug 23, 2018
@remkop
Copy link
Owner

remkop commented Aug 23, 2018

Also, #22 will dovetail nicely into this PR: that will allow users to specify description text in a separate properties file (or other resource bundle) to keep the method parameter annotations brief and readable.

remkop added a commit that referenced this pull request Aug 27, 2018
remkop added a commit that referenced this pull request Sep 6, 2018
…ir starting index from their method parameter position, but their max index from the type
remkop added a commit that referenced this pull request Sep 12, 2018
charphi pushed a commit to charphi/picocli that referenced this pull request Sep 17, 2018
# Conflicts:
#	src/main/java/picocli/CommandLine.java
#	src/test/java/picocli/CommandLineTest.java
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants