So much of Java development in the enterprise often seems to be focused around web applications and other aspects of JEE. Sometimes it is almost comical to watch another developer who has typically been focused on this type of work try to develop a stand-alone Java program. One of the challenges faced here is typically proper acceptance, handling, and validation of command-line arguments.
Fortunately, Apache Commmons CLI exists to help with this effort. The summary from their home page:
The Apache Commons CLI library provides an API for parsing command line options passed to programs. It's also able to print help messages detailing the options available for a command line tool.
Commons CLI supports different types of options:
- POSIX like options (ie.
tar -zxvf foo.tar.gz
) - GNU like long options (ie.
du --human-readable --max-depth=1
) - Java like properties (ie.
java -Djava.awt.headless=true -Djava.net.useSystemProxies=true Foo
) - Short options with value attached (ie.
gcc -O2 foo.c
) - long options with single hyphen (ie.
ant -projecthelp
)
If anything, I feel that the Apache Commons CLI project is selling themselves short. I've found it to be a very comprehensive, well-designed library for effectively parsing the command-line. The only shortcoming I've observed is that the project was developed before Java 5 - and annotations - were available. As such, the library doesn't offer support for any features that annotations have to offer.
Introducing the latest addition to MarkUtils: MarkUtils-CLI is a library that provides an effective bridge between Apache Commons CLI and Java annotations - without replacing the mature Commons CLI library. Originally developed in 2013 with Commons CLI 1.2, the stability of and between both libraries across multiple releases has been a proven success. Like all of the MarkUtils libraries, there are minimal project dependencies: just commons-cli and slf4j-api.
Of the three stages to command line processing, MarkUtils-CLI should most commonly be used to:
- Replace the "Definition Stage" - wrapping it with functionality driven by annotations.
- Leave the "Parsing Stage" intact - while also offering a convenient wrapper to help "glue" everything together into a seamless process.
- Wrap the "Interrogation Stage" - automatically setting the annotated fields or calling the annotated methods as configured with the values received from the command line.
classParser
As with most languages implementing a "main
" method, all command-line arguments coming into a Java program are received simply as Strings.
Apache Commons CLI doesn't provide any assistance with parsing or converting these String values to the other data types that may be expected or required by the program being developed.
MarkUtils-CLI contains a child "classParser" library to effectively bridging this gap - automatically converting one or more String values to the value type being expected, including:
- All primitive types:
boolean
,char
,byte
,short
,int
,long
,float
, anddouble
. -
Number
Object types:Byte
,Short
,Integer
,Long
,Float
,Double
,BigInteger
,BigDecimal
. - Direct assignment to the abstract
Number
type - usingBigDecimal
unless a number with a radix != 10 is specified. - Support for alternate number bases for all integral types listed above:
- Hexadecimal: Prefixes
0x
,0X
,0h
, or0H
. - Octal: Prefix
0o
.
- Hexadecimal: Prefixes
Boolean
Character
String
andCharSequence
java.io.File
java.net.URI
java.net.URL
java.util.UUID
- Identifiers to
java.nio.charset.Charset
- Identifiers to
java.util.TimeZone
Enum
types, using the enum type's named identifiers.Class
(usingforName
)Object
(using default instantiation of the passed-in class name, similar toClass
above - 0-parameter or default constructor required.)java.net.InetAddress
andjava.net.InetSocketAddress
, and their respective subtypes - such asjava.net.Inet4Address
andjava.net.Inet6Address
.InetSocketAddress
es are expected in the form of<InetSocketAddress>:<int port>
. For example: "127.0.0.1:80
" or "::1:80
".
Boolean (either to the primitive or Object forms) conversion builds upon the default Boolean.valueOf
logic, which evaluates to true
if and only if the value is case-insensitive equals to "true
".
The conversion accepts the following values:
true
:Y
,YES
,T
,TRUE
,1
,-1
,X
false
:N
,NO
,F
,FALSE
,0
- Any other values will result in a detailed
ClassParseException
.
The parsers are split into a few different groups, with each class implementing IClassParser
.
ClassParserChain
simply chains together these groups, and also implements IClassParser
itself.
Each parser may either return a definitive result (including null
), throw an Exception which will bubble-up to the caller, or return IClassParser.NO_RESULT
to defer processing to a later parser in the chain.
UnhandledParser
is a parser that always throws a detailed "Unhandled type" ClassParseException
.
ClassParserChain
defines a default chain that can be obtained by calling getDefault()
, and contains all available parsers within the library (those listed above) - ending with UnhandledParser
.
If something in the default parse chain doesn't work for you (or if you simply don't like it) - simply create and use your own chain. For simplicity, the default chain could be obtained and prepended to in order to add a custom parser that takes precedence over all the following (default) parsers - or appended in order to handle any conversions that aren't already handled by the former (default) parsers.
Nothing in the "classParser
" package is tied to any of the CLI logic - either that of Commons CLI, or the CLI-specific logic within MarkUtils-CLI.
As such, this code may be appropriately reused for other purposes beyond parsing of command lines.
(If appropriate, this code may be factored into a separate project outside of MarkUtils-CLI, which would then become a dependency of MarkUtils-CLI.)
cli
Now that we have a comprehensive set of code to handle the String-to-type conversions, it's time to put it to work.
The Parameter
annotation can be assigned to any field or method - and provides attributes that map to most of the available options in the Commons CLI Option
class.
If a parameter name is not specifically defined, the base field/method name is used by default - following standard JavaBean conventions.
Additional ParameterGroup
and ParameterGroups
annotations are used to work with the Commons CLI OptionGroup
functionality.
To eliminate needing to deal with security manager concerns, all fields or methods must be declared public
to be accessed by this library.
Proper design of the application code - including using separate "plain-old Java objects" ("POJOs") for containing the mapped command-line arguments - should eliminate most concerns with this.
Mappings support 1 or more argument values, acceptable by a suitable Array type as either a field, or an equivalent set
method - including allowing for varargs.
If the annotated class wishes to add any special processing - such as additional validations, or storing multiple values in a Collection type instead of an Array - simply annotate an appropriate set
method instead of a field, and implement accordingly.
Most of the actual "magic" happens within ClassOptions
.
To allow for future extensibility and customization, all methods here are non-static, so an instance of this class must be created before use (using the default constructor).
General usage is as follows:
ClassOptionsData get(Class<?>)
-
Pass-in the type of the "config" class, containing the
@Parameter
annotations. The returnedClassOptionsData
instance contains mappings from each Commons CLIOption
to the reflectionMember
for efficiency - especially if multiple executions are to be evaluated from within the same program (sure, this may be rare - but it is accounted for). Use of this composite class also allows for future, internal extensions - hopefully without breaking the public API. - This essentially wraps the Commons CLI "Definition Stage".
-
Pass-in the type of the "config" class, containing the
Options getOptions(ClassOptionsData cod)
-
Using the
ClassOptionsData
obtained above, returns a fresh Commons CLIOptions
instance - for use by the Commons CLICommandLineParser
- completing the Commons CLI "Parsing Stage".
-
Using the
void autoMap(Object target, ClassOptionsData, CommandLine, IClassParser)
-
Given an instance of the actual "config" class, along with the
ClassOptionsData
, a Commons CLICommandLine
, and anIClassParser
- will read the values from the parsed command-line and automatically map the values to the annotated fields / methods in the "config" target. - This completed the Commons CLI "Interrogation Stage" - but besides simply providing the necessary details from the command-line, this should hopefully eliminate most of the "boilerplate" coding that would have to be done otherwise.
-
Given an instance of the actual "config" class, along with the
CliRunner
As part of the cli
package, the CliRunner
class aims to further simplify the above.
While the separate calls to the various ClassOptions
methods provide for almost unbounded flexibility, and may still be required in some cases, CliRunner
should help further eliminate any remaining "boilerplate" code.
Click here to view the CliRunner
Javadoc.
All that is required is instantiation using one of the constructors, and calling its run
method.
As such, the complete code necessary to handle command-line processing - using both Apache Commons CLI and MarkUtils-CLI - can be as simple as:
package com.ziesemer.utils.cli; import java.util.concurrent.Callable; public class CliRunnerTestConfigTarget implements Callable<Integer>{ @Parameter public int testReturn; public Integer call() throws Exception{ return testReturn; } }
package com.ziesemer.utils.cli.examples; import org.apache.commons.cli.PosixParser; import com.ziesemer.utils.cli.CliRunner; import com.ziesemer.utils.cli.CliRunnerTestConfigTarget; public class CliRunnerExample{ public static void main(String[] args){ CliRunner<CliRunnerTestConfigTarget> runner = new CliRunner<CliRunnerTestConfigTarget>( new PosixParser(), CliRunnerTestConfigTarget.class); CliRunnerTestConfigTarget configTarget = new CliRunnerTestConfigTarget(); runner.run(configTarget, configTarget, args); } }
Note that here, "configTarget
" is being used both as the "configuration" object - as well as the target to be executed.
I.E., CliRunnerTestConfigTarget
has @Parameter
annotations, and implements Callable<Integer>
.
The Integer result returned by the callable is used as the return code / exit status for the program (assuming everything else executes "normally" - further details below).
There are a significant number of getters and setters provided that can be used to customize the operation of CliRunner
.
Any unit of significant processing within CliRunner
is factored into its own method - allowing for sub-classing and overriding as desired / required.
Many of these methods are specifically documented as extension points.
Please view the Javadocs (and possibly the source code) for additional details.
As noted above, an additional advantage of using CliRunner
is that it can handle the return code / exit status of the program without any additional coding.
As noted in the Javadocs, the following codes are used by default (all of which can be overridden):
- If the command line is successfully parsed, the result returned by the target. Otherwise:
1
if the default help was requested.254
if a ParseException is generated.255
for any other exception (considered the "worst").
Note that to properly handle the return code / exit status for most cases, System.exit()
will be called (by default).
Download
com.ziesemer.utils.cli
is available on java.ziesemer.com under the GPL license, complete with source code, a compiled .jar, generated Javadocs, and a suite of 95+ JUnit tests.
Please report any bugs or feature requests on the GitHub Issue Tracker.
No comments:
Post a Comment