README
The
REFCODES.ORG
codes represent a group of artifacts consolidating parts of my work in the past years. Several topics are covered which I consider useful for you, programmers, developers and software engineers.
What is this repository for?
This artifact defines some helpful toolkit to parse your command line arguments (as passed to your public static void main( String[] args) { ... }
method. It lets you define the exact valid combinations of command line arguments, it parses them arguments for you and it lets you print the syntax as you programmatically defined it. Let’s get started.
Quick start archetype
For a jump start into developing Java driven command line tools, I created some fully pre-configured Maven Archetypes available on Maven Central. Those Maven Archetypes already provide means to directly create native executables, bundles as well as launchers and support out of the box command line argument parsing as well as out of the box property file handling.
Use the refcodes-archetype-alt-cli
archetype to create a bare metal command line interface (CLI
) driven Java application:
Please adjust
my.corp
with your actualGroup-ID
andmyapp
with your actualArtifact-ID
:
Using the defaults, this will generate a CLI
application providing a command line interface by harnessing the refcodes-cli
library.
How do I get set up?
To get up and running, include the following dependency (without the three dots “…”) in your pom.xml
:
1
2
3
4
5
6
7
8
9
<dependencies>
...
<dependency>
<groupId>org.refcodes</groupId>
<artifactId>refcodes-cli</artifactId>
<version>3.3.8</version>
</dependency>
...
</dependencies>
The artifact is hosted directly at Maven Central. Jump straight to the source codes at Bitbucket. Read the artifact’s javadoc at javadoc.io.
How do I get started?
Consider you have a tool called foobar
to be invoked with the below allowed argument combinations (syntax):
foobar [{ -a | -d }] -f <file>
The foobar
command can be invoked either with an optional -a
or with an optional -d
switch, but not both of them at the same time, and a file -f <file>
must be provided, else the passed arguments are rejected as not being valid.
Valid arguments would be:
foobar -f someFile
foobar -d -f anyFile
foobar -f otherFile -a
foobar -a -f otherFile
Invalid arguments would be:
foobar -f someFile -b
foobar -a someFile -f
foobar -a -d -f anyFile
foobar -a -x -f otherFile
This means that additional switches not supported are not valid. The parser detects such situations and you can print out a help message in such cases.
Construct your parser
First build your syntax using Flags
, Options
and Conditions
. You actually define your command line tool’s supported arguments and how your tool can be invoked (e.g. the valid combination of arguments passed to your tool):
1
2
3
4
5
6
7
8
9
StringOption theFileArg = new StringOption( 'f', "file", "A file" );
Flag theAddFlag = new Flag( 'a', "add", "Add the specified file" );
Flag theDeleteFlag = new Flag( 'd', "delete", "Delete the specified file" );
Condition theXorCondition = new XorCondition( theAddFlag, theDeleteFlag );
Condition theAnyCondition = new AnyCondition( theXorCondition );
Condition theAndCondition = new AndCondition( theAnyCondition, theFileArg );
ArgsParser theArgsParser = new ArgsParser( theAndCondition );
theArgsParser.printUsage();
// theArgsParser.printHelp();
Snippets of interest
Below find some code snippets which demonstrate the various aspects of using the refcodes-cli
artifact (and , if applicable, its offsprings). See also the example source codes of this artifact for further information on the usage of this artifact.
ANSI Escape-Codes
You can start right off by playing around with ANSI Escape-Codes to tweak your ArgsParser
’s appearance:
Default ANSI-Console | Tweaked ANSI-Console | Crazy ANSI-Console |
As a foundation for working with ANSI Escape-Codes the refcodes-data
artifact provides the AnsiEscapeCode
enumeration for easy construction of ANSI Escape-Code
sequences. Below we use the AnsiEscapeCode
enumeration to color our ArgsParser
’s output (the code corresponds to the last screenshot above):
1
2
3
4
5
6
7
...
ArgsParser theArgsParser = new ArgsParser( ... );
theArgsParser.setBannerEscapeCode( AnsiEscapeCode.toEscapeSequence( AnsiEscapeCode.REVERSE_VIDEO ) );
theArgsParser.setBannerBorderEscapeCode( AnsiEscapeCode.toEscapeSequence( AnsiEscapeCode.FG_BRIGHT_CYAN, AnsiEscapeCode.BOLD ) );
theArgsParser.setParameterEscapeCode( AnsiEscapeCode.toEscapeSequence( AnsiEscapeCode.FG_RED, AnsiEscapeCode.UNDERLINE ) );
theArgsParser.setParameterDescriptionEscapeCode( AnsiEscapeCode.toEscapeSequence( AnsiEscapeCode.FG_BRIGHT_YELLOW, AnsiEscapeCode.BOLD ) );
...
Using syntactic sugar
The TinyRestfulServer
demo application uses syntactic sugar
for setting up the command line arguments parser:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import static org.refcodes.cli.CliSugar.*;
...
public static void main( String args[] ) {
...
IntOption theWidth = intOption( 'w', "width", "Sets the console width" );
IntOption thePort = intOption( 'p', "port", "Sets the port for the server" );
IntOption theMaxConns = intOption( 'c', "connections", "Sets the number of max. connections" );
StringOption theUsername = stringOption( 'u', "user", "The username for HTTP basic authentication" );
StringOption theSecret = stringOption( 's', "secret", "The password for HTTP basic authentication" );
Flag theHelp = helpFlag( "Shows this help" );
Condition theArgsSyntax = xor(
and(
thePort, optional( theMaxConns ), optional( and( theUsername, theSecret ) ), optional( theWidth )
),
theHelp
);
ArgsParser theArgsParser = new ArgsParser( theArgsSyntax );
theArgsParser.withSyntaxNotation( SyntaxNotation.REFCODES );
theArgsParser.withName( "TinyRestful" ).withTitle( "TINYRESTFUL" ).withCopyrightNote( "Copyright (c) by FUNCODES.CLUB, Munich, Germany." ).withLicenseNote( "Licensed under GNU General Public License, v3.0 and Apache License, v2.0" );
theArgsParser.withBannerFont( new FontImpl( FontFamily.DIALOG, FontStyle.BOLD, 14 ) ).withBannerFontPalette( AsciiColorPalette.MAX_LEVEL_GRAY.getPalette() );
theArgsParser.setDescription( "Tiny evil RESTful server. TinyRestfulServer makes heavy use of the REFCODES.ORG artifacts found together with the FUNCODES.CLUB sources at <http://bitbucket.org/metacodez>." );
theArgsParser.addExampleUsage( "To use a specific port", thePort );
theArgsParser.addExampleUsage( "To secure with a user's credentials", theUsername, theSecret);
List<? extends Operand<?>> theResult = theArgsParser.evalArgs( args );
...
}
...
Most obvious is the missing new
statement for instantiating the parts of your command line parser as this is done by the statically imported methods.
Flip through fluent methods provided by the
CliSugar
class to find out about further possibilities of therefcodes-cli
artifact, such as the creation of arrays fromOption
arguments by invokingasArray(...)
.
The CLI helper
The CliHelper
class found in the refcodes-archetype
artifact aggregates a bunch of functionality to easily create commands with a nifty command line interface. The CliHelper
is actually harnessed by the refcodes-archetype-alt-cli
archetype and to use it you just need to add this dependency (without the three dots “…”) to your pom.xml
:
1
2
3
4
5
6
7
8
9
<dependencies>
...
<dependency>
<groupId>org.refcodes</groupId>
<artifactId>refcodes-archetype</artifactId>
<version>3.3.8</version>
</dependency>
...
</dependencies>
The usage of the CliHelper
is quite simple, most of the work to be done is providing all the information specific to your syntax:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
...
import static org.refcodes.cli.CliSugar.*;
import org.refcodes.archetype.CliHelper;
import org.refcodes.cli.Term;
import org.refcodes.cli.ConfigOption;
import org.refcodes.cli.Example;
import org.refcodes.cli.Flag;
import org.refcodes.cli.IntOption;
import org.refcodes.cli.StringOption;
import org.refcodes.data.AsciiColorPalette;
import org.refcodes.properties.ext.application.ApplicationProperties;
import org.refcodes.textual.FontFamily;
import org.refcodes.textual.Font;
import org.refcodes.textual.FontStyle;
...
public static void main( String args[] ) throws SecurityException, UnsupportedEncodingException {
...
StringOption theEchoOption = stringOption( 'e', "echo", TEXT_PROPERTY, "Echoes the provided message to the standard out stream." );
ConfigOption theConfigOption = configOption();
Flag theInitFlag = initFlag();
Flag theVerboseFlag = verboseFlag();
Flag theSysInfoFlag = sysInfoFlag();
Flag theHelpFlag = helpFlag();
Flag theDebugFlag = debugFlag();
Term theArgsSyntax = cases(
and( theEchoOption, optional( theConfigOption, theVerboseFlag, theDebugFlag ) ),
and( theInitFlag, optional( theConfigOption, theVerboseFlag, theDebugFlag) ),
xor( theHelpFlag, and( theSysInfoFlag, any ( theVerboseFlag ) ) )
);
Example[] theExamples = examples(
example( "Echo a message", theEchoOption),
example( "Echo a message, be more verbose", theEchoOption, theVerboseFlag ),
example( "Echo a message, print stack trace upon failure", theEchoOption, theDebugFlag ),
example( "Load specific config file", theConfigOption),
example( "Initialize default config file", theInitFlag, theVerboseFlag),
example( "Initialize specific config file", theConfigOption, theInitFlag, theVerboseFlag),
example( "To show the help text", theHelpFlag ),
example( "To print the system info", theSysInfoFlag )
);
CliHelper theCliHelper = CliHelper.builder().
withArgs( args ).
withArgsSyntax( theArgsSyntax ).
withExamples( theExamples ).
withDefaultConfig( "foobar.ini" ).
withResourceLocator( Main.class ).
withName( "foobar" ).
withTitle( ">>> FOOBAR >>>" ).
withDescription( "A minimum REFCODES.ORG enabled command line interface (CLI) application. Get inspired by [https://bitbucket.org/funcodez]." ).
withLicenseNote( "Licensed under GNU General Public License, v3.0 and Apache License, v2.0" ).
withCopyrightNote( "Copyright (c) by CLUB.FUNCODES (see [https://www.funcodes.club])" ).
withBannerFont( new Font( FontFamily.DIALOG, FontStyle.BOLD ) ).
withBannerFontPalette( AsciiColorPalette.MAX_LEVEL_GRAY.getPalette() ).build();
ApplicationProperties theArgsProperties = theCliHelper.getApplicationProperties();
boolean isVerbose = theCliHelper.isVerbose();
boolean isDebug = theArgsProperties.getBoolean( theDebugFlag );
try {
...
}
catch ( Exception e ) {
theCliHelper.printException( e );
System.exit( e.hashCode() % 0xFF );
}
}
First we define the types of argumuments we want to support (lines 18 to 24). Then
we define the valid usage for our argument types (lines 26 to 30) by defining our
root syntax node alongside the constraints in which our argument types relate to
each other. To give the users of our command some help on how to use our command,
we provide a bunch of examples for our command (lines 32 to 41). Next we provide
further information (lines 43 to 55): The command line arguments, (line 44), some
information describing our command and the license being used as well as a copyright
note, a title and further properties configuring the ASCII art
banner. We also provide the name of a configuration file (line 47), which may be
used to provide default values for optional arguments or other configuration properties
(supported formats are .properties
, .ini
, .xml
, .yaml
or .json
).
Finally the CliHelper
is being built (line 55), exiting out in case of wrong usage or the request to show
a --help
text. Your code lands in the try
catch
block, so that upon any
exceptions being thrown, the error is displayed in a conforming manner (e.g. with a
stack trace in case a --debug
flag has been provided). By the way. the --sysinfo
flag causes some system information being printed out.
Syntax schema
To introspect your command line syntax, the Term
type
provides means to generate a CliSchema
object, recursively including all the tree node’s CliSchema
objects. The CliSchema
gives insights on the construction and the current state of all the nodes building up your args syntax. Creating a CliSchema
is a helpful tool to analyze more complex command line syntax constructions:
1
2
3
4
...
Condition theArgsSyntax = cases( and( debugFlag(), verboseFlag() ), xor( helpFlag(), and( sysInfoFlag(), any( verboseFlag() ) ) ) );
System.out.println( theArgsSyntax.toSchema() );
...
The above snippet defines a syntax as of --debug -v | { --help | --sysinfo [ -v ] }
(line 1) and prints out the according CliSchema
(line 2). The result of the printable version of theCliSchema
then looks as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
CasesCondition: {
DESCRIPTION: "Switches (CASE) exactly one matching case and makes sure that a match must(!) consume all provided args.",
TYPE: "org.refcodes.cli.CasesCondition",
AllCondition: {
DESCRIPTION: "All (ALL) arguments passed are to be consumed by the nested syntaxables.",
TYPE: "org.refcodes.cli.AllCondition",
AndCondition: {
DESCRIPTION: "All (AND) nested syntaxables must match from the arguments.",
TYPE: "org.refcodes.cli.AndCondition",
DebugFlag: {
ALIAS: "debug",
DESCRIPTION: "Enables the debug mode with additional (developer readable) informational output.",
LONG_OPTION: "debug",
SHORT_OPTION: null,
TYPE: "org.refcodes.cli.DebugFlag",
VALUE: false
},
VerboseFlag: {
ALIAS: "verbose",
DESCRIPTION: "Enables the verbose mode with additional (human readable) informational output.",
LONG_OPTION: "verbose",
SHORT_OPTION: 'v',
TYPE: "org.refcodes.cli.VerboseFlag",
VALUE: false
}
}
},
AllCondition: {
DESCRIPTION: "All (ALL) arguments passed are to be consumed by the nested syntaxables.",
TYPE: "org.refcodes.cli.AllCondition",
XorCondition: {
DESCRIPTION: "Exactly one (XOR) of the nested syntaxables must match from the arguments.",
TYPE: "org.refcodes.cli.XorCondition",
HelpFlag: {
ALIAS: "help",
DESCRIPTION: "Shows this help.",
LONG_OPTION: "help",
SHORT_OPTION: null,
TYPE: "org.refcodes.cli.HelpFlag",
VALUE: false
},
AndCondition: {
DESCRIPTION: "All (AND) nested syntaxables must match from the arguments.",
TYPE: "org.refcodes.cli.AndCondition",
SysInfoFlag: {
ALIAS: "sysinfo",
DESCRIPTION: "Shows some system information for debugging purposes.",
LONG_OPTION: "sysinfo",
SHORT_OPTION: null,
TYPE: "org.refcodes.cli.SysInfoFlag",
VALUE: false
},
AnyCondition: {
DESCRIPTION: "Any (OPTIONAL) nested syntaxables optionally matches from the arguments.",
TYPE: "org.refcodes.cli.AnyCondition",
VerboseFlag: {
ALIAS: "verbose",
DESCRIPTION: "Enables the verbose mode with additional (human readable) informational output.",
LONG_OPTION: "verbose",
SHORT_OPTION: 'v',
TYPE: "org.refcodes.cli.VerboseFlag",
VALUE: false
}
}
}
}
}
}
As you can see, the text output format uses a
JSON
alike notation for easy further processing.
The AllCondition
nodes (line 4 and line 28) being part of the top CasesCondition
node (line 1) only pass parsing of the command line arguments when all arguments have been consumed (identified and processed) without error by the sub tree of the according AllCondition
.
Under the hood
As seen above, you pass your root Condition
to the ArgsParser
which then prints command line tool’s usage string:
1
2
3
...
theArgsParser.printUsage();
...
Test (invoke) your parser: In real live you would pass your main-method’s args[] array to the parser. Now just for a test-run, pass a java.lang.String array to your parser and let it parse it:
1
2
3
4
5
String[] args = new String[] {
"-f", "someFile", "-d"
};
List<? extends Operand<?>> theResult = theArgsParser.evalArgs( args );
File theConfigFile = new File( theFileArg.getValue() );
Now the leaves of your syntax tree are filled with the argument’s values according to the syntax you have been setting up: Your StringOption
instance alhttps://www.javadoc.io/docains the value “someFile”.
The theResult
contains the parsed arguments for you to use in your business logic.
In case of argument rejection a sub-type of the ArgsSyntaxException
such as UnknownArgsException
, AmbiguousArgsException
, SuperfluousArgsException
, ParseArgsException
points to the cause of the rejection. So you can either catch the ArgsSyntaxException
or (one of) the other sub-exceptions.
Term
The Term
type provides the minimum requirements for traversing the syntax tree and all nodes must implement this type. Subtypes of the Term
type represent the nodes when building a command line arguments syntax tree. This syntax tree is used for defining the command line arguments syntax and for parsing the command line arguments accordingly and correctly.
By providing the Operand
, Option
, Condition
or Flag
extensions of the Term
type as well as their their subclasses, a command line argument syntax tree can be constructed. This syntax tree can be used to create a human readable (verbose) command line arguments syntax and parse an array of command line arguments for determining the Operand
, Flag
or Option
nodes’ values.
Operand
An Operand
represents a value parsed from command line arguments. An Operand
has a state which changes with each invocation of the parseArgs(String[])
method.
It is recommended to put your Operand
instance(s) at the end of your top ArgsSyntax
to enforce it to be the last Syntaxable(s) when parsing the command line arguments - this makes sure that any Options
pick their option arguments so that the Operand
(s) will correctly be left over for parsing command line argument(s); the Operand
will not pick by mistake an Option
argument.
Option
An Option
represents a command line option with the according option’s value. An Option
can be seen as a key / value(s) pair defined in the command line arguments parsed via the parseArgs(String[])
method.
An Option
has a state which changes with each invocation of the parseArgs(String[])
method.
Flag
A Flag
is an Option
with a Boolean
https://www.javadoc.io/docare just set or omitted in the command line arguments with no value provided; former representing a true
status and latter representing a false
status.
Operation
The Operation
is an argument representing a function or a method and is either provided or not provided as of the isEnabled()
method. It must neither be prefixed with -
nor with --
in contrast to the Option
or the Flag
type.
Happy coding
Want more than just the usage text? You can print out each building block of the help text on its own, below is all you can get:
1
2
3
4
...
// theArgsParser.printUsage();
theArgsParser.printHelp();
...
Resources
- See
AndCondition
- See
EnumOption
- See
OrCondition
- See
Flag
- See
FloatOption
- See
IntOption
- See
StringOperand
- See
StringOption
- See
XorCondition
Contribution guidelines
- Report issues
- Finding bugs
- Helping fixing bugs
- Making code and documentation better
- Enhance the code
Who do I talk to?
- Siegfried Steiner (steiner@refcodes.org)
Terms and conditions
The REFCODES.ORG
group of artifacts is published under some open source licenses; covered by the refcodes-licensing
(org.refcodes
group) artifact - evident in each artifact in question as of the pom.xml
dependency included in such artifact.