picocli icon indicating copy to clipboard operation
picocli copied to clipboard

Return a nonzero exit code when help/usage is displayed

Open elyograg opened this issue 1 year ago • 18 comments

The picoclii verson is 4.7.3, running with OpenJDK 17 on Ubuntu Server 22.04. The gradle project builds for compatibility with Java 11.

I can't figure out how to have my program return a nonzero exit code if it displays the usage help. I need my script to do something different based on the exit code. Changng the exit code is easy enough in my own program logic, but I can't figure out how to have picocli do it automatically.

This is the main method I have concocted with help from the documentation, but if I run it without any of the required parameters, its exit code is still zero:

  public static final void main(final String[] args) {
    try {
      final CommandLine cmd = new CommandLine(new MailCheckMain());
      cmd.setHelpFactory(StaticStuff.createLeftAlignedUsageHelp());
      cmd.getCommandSpec().exitCodeOnUsageHelp(1).exitCodeOnVersionHelp(1);
      cmd.execute(args);
    } catch (final Exception e) {
      throw new RuntimeException("Error starting program", e);
    }
  }

This particular class does implement Runnable, so my logic can be found in the run() method. I must be missing something.

elyograg avatar May 14 '23 10:05 elyograg

I think this can be accomplished in the annotations. See: https://picocli.info/#_exception_exit_codes And https://picocli.info/apidocs-all/info.picocli/picocli/CommandLine.Command.html#exitCodeOnUsageHelp()

@Command(exitCodeOnUsageHelp = 42) 
class Cmd { //...

remkop avatar May 14 '23 10:05 remkop

You may need to do this in the @Command annotation for the subcommands also...

remkop avatar May 14 '23 11:05 remkop

I already have the exitCodeOnUsageHelp parameter in the annotation. It's not working.

elyograg avatar May 14 '23 21:05 elyograg

This is the class signature:

@Command(name = "mail_status_check", sortOptions = false, scope = ScopeType.INHERIT, description = "End to End mail checker", exitCodeOnUsageHelp = 1)
public class MailCheckMain implements Runnable {

elyograg avatar May 14 '23 21:05 elyograg

If you have any interest, I can share a repo URL for it. It's not on github. I can't find a way to send a private message on github, or I would have already sent it.

elyograg avatar May 14 '23 23:05 elyograg

Let's try to get a minimal example that reproduces the issue. This test passes for me. Does it pass for you?

import org.junit.jupiter.api.Test;
import picocli.CommandLine;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class ExitCodeTest {
    @Test
    public void testStandardExitCode() {
        @CommandLine.Command(mixinStandardHelpOptions = true)
        class App implements Runnable {
            public void run() { }
        }

        CommandLine cmd = new CommandLine(new App());
        int exitCode = cmd.execute("-h");
        assertEquals(CommandLine.ExitCode.OK, exitCode);
        int exitCodeVersion = cmd.execute("-V");
        assertEquals(CommandLine.ExitCode.OK, exitCodeVersion);
    }

    @Test
    public void testCustomExitCode() {
        @CommandLine.Command(mixinStandardHelpOptions = true, exitCodeOnUsageHelp = 33, exitCodeOnVersionHelp = 44)
        class App implements Runnable {
            public void run() { }
        }

        CommandLine cmd = new CommandLine(new App());
        int exitCode = cmd.execute("-h");
        assertEquals(33, exitCode);
        int exitCodeVersion = cmd.execute("-V");
        assertEquals(44, exitCodeVersion);
    }
}

remkop avatar May 15 '23 00:05 remkop

Okay, looking at your original code, I can see the answer: the code needs to call System.exit from the main method with the result of CommandLine::execute.

I believe this will work:

  public static final void main(final String[] args) {
    try {
      final CommandLine cmd = new CommandLine(new MailCheckMain());
      cmd.setHelpFactory(StaticStuff.createLeftAlignedUsageHelp());
      cmd.getCommandSpec().exitCodeOnUsageHelp(1).exitCodeOnVersionHelp(1);
      int exitCode = cmd.execute(args);
      System.exit(exitCode);
    } catch (final Exception e) {
      throw new RuntimeException("Error starting program", e);
    }
  }

remkop avatar May 15 '23 02:05 remkop

(By the way, picocli does not throw any checked exceptions, so I believe you can omit the try/catch from the main method.)

remkop avatar May 15 '23 02:05 remkop

Yep, the test worked. I added the System.exit() and now it all works. And I removed the try/catch with no compile errors. It's basically a reflex adding that, because it's very often required.

elyograg avatar May 15 '23 03:05 elyograg

Or maybe it's not completely correct. Running with no options kicks back an exit code of 2, but I set both of the exit codes to 666, so I could be absolutely sure that the exit code would be very unique for usage printing. It's nonzero now which makes my script work better, but not completely right.

elyograg avatar May 15 '23 03:05 elyograg

The built-in exit codes are as follows:

By default, the execute method returns CommandLine.ExitCode.OK (0) on success, CommandLine.ExitCode.SOFTWARE (1) when an exception occurred in the Runnable, Callable or command method, and CommandLine.ExitCode.USAGE (2) for invalid input. (These are common values according to this StackOverflow answer). This can be customized with the @Command annotation.

Running with no options kicks back an exit code of 2 ... It's nonzero now which makes my script work better, but not completely right.

As I recall your command requires a subcommand to be specified, so an exit code of 2 (incorrect usage) seems correct: When a required option or required subcommand is missing, the execute method will return exit code 2. Is your expectation different?

remkop avatar May 15 '23 06:05 remkop

As I recall your command requires a subcommand to be specified, so an exit code of 2 (incorrect usage) seems correct: When a required option or required subcommand is missing, the execute method will return exit code 2. Is your expectation different?

This is a different project than the one that requires a subcommand. For this one, only the one Command class is defined. Picocli is my new hammer, and I am finding a lot of nails to use it on. :) It's a GREAT tool.

I had set both the annotation parameter exitCodeOnUsageHelp = 666 and cmd.getCommandSpec().exitCodeOnUsageHelp(666); so I was expecting to see an exit code of 666. I remember that with those setting, the exit code was 154 ... no idea at all where that came from.

The value of 2 is something I can use, and I have done so, but I was hoping for the very distinctive value.

elyograg avatar May 16 '23 02:05 elyograg

I wonder if I should have put quotes around 666 for the annotation...

elyograg avatar May 16 '23 02:05 elyograg

Can you provide a test (maybe like what I did above) to demonstrate the 666 vs 154 issue you mention?

Quotes in the annotations won’t compile (int value only).

And glad to hear you are enjoying using picocli! 😅

remkop avatar May 16 '23 05:05 remkop

@elyograg were you able to reproduce the 666 vs 154 issue you mentioned? Could you provide a program to reproduce it?

remkop avatar May 25 '23 02:05 remkop

@elyograg were you able to reproduce the 666 vs 154 issue you mentioned? Could you provide a program to reproduce it?

I will need to make a minimal project and experiment.

elyograg avatar May 25 '23 02:05 elyograg

Yes, this is several months later, but… It's probably because exit code is always 8 bits? because 666 % 256 = 154

ambre-m avatar Apr 02 '24 12:04 ambre-m

Good point! Java can return any int value as exit code, but the OS or shell may truncate it to a single byte…

remkop avatar Apr 02 '24 20:04 remkop