23

If I have 2 options defined as required such as:

 public static void main(String [] args){
      Options options= new Options();
      Option  inputFileOp=Option.builder("i").longOpt("input").hasArg().desc("Input file").argName("file").required().build();
        options.addOption(inputFileOp);

      Option outputFileOp=Option.builder("o").longOpt("output").hasArg().desc("Output file").argName("file").required().build();
        options.addOption(outputFileOp);

and a help option

    Option helpOp =new Option("h",false,"Show Help");
    helpOp.setLongOpt("help");
    helpOptions.addOption(helpOp);

and parser

DefaultParser parser = new DefaultParser();
CommandLine cmd=parser.parse(options,args);

if(cmd.hasOption(helpOp.getOpt())){
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp( "MyApp.sh", options );
        System.exit(0);
    }

}

When the user input for example myApp -h .. An exception is raised in the parsing step that there are missing required options, While I want to print the help data.

How to allow calling the help while keeping these options declared as required?

Mohamed Gad-Elrab
  • 636
  • 1
  • 6
  • 20

3 Answers3

17

The code for DefaultParser appears to always call the checkRequiredArgs() method. Which seems to indicate that you cannot, in one fell swoop, avoid the problem.

The way we have addressed this situation in the past, perhaps in a suboptimal fashion, is to parse the command line twice. The parsing is fast, so the overhead it minimal.

We created a method checkForHelp(String[] args) that takes the (String[] args). It adds only the help option to an options, parses the command line, and then ascertains whether help is specified. If so, the help is printed, and the program exits. Otherwise, the full set of options is processed. This approach allows for the required fields to be processed as expected. Note that the help option must also be added in the main list.

  public static Option helpOption = Option.builder("h")
          .longOpt("help")
          .required(false)
          .hasArg(false)
          .build();

  public static boolean checkForHelp(String[] args) throws ParseException  { 
    boolean hasHelp = false;

    Options options = new Options();


    try {
      options.addOption(helpOption);

      CommandLineParser parser = new DefaultParser();

      CommandLine cmd = parser.parse(options, args);

      if (cmd.hasOption(helpOption.getOpt())) {
          hasHelp = true;
      }

    }
    catch (ParseException e) {
      throw e;
    }

    return hasHelp;
  }

Then in the main method, something akin to:

    options.addOption(hostOption);
    options.addOption(portOption);
    options.addOption(serviceNameOption);
    options.addOption(helpOption); // <-- must also be here to avoid exception

    try {
        if (checkForHelp(args)) {
            HelpFormatter fmt = new HelpFormatter();
            fmt.printHelp("Help", options);
            return;
        }

        CommandLineParser parser = new DefaultParser();
        CommandLine cmd = parser.parse(options, args);


        if (cmd.hasOption("host")) {
            host = cmd.getOptionValue("host");
            System.out.println(host); // gets in here but prints null
        }
        if (cmd.hasOption("port")) {
            port = ((Number) cmd.getParsedOptionValue("port")).intValue();
            System.out.println(port); // gets in here but throws a null
                                      // pointer exception

        }
        if (cmd.hasOption("service_name")) {
            serviceName = cmd.getOptionValue("service_name");
            System.out.println(serviceName); // gets in here but prints null
        }
    }
    catch (Exception e) {
        e.printStackTrace();
    }

EDIT: as it turns out, this approach is similar to the answer provided here: Commons CLI required groups. I guess I feel better that our approach has others supporting what we believed.

EDIT2: In a quick test, I believe the problem of having required options may be avoided by using an "OptionGroup". Here is a revised checkForHelp that works by adding all of the options to an OptionGroup. In my quick testing, it avoids the problem that was present if one did, e.g., ("--arg1 --help").

public static boolean checkForHelp(String[] args) throws ParseException
{
    boolean hasHelp = false;

    Options options = new Options();


    try {
        options.addOption(hostOption);  //has required set
        options.addOption(portOption);
        options.addOption(serviceNameOption);
        options.addOption(helpOption);            

        // create an option group
        OptionGroup og = new OptionGroup();
        og.addOption(hostOption);
        og.addOption(portOption);
        og.addOption(serviceNameOption);
        og.addOption(helpOption);

        CommandLineParser parser = new DefaultParser();

        CommandLine cmd = parser.parse(options, args, false);

        if (cmd.hasOption(helpOption.getOpt()) || cmd.hasOption(helpOption.getLongOpt())) {
            hasHelp = true;
        }

    }
    catch (ParseException e) {
        throw e;
    }

    return hasHelp;
}
Community
  • 1
  • 1
KevinO
  • 4,303
  • 4
  • 27
  • 36
  • Thank you for the answer. Actually I have already tried it before despite it looks like a hack more than a formal way :(. Still the problem is if you have options object with help only, other options will throw unrecognized option exception. – Mohamed Gad-Elrab Apr 19 '16 at 15:08
  • Thank you for the edit also and the pointer I could not reach it when I was searching. In the answer comments they are complaining about the problems may arise from having 2 different groups of options – Mohamed Gad-Elrab Apr 19 '16 at 15:13
  • @MohamedGad-Elrab, It is true that other values can create that situation. However, look [here](http://stackoverflow.com/questions/6049470/can-apache-commons-cli-options-parser-ignore-unknown-command-line-options) for a quick way to potentially solve the unrecognized argument exception. – KevinO Apr 19 '16 at 15:14
  • @MohamedGad-Elrab, I believe the use of an "OptionGroup" will avoid the issue with other specified options throwing an Exception. I haven't tested extensively, but in my couple of tests it seems to work. Please review the code under "Edit2" and let me know if that works for you. I still think you will need to process the `args[]` twice, but I believe this approach addresses the requirements. – KevinO Apr 19 '16 at 15:42
7

Wouldn't it be easier to just parse raw args and check for --help/-h or help.getOpt()/help.getLongOpt() keyword before parsing using DefaultParser? This way you avoid double parsing overhead.

for (String s : args) {
    if (s.equals("-h") || s.equals("--help")) {  // or use help.getOpt() || help.getLongOpt()
        formatter.printHelp("ApplicationName", arguments);
        System.exit(1);
    }
}
krizajb
  • 1,715
  • 3
  • 30
  • 43
5

Add another method, as the previous poster said, but make sure you stop at the first unrecognized argument.

private static boolean hasHelp(final Option help, final String[] args) throws ParseException
{
    Options options = new Options();
    options.addOption(help);
    CommandLineParser parser = new DefaultParser();
    CommandLine cmd = parser.parse(options, args, true);
    return cmd.hasOption(help.getOpt());
}

If stopAtNonOption is set to false, then this function would throw for valid arguments, like java -jar app.jar -doStuff

Florin M
  • 179
  • 2
  • 9