Classes in this module allow to declaratively construct command line interfaces. It uses picocli to execute commands and capability framework to collect sub-commands and mix-ins. This way command line interfaces can be constructed top-down (default picocli functionality) - parent commands explicitly define sub-commands, and bottom-up - sub-commands are added to parent commands by the framework. Top-down construction can be done using out-the-box picocli capabilities - programmatic add and annotations. Both top-down and bottom-up construction can be done using the capability framework which allows sub-commands/mix-ins to request capabilities they need and add themselves to parent commands only if all requirements are met.
The module provides a capability to build polymorphic CLI’s - sub-commands and mix-ins may override other sub-commands and mix-ins with the same name. This is similar to method overriding in Object-Oriented languages like Java. For example, a base CLI package may have a basic implementation of some sub-command. A derived package would add dependencies with advanced sub-commands to pom.xml
. These sub-commands would replace (override) basic sub-commands during construction of the command hierarchy.
In addition to the picocli way of adding sub-commands programmatically and using @Command
annotation subcommands
element this module provides a few more ways to contribute sub-commands which are explained below.
In all cases create a sub-class of SubCommandCapabilityFactory
and implement/override the following methods:
getCommandType
- used for declarative matchingcreateCommand
for imperative (programmatic) matchingdoCreateCommand
:
@SubCommands
or @Parent
match()
as well.Add to module-info.java
:
provides org.nasdanika.capability.CapabilityFactory with <factory class>
opens <sub-command package name> to info.picocli, org.nasdanika.html.model.app.gen.cli;
Opening to org.nasdanika.html.model.app.gen.cli
is needed if you want to generate extended documentation (see below).
This one is similar to @Command.subcommands
- the parent command declares types of sub-commands. However:
SubCommandCapabilityFactory
’s.HelpCommand
interface or base class and all commands implementing/extending this class will be added to the parent command. If there are two commands with the same name one of them might override the other as explained below.In this case the sub-command or mix-in class are annotated with @Parent
annotation listing types of parents. The sub-command/mix-in will be added to all commands in the hierarchy which are instances of the specified parent types - exact class, interface implementation, or sub-class.
The above two ways of matching parent commands and sub-commands are handled by the SubCommandCapabilityFactory.match()
method. You may override this method or createCommand()
method to programmatically match parent path and decide whether to contribute a sub-command or not.
Similar to sub-commands, mix-ins can be contributed top-down and bottom-up - declaratively using annotations and programmatically.
In all cased create s sub-class of MixInCapabilityFactory
, implement/override:
getMixInType()
- for declarative matchinggetName()
createMixIn()
for imperative matching, ordoCreateMixIn()
@MixIns
or @Parent
match()
as well.Add to module-info.java
:
provides org.nasdanika.capability.CapabilityFactory with <factory class>
opens <mix-in package name> to info.picocli;
MixInCapabilityFactory
’s.See “@Parent annotation” sub-section in “Contributing sub-commands” section above.
The above two ways of matching parent commands and sub-commands/mix-ins are handled by the MixInCapabilityFactory.match()
method. You may override this method or createMixIn()
method to programmatically match parent path and decide whether to contribute a mix-in or not.
A command/mix-in overrides another command/mix-in if:
Overrider
interface and returns true
from overrides(Object other)
method.@Overrides
and the other command is an instance of one of the value classes.You may annotate commands with @Description
to provide additional information in generated HTML site.
The CLI module provides several base command classes:
CommandBase
- base class with standard help mix-inCommandGroup
- base class for commands which don’t have own functionality, only sub-commandsContextCommand
- command with options to configure ContextDelegatingCommand
- options to configure Context and ProgressMonitor and delegate execution to SupplierFactoryHelpCommand
- outputs usage for the command hierarchy in text, html, action model, or generates a documentation siteThe module also provides several mix-ins:
ContextMixIn
- creates and configures ContextProgressMonitorMixIn
- creates and configures ProgressMonitorResourceSetMixIn
- creates and configures ResourceSet using CapabilityLoader to add packages, resource and adapter factories, …ShellCommand can be used to execute multiple commands in the same JVM.
Commands implementing org.nasdanika.common.Closeable, including CommandBase and its subclasses are closed recursively. This functionality can be used to release resources or save state to the permanent storage, e.g. file system.
A distribution is a collection of modules contributing commands and mix-ins plus launcher scripts for different operating systems. org.nasdanika.cli
and org.nasdanika.launcher
modules are examples of building distributions as part of a Maven build. Building a distribution involves the following steps:
All of the above steps are executed by mvn verify
or mvn clean verify
Dependencies can be downloaded using Maven dependency plug-in:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<dependencies>
...
</dependencies>
<build>
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/dist/lib
</outputDirectory>
<useRepositoryLayout>true</useRepositoryLayout>
</configuration>
</execution>
</executions>
</plugin>
...
</build>
...
</project>
Launcher scripts can be generated using launcher
command. The command can be issued manually from the command line.
Alternatively, you can execute the launcher command from an integration test as shown below:
public class BuildDistributionIT {
@Test
public void generateLauncher() throws IOException {
for (File tf: new File("target").listFiles()) {
if (tf.getName().endsWith(".jar") && !tf.getName().endsWith("-sources.jar") && !tf.getName().endsWith("-javadoc.jar")) {
Files.copy(
tf.toPath(),
new File(new File("target/dist/lib"), tf.getName()).toPath(),
StandardCopyOption.REPLACE_EXISTING);
}
}
ModuleLayer layer = Application.class.getModule().getLayer();
try (Writer writer = new FileWriter(new File("target/dist/modules"))) {
for (String name: layer.modules().stream().map(Module::getName).sorted().toList()) {
writer.write(name);
writer.write(System.lineSeparator());
};
}
CommandLine launcherCommandLine = new CommandLine(new LauncherCommand());
launcherCommandLine.execute(
"-b", "target/dist",
"-M", "target/dist/modules",
"-f", "options",
"-j", "@java",
"-o", "nsd.bat");
launcherCommandLine.execute(
"-b", "target/dist",
"-M", "target/dist/modules",
"-j", "#!/bin/bash\n\njava",
"-o", "nsd",
"-p", ":",
"-a", "$@");
}
}
If the Maven project which builds the distribution does not contribute its own code, then the for
loop copying the jar file can be omitted.
Create an assembly file dist.xml
similar to the one below in src\assembly
directory:
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>dist</id>
<formats>
<format>tar.gz</format>
<format>tar.bz2</format>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.build.directory}/dist</directory>
<outputDirectory>/</outputDirectory>
<useDefaultExcludes>false</useDefaultExcludes>
</fileSet>
</fileSets>
</assembly>
then add the following plugin definition to pom.xml
:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<formats>zip</formats>
<appendAssemblyId>false</appendAssemblyId>
<finalName>nsd-cli-${project.version}</finalName>
<descriptors>
<descriptor>src/assembly/dist.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>create-archive</id>
<phase>verify</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
Change the final name to your CLI name. E.g. my-company-cli
.