DEV Community

onebitwonder
onebitwonder

Posted on

Building CLIAR — A simple drop-in Java class for parsing command-line arguments (Part 3)

Welcome back to the third part of my rolling blog on building CLIAR, a lightweight, transparent command‑line argument parser for Java.
In Part 2, we added support for short options, long options, and key–value pairs.
This chapter focuses on two major improvements:

  • separating concerns inside the parser
  • introducing a formal option‑registration system so CLIAR can validate user input

This is the point where CLIAR becomes a real utility rather than a loose collection of parsing rules.
You can find current snapshots of the code on my GitHub page.

Defining rules for option names

Before we can validate user input, we need clear rules for declared option names — the names the developer defines in code.
To keep CLIAR predictable and easy to read:

  • Short option names must be single alphabetic characters.
  • Long option names must start with a letter and may contain letters, digits, or hyphens.
  • Values and positional arguments remain unrestricted. These rules apply only to declared options, not to the raw command‑line arguments.

With naming rules established, we can now look at how CLIAR processes the actual arguments supplied by the user.

Parsing command‑line arguments with dedicated methods

CLIAR does not parse option names directly. It parses arguments, then maps them to declared options.
To keep the logic clean, we delegate parsing to two methods:

  • parseShortOption(String arg, Cliar cliar)
  • parseLongOption(String arg, Cliar cliar)

The main loop becomes:

for (String arg : args) {
    if (arg.startsWith("--") && arg.length() > 2) {
        parseLongOption(arg.substring(2), cliar);
    } else if (arg.startsWith("-") && !arg.startsWith("--") && arg.length() > 1) {
        // NOTE: "-" becomes a positional argument
        parseShortOption(arg.substring(1), cliar);
    } else {
        cliar.positionalArguments.add(arg);
    }
}
Enter fullscreen mode Exit fullscreen mode

Parsing Short Options

Short options are simple: each character is a boolean flag.

for (char chr : arg.toCharArray()) {
    if (Character.isLetter(chr)) {
        // ...
    } else {
        throw new IllegalArgumentException(
            String.format("Invalid option '%c' in argument %s.", chr, arg)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

CLIAR treats grouped flags (-abc) as three separate options.

Parsing Long Options

Long options may include a value:

  • --verbose
  • --output=file.txt

We split the argument:

int index = arg.indexOf('=');
String key = arg;
String val = null;

if (index != -1) {
    key = arg.substring(0, index);
    val = arg.substring(index + 1);
}
Enter fullscreen mode Exit fullscreen mode

Then validate the key:

if (key.isEmpty()) {
    throw new IllegalArgumentException("Long option name is empty.");
}

if (!Character.isLetter(key.charAt(0))) {
    throw new IllegalArgumentException("Long option must start with a letter.");
}

for (int i = 1; i < key.length(); i++) {
    char chr = key.charAt(i);

    if (!(Character.isLetter(chr) || Character.isDigit(chr) || chr == '-')) {
        throw new IllegalArgumentException(
            String.format("Illegal character %c in option %s at position %d.", chr, key, i)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Declaring Options with the Option Class

CLIAR can now parse arguments, but it still doesn’t know which options are allowed.
This information must be declared in code.

We encapsulate a declared option in an inner class:

public static class Option {

    public Option(String shortOption, String longOption, String description,
                  boolean required, boolean expectsValue) { }

    public String getShortOption() { }
    public boolean hasShortOption() { }

    public String getLongOption() { }
    public boolean hasLongOption() { }

    public String getDescription() { }

    public boolean isRequired() { }
    public boolean expectsValue() { }
}
Enter fullscreen mode Exit fullscreen mode

Constructor expectations:

  • At least one of shortOption or longOption must be provided.
  • Short options must be a single character.
  • Short options cannot expect values.
  • Long options may expect values.

Validation:

if ((shortOption == null || shortOption.isBlank()) &&
    (longOption == null || longOption.isBlank())) {
    throw new IllegalArgumentException("Option requires short or long name.");
}

if (shortOption != null && shortOption.length() != 1) {
    throw new IllegalArgumentException("Short option must be a single character.");
}

if (shortOption != null && expectsValue) {
    throw new IllegalArgumentException("Short options cannot expect values.");
}
Enter fullscreen mode Exit fullscreen mode

Registering Options

To track declared options and parsed arguments, CLIAR maintains four collections:

private final Map<String, Option> shortOptions = new HashMap<>();
private final Map<String, Option> longOptions  = new HashMap<>();
private final Map<Option, String> parsedOptions = new HashMap<>();
private final List<String> positionalArguments = new ArrayList<>();
Enter fullscreen mode Exit fullscreen mode

and the factory method now accepts a list of declared options:

Cliar cliar = new Cliar();

for (Option opt : options) {
    if (opt == null) {
        throw new IllegalArgumentException("Option must not be null.");
    }

    if (opt.hasShortOption()) {
        String key = opt.getShortOption();
        if (cliar.shortOptions.containsKey(key)) {
            throw new IllegalArgumentException("Duplicate short option: -" + key);
        }
        cliar.shortOptions.put(key, opt);
    }

    if (opt.hasLongOption()) {
        String key = opt.getLongOption();
        if (cliar.longOptions.containsKey(key)) {
            throw new IllegalArgumentException("Duplicate long option: --" + key);
        }
        cliar.longOptions.put(key, opt);
    }
}
Enter fullscreen mode Exit fullscreen mode

This ensures CLIAR knows exactly which options are valid.

Validating Parsed Options

This is where argument parsing and option validation meet: each parsed argument is checked against the set of declared options, and only valid, non‑duplicate options are accepted.

For short options, each character (or each character in a grouped short option) is looked up in the map of declared short options:

for (char chr : arg.toCharArray()) {
    if (Character.isLetter(chr)) {
        Option opt = cliar.shortOptions.get(String.valueOf(chr));

        if (opt == null) {
            throw new IllegalArgumentException(
                String.format("Invalid option '%c' in argument %s.", chr, arg)
            );
        }

        if (cliar.parsedOptions.containsKey(opt)) {
            throw new IllegalArgumentException("Duplicate short option: -" + opt.getShortOption());
        }

        cliar.parsedOptions.put(opt, null);
    } else {
        throw new IllegalArgumentException(
            String.format("Invalid option '%c' in argument %s.", chr, arg)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

And similarly for long options, where the parser validates the option name, checks whether a value is required, and ensures no duplicates are supplied:

Option opt = cliar.longOptions.get(key);

if (opt == null) {
    throw new IllegalArgumentException(String.format("Invalid option '%s'.", key));
}

if (opt.expectsValue() && (index == -1 || val.isBlank())) {
    throw new IllegalArgumentException(String.format("Option '%s' requires a value.", key));
}

if (cliar.parsedOptions.containsKey(opt)) {
    throw new IllegalArgumentException("Duplicate long option: --" + opt.getLongOption());
}

cliar.parsedOptions.put(opt, val);
Enter fullscreen mode Exit fullscreen mode

Many frustrations come from unspoken expectations

At this stage, CLIAR has verified that all supplied arguments correspond to declared options — but it has not yet checked whether all required options were actually provided. This final validation step happens after parsing and ensures that every option marked as required is present:

for (Option opt : options) {
    if (opt.isRequired() && !cliar.parsedOptions.containsKey(opt)) {
        String name = opt.hasLongOption()
            ? "--" + opt.getLongOption()
            : "-" + opt.getShortOption();

        throw new IllegalArgumentException(
            String.format("Missing required option %s.", name)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

with this, CLIAR contains:

  • all valid parsed options
  • all positional arguments
  • and has rejected any invalid or missing required options

Conclusion

In this chapter, we turned CLIAR into a structured, declarative command‑line parser.
We introduced:

- dedicated parsing methods
- strict naming rules
- the Option class
- option registration
- duplicate detection
- required‑option enforcement
Enter fullscreen mode Exit fullscreen mode

With these foundations in place, CLIAR is now robust and predictable.

In Part 4, we will expose the parsed options to the consumer, refine the API, and address a few remaining edge cases.


This article was written with the help of an LLM for structuring and wording. All technical content reflects my own understanding and decisions.

Top comments (0)