Ruiqi Dong created CLI-346:
------------------------------

             Summary: Options.addOptionGroup() silently overwrites existing 
options while addOptions() throws exception for duplicates
                 Key: CLI-346
                 URL: https://issues.apache.org/jira/browse/CLI-346
             Project: Commons CLI
          Issue Type: Bug
          Components: Options definition
            Reporter: Ruiqi Dong


The {{Options.addOptionGroup()}} method silently overwrites existing options 
when adding an {{OptionGroup}} that contains options with keys already present 
in the {{Options}} instance. This behavior is inconsistent with 
{{{}Options.addOptions(){}}}, which throws an {{IllegalArgumentException}} when 
attempting to add duplicate options. This inconsistency violates the principle 
of least surprise and can lead to subtle bugs where previously configured 
options are unintentionally overwritten without any warning.

 

{*}Impact{*}:
 * Can cause unexpected behavior in applications using Commons CLI
 * Previously required options may become optional without warning
 * Configuration may be silently lost
 * Debugging such issues is difficult due to lack of error messages

 

*Test Case:*

 
{code:java}
@Test
void testAddOptionGroupShouldThrowExceptionForDuplicateOption() {
    // Arrange
    Options options = new Options();
    
    // Add an existing option
    Option existingOption = new Option("f", "file", true, "Input file");
    existingOption.setRequired(true);
    options.addOption(existingOption);
    
    // Create an OptionGroup with the same option key
    OptionGroup group = new OptionGroup();
    Option duplicateOption = new Option("f", "file", true, "Config file");
    group.addOption(duplicateOption);
    
    // Act & Assert
    // This test expects addOptionGroup to throw exception like addOptions does
    // BUT IT FAILS - demonstrating the bug
    IllegalArgumentException exception = assertThrows(
        IllegalArgumentException.class, 
        () -> options.addOptionGroup(group),
        "addOptionGroup should throw IllegalArgumentException for duplicate 
option key, " +
        "consistent with addOptions() behavior"
    );
    
    // This assertion would verify the exception message (if exception was 
thrown)
    assertTrue(exception.getMessage().contains("f") || 
              exception.getMessage().contains("duplicate") ||
              exception.getMessage().contains("Duplicate"),
              "Exception message should mention the duplicate key");
}{code}
{*}Test Failure Output{*}{*}:{*}
{code:java}
[ERROR] 
org.apache.commons.cli.OptionsTest.testAddOptionGroupShouldThrowExceptionForDuplicateOption
 -- Time elapsed: 0.001 s <<< FAILURE!
org.opentest4j.AssertionFailedError: addOptionGroup should throw 
IllegalArgumentException for duplicate option key, consistent with addOptions() 
behavior ==> Expected java.lang.IllegalArgumentException to be thrown, but 
nothing was thrown.
        at 
org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
        at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:73)
        at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:39)
        at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3153)
        at 
org.apache.commons.cli.OptionsTest.testAddOptionGroupShouldThrowExceptionForDuplicateOption(OptionsTest.java:46)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at java.util.ArrayList.forEach(ArrayList.java:1259)
        at java.util.ArrayList.forEach(ArrayList.java:1259) {code}
 

{*}Suggested Fix{*}:

Modify {{addOptionGroup()}} to check for existing options before adding, making 
it consistent with {{{}addOptions(){}}}:
{code:java}
public Options addOptionGroup(final OptionGroup group) {
    // Check for duplicates first - consistent with addOptions() behavior
    for (final Option option : group.getOptions()) {
        if (hasOption(option.getKey())) {
            throw new IllegalArgumentException("Option '" + option.getKey() + 
                "' already exists");
        }
    }
    
    // Existing logic
    if (group.isRequired()) {
        requiredOpts.add(group);
    }
    for (final Option option : group.getOptions()) {
        option.setRequired(false);
        addOption(option);
        optionGroups.put(option.getKey(), group);
    }
    return this;
} {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to