refcodes-cli: Parse your args[]

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 actual Group-ID and myapp with your actual Artifact-ID:

mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-cli \
  -DarchetypeVersion=3.3.5 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

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.5</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:

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 the refcodes-cli artifact, such as the creation of arrays from Option arguments by invoking asArray(...).

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.5</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

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.