DEV Community

onebitwonder
onebitwonder

Posted on

Building CLIAR — A simple drop-in Java class for parsing command-line arguments (Final part of 4)

Welcome to the final part of my rolling blog series. In this installment, we’ll add the remaining methods needed to access parsed command‑line arguments, along with a small but useful feature: automatically generating a formatted help message from the declared options.
To wrap things up, I’ll also highlight a few limitations of CLIAR and outline how it could evolve in a future revision.

You can find the previous part here and current snapshots of the code on my GitHub page.

Retrieving option names

Throughout the implementation, CLIAR frequently needs to refer to an option by name. Since an option may define a short name, a long name, or both, it’s helpful to centralize this logic. To avoid repeating the same formatting code everywhere, we add a getName() method to the Option class:

public String getName() {
    if (hasShortOption() && hasLongOption()) {
        return String.format("-%s/--%s", getShortOption(), getLongOption());
    } else if (hasLongOption()) {
        return String.format("--%s", getLongOption());
    } else {
        return String.format("-%s", getShortOption());
    }
}
Enter fullscreen mode Exit fullscreen mode

With this small addition in place, we can finally turn to the part of CLIAR that motivated the entire project in the first place.

User access to option values

So far, CLIAR has accepted options declared at compile time and validated them against the supplied command‑line arguments at runtime. Now it’s time to bridge those two worlds by exposing accessor methods that let users retrieve the parsed values.

To begin with, we add a convenience method that checks whether a particular option was supplied:

public boolean has(Option name) {
    return parsedOptions.containsKey(name);
}
Enter fullscreen mode Exit fullscreen mode

For positional arguments, we provide two methods: one to retrieve the number of parsed positional arguments, and one to access them by index:

public int getNumArguments() {
    return positionalArguments.size();
}

public String getArgument(int index) throws IndexOutOfBoundsException {
    return positionalArguments.get(index);
}
Enter fullscreen mode Exit fullscreen mode

Next, we implement a family of getter methods—one for each primitive type plus String.
Each method accepts a default value (used when the option is optional and not present) and performs a simple validation to ensure the option’s value expectations match the accessor being used:

public boolean getBoolean(Option name, boolean defaultValue) throws IllegalStateException{
    if (name.expectsValue()) {
        throw new IllegalStateException(String.format("Option \'%s\' expects a value and cannot be used as a boolean.", name.getName()));
    }

    String val = parsedOptions.get(name);

    return null == val ? defaultValue : Boolean.parseBoolean(val);
}

public byte getByte(Option name, byte defaultValue) throws IllegalStateException, NumberFormatException {
    if (!name.expectsValue()) {
        throw new IllegalStateException(String.format("Option \'%s\' does not expect a value.", name.getName()));
    }

    String val = parsedOptions.get(name);

    return null == val ? defaultValue : Byte.parseByte(val);
}

// ...
Enter fullscreen mode Exit fullscreen mode

And likewise for:

- short
- int
- long
- float
- double
- String
Enter fullscreen mode Exit fullscreen mode

With these accessors in place, CLIAR is now fully functional.

Helping the hopeless

Since people regularly ignore the RTFM principle, we want CLIAR to be at least somewhat self‑explanatory. That means providing a simple, formatted listing of all declared options along with their descriptions. To keep things straightforward, we won’t implement line‑wrapping for long descriptions; instead, we’ll simply align the short option, long option, and description columns.

public String help() {
    int maxLen = 0;

    for (Option opt : declaredOptions) {
        if (null != opt.getLongOption()) {
            int len = opt.getLongOption().length();
            maxLen = len > maxLen ? len : maxLen;
        }
    }

    StringBuilder helpString = new StringBuilder();

    for (Option opt: declaredOptions) {
        String shortOption = null != opt.getShortOption() ? "-" + opt.getShortOption() : "  ";
        String longOption = null != opt.getLongOption() ? "--" + opt.getLongOption() : "";

        helpString.append(String.format("%s %-" + maxLen + "s %s\n", shortOption, longOption, opt.getDescription()));
    }

    return helpString.toString();
}
Enter fullscreen mode Exit fullscreen mode

Now CLIAR is not only functional but also helpful.

Theory without practice is useless

Below is a short example of how CLIAR can be used inside an application:

public class Main {

    private final Option verbose = new Option("v", "verbose", "Enable verbose output", false, false);

    private final Option color = new Option(null, "color", "Set output color", false, true);

    private final Option input = new Option(null, "input", "Input file", true, true);

    private void init(String[] args) {

        Cliar cliar = null;

        try {
            cliar = Cliar.from(args, new Cliar.Option[] {
                verbose,
                color,
                input
            });

            // ...

            if (cliar.getBoolean(verbose, false)) {
                // ...
            }

        } catch (IllegalArgumentException ex) {
            System.err.println(ex.getMessage());

            if (null != cliar) {
                System.err.println(cliar.help());
            }

            System.exit(-1);
        }
    }

    public static void main(String[] args) {
        Main myApp = new Main();

        myApp.init(args);

        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Instead of declaring fields of primitive types such as private boolean verbose or private int color, each declared Option effectively is such a field. It connects directly to the supplied command‑line arguments without any reflection magic, making CLIAR suitable even for legacy applications or environments running on older JVMs or JDKs.

What could be done better in a future revision

While CLIAR is intentionally minimal, several areas could be improved in a future revision.
Right now, options are represented as individual fields rather than being grouped into a dedicated configuration class, which limits how cleanly they can be passed around. Options also do not carry explicit types; instead, the caller chooses the appropriate getter, which works but leaves room for type‑safe improvements. Positional arguments are stored as raw strings, meaning any further parsing must be done manually. And finally, the generated help text is intentionally simple: it lists the available options, but it does not show the expected command syntax or provide a high‑level summary of the application.

Conclusion

CLIAR comes in at just about 220 lines of real code — small enough to read in one sitting, yet complete enough to use in real applications.
It provides a functional, transparent, dependency‑free command‑line parser that you can drop into almost any Java project.

I hope you enjoyed this series, and I’d be happy to have you along for the next one as well.


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)