remkop/picocli

"the class has no constructor" for an inner class

Closed this issue · 3 comments

I have two programs that should take almost the same command-line arguments.
I thought to make the two different functionalities "subcommands" in the picocli sense.

So that each can observe the effects of command-line arguments passed to the main program, I made each subcommand an inner class (that is, a non-static nested class) of the main program. However, at run time picocli gives me the error "the class has no constructor" (see program and stack trace below). The class does have a zero-parameter constructor, but in the bytecode the <init> method has a parameter to which the enclosing class must be passed as an argument. Can picocli accommodate this?

More generally, what is a way to permit two different programs to accept mostly the same command-line arguments, which may be passed before or after the subcommand name (whichever the user prefers)?

Here is the picocli stack trace:

picocli.CommandLine$InitializationException: Cannot instantiate Main$MergeDriver: the class has no constructor
	at picocli.CommandLine$DefaultFactory.create(CommandLine.java:5689)
	at picocli.CommandLine$Model$CommandUserObject.getInstance(CommandLine.java:12273)
	at picocli.CommandLine$Model$CommandSpec.userObject(CommandLine.java:6441)
	at picocli.CommandLine$Interpreter.clear(CommandLine.java:13530)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13576)
	at picocli.CommandLine$Interpreter.processSubcommand(CommandLine.java:13915)
	at picocli.CommandLine$Interpreter.processArguments(CommandLine.java:13811)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13597)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13565)
	at picocli.CommandLine$Interpreter.parse(CommandLine.java:13460)
	at picocli.CommandLine.parseArgs(CommandLine.java:1552)
	at picocli.CommandLine.execute(CommandLine.java:2173)
	at org.plumelib.merging.Main.main(Main.java:24)
Caused by: java.lang.NoSuchMethodException: org.plumelib.merging.Main$MergeDriver.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3585)
	at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2754)
	at picocli.CommandLine$DefaultFactory.create(CommandLine.java:5660)
	at picocli.CommandLine$DefaultFactory.create(CommandLine.java:5687)
	... 12 more

Here is the program:

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

/** Acts as a git merge driver or merge tool. */
@Command(
    name = "plumelib-merge",
    subcommands = {Main.MergeDriver.class, Main.MergeTool.class, CommandLine.HelpCommand.class},
    description = "Acts as a git merge driver or merge tool.")
public class Main {

  /** If true, merge adjacent. */
  @Option(names = "--adjacent")
  public boolean adjacent = false;

  /**
   * Acts as a git merge driver or merge tool.
   *
   * @param args the command-line arguments
   */
  public static void main(String[] args) {
    int exitCode = new CommandLine(new Main()).execute(args);
    System.exit(exitCode);
  }

  /**
   * Does the actual work of merging.
   *
   * @param ms a merge state
   * @return int the status code indicating success or failure
   */
  private int mainHelper(Object ms) {
    // All the business logic is here.
    System.out.println(ms);
    return 0;
  }

  ///////////////////////////////////////////////////////////////////////////
  /// Subcommands (implemented as non-static inner classes)
  ///

  /** Acts as a git merge driver. */
  @Command(name = "driver", description = "Acts as a git merge driver")
  public class MergeDriver implements Runnable {

    /** Creates a MergeDriver. */
    public MergeDriver() {}

    @Override
    public void run() {
      Object ms = new Object();
      mainHelper(ms);
    }
  }

  /** Acts as a git merge tool. */
  @Command(name = "tool", description = "Acts as a git merge tool")
  public class MergeTool implements Runnable {

    /** Creates a MergeTool. */
    public MergeTool() {}

    @Override
    public void run() {
      Object ms = new Object();
      mainHelper(ms);
    }
  }
}

Thanks in advance for your help.

Hi @mernst !
Yes this is possible, you need to use a factory that passes the enclosing instance to the constructor of the inner class.

I have used this in testing:

src/test/java/picocli/InnerClassFactory.java

Can you give this a try?

@mernst Did that help?

I found the example a bit complex. I ended up changing the command line interface to prohibit arguments before the subcommand (and changing clients), plus some large code refactorings to accommodate picocli's preferred programming model. This works well enough. Thanks!