CLI

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.


Contributing sub-commands

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 matching
  • createCommand for imperative (programmatic) matching
  • doCreateCommand:
    • Declarative - in combination with @SubCommands or @Parent
    • Imperative - override 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).

@SubCommands annotation

This one is similar to @Command.subcommands - the parent command declares types of sub-commands. However:

  • Sub-commands are collected using the capability framework from SubCommandCapabilityFactory’s.
  • Sub-commands types listed in the annotation are base types - classes or interfaces - not necessarily concrete implementation types. E.g. you may have 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.

@ParentCommands annotation

In this case the sub-command or mix-in class are annotated with @ParentCommands 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, or implement Adaptable and return non-null value from adaptTo(Class) method. This allows to create declarative command pipelines as explained in the Declarative Command Pipelines Medium story.

Programmatic match

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.

Contributing mix-ins

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 matching
  • getName()
  • createMixIn() for imperative matching, or
  • doCreateMixIn()
    • Declarative - in combination with @MixIns or @Parent
    • Imperative - override match() as well.

Add to module-info.java:

  • provides org.nasdanika.capability.CapabilityFactory with <factory class>
  • opens <mix-in package name> to info.picocli;

@MixIns annotation

  • Mix-ins are collected using the capability framework from MixInCapabilityFactory’s.
  • Mix-in types listed in the annotation are base types - classes or interfaces - not necessarily concrete implementation types.

@ParentCommands annotation

See “@ParentCommands annotation” sub-section in “Contributing sub-commands” section above.

Programmatic match

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.

Overriding

A command/mix-in overrides another command/mix-in if:

  • It is a sub-class of that command/mix-in
  • It implements Overrider interface and returns true from overrides(Object other) method.
  • It is annotated with @Overrides and the other command is an instance of one of the value classes.

Extended documentation

You may annotate commands, parameters, and options with @Description to provide additional information in the generated HTML site.

If @Description annotation does not have value or resource attributes set, then documentation resource name is implied from command, parameter, or option as follows:

  • Command - documentation resource name is <class name>.<extension> where <extension> is one of extensions supported by documentation factories. All factories and all extensions are iterated and the first found resource is used to generate documentation. Example: MyCommand.md.
  • Option - documentation resource name is <declaring class name>-opt<option name>.<extension>. All factories, all extensions, and all option names are iterated and the first found resource is used to generate documentation. Example: MyCommand-opt--my-option.md.
  • Parameter - supported only for field parameters. Documentation resource name is <declaring class name>-param-<field name>.<extension>. All factories and all extensions are iterated and the first found resource is used to generate documentation. Example: MyCommand-param--myParam.md.

Commands

The CLI module provides several base command classes:

  • CommandBase - base class with standard help mix-in
  • CommandGroup - base class for commands which don’t have own functionality, only sub-commands
  • ContextCommand - command with options to configure Context
  • DelegatingCommand - options to configure Context and ProgressMonitor and delegate execution to SupplierFactory
  • HelpCommand - outputs usage for the command hierarchy in text, html, action model, or generates a documentation site

Mix-ins

The module also provides several mix-ins:

  • ContextMixIn - creates and configures Context
  • ProgressMonitorMixIn - creates and configures ProgressMonitor
  • ResourceSetMixIn - creates and configures ResourceSet using CapabilityLoader to add packages, resource and adapter factories, …

Shell

ShellCommand can be used to execute multiple commands in the same JVM.

Closing commands

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.

Building distributions

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:

  • Downloading modules (dependencies)
  • Generating launcher scripts
  • Building an assembly (zip)

All of the above steps are executed by mvn verify or mvn clean verify

Downloading dependencies

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>

Generating launcher scripts

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.

Assembly

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.

Commands as grammar

Most CLI frameworks treat commands as a fixed catalog. At build time, the developer enumerates commands and sub-commands; at runtime, the user invokes named commands with known options. The vocabulary is closed. New operations require code changes to the central tool. Picocli - the library most Java CLIs build on, including this one underneath - operates this way by default.

The Nasdanika CLI treats commands grammatically. Commands compose into pipelines the way words compose into sentences. A pipeline like model html-app site reads as a sentence: load the model, qualify it through the html-app renderer, produce a site. Each command consumes the previous command’s output and produces input for the next. The pipeline is the syntax; the typed data flowing through it is the grammar’s payload. Unix pipes operate on the same principle - cat foo | grep bar | sort is grammatical CLI composition - but Unix pipes pass text streams, where Nasdanika pipelines pass typed objects. Same composition, more structural integrity.

The vocabulary grows by federation rather than by enumeration. New commands become available simply by adding their containing modules to the CLI assembly. The capability framework discovers the new commands at startup and exposes them in the grammar without any change to the core CLI. A consumer who adds the AsciiDoc module gets asciidoc commands; a consumer who adds the product management model gets PM-specific commands; a consumer who adds both can compose them. The composition is the unit of expression, not the individual commands.

What this earns is that operations the CLI’s authors did not enumerate become naturally expressible by the consumer who needs them. A user composing model validate html-app site --strict is producing a sentence whose authors did not have to write it as a discrete command; the grammar makes the composition valid, the type-checking on the pipe boundaries makes it safe, and the auto-discovery makes the constituent words available.

Whorf’s observation - that language shapes the way we think, and determines what we can think about - applies to command vocabularies as much as to natural languages. A CLI whose command tree is fixed at build time flattens an expressible space into a finite list of named operations. A CLI whose command vocabulary grows by federation lets users articulate operations the central tool’s authors did not enumerate. The same grammatical move appears in other parts of the Nasdanika stack, including the federated capability model that the product management framework is built on - different layers, same underlying property.

Good parallel — the same Whorfian point lands cleanly on the CLI because the CLI is also a generative grammar over auto-discovered vocabulary, and the auto-discovery is the move that distinguishes it from Picocli’s fixed command tree. One small adjustment from the PM-model framing: capabilities had a clean verb/adjective distinction (functional vs non-functional roles); CLI commands are predominantly verbs (actions) that chain into pipelines, so the framing is more sentence composition than noun-phrase modification. The Whorfian point survives the adjustment; the linguistic vocabulary needs to shift slightly.

Draft section for the CLI home page:

A few specific choices and where you may want to adjust:

The section is written as standalone — a reader who has not seen the PM model can still get the point, with one sentence near the end gesturing at the broader pattern. If the section is positioned alongside other introductory CLI content, that one sentence can be expanded into a paragraph; if the CLI page is its own front door without nearby cross-references, the current sentence is enough.

I kept the Unix-pipes comparison because it lands instantly with the audience and clarifies the distinction (typed vs textual) in a few words. If the comparison feels too elementary for your reader, drop the sentence — the rest of the section stands without it.

The internal cross-reference to the PM model is light (“different layers, same underlying property”) — strong enough that a reader who knows the PM model recognizes the parallel, weak enough that a reader who does not is not distracted. If you want to make the cross-reference explicit, change the line to “The same grammatical move appears in the Nasdanika product management model, where capabilities — rather than commands — compose into expressions of consumer need.”

The Whorf quote is woven into prose rather than placed as an epigraph. The CLI page is a reference document; an epigraph would feel out of place. The same quote in the JVM-languages LinkedIn post is presented as an opening epigraph because that piece is opinion content; the CLI page is documentation, so prose integration fits its register better.

The Picocli mention is direct because Picocli is the underlying library and many readers will recognize the contrast. If you want to avoid naming Picocli specifically — some readers may use Picocli directly elsewhere and feel it is being unfavorably compared — generalize to “most Java CLI frameworks” and let the informed reader infer.

One related thought. This section now exists in three places in your portfolio: the PM model docs (capabilities as grammar), the CLI docs (commands as grammar), and the upcoming Medium piece (Markdown as grammar / internal DSLs). The three together form a thesis — “Nasdanika treats every layer of the stack as a host for grammatical composition over auto-discovered vocabularies.” That thesis is worth being explicit about somewhere — probably the book, possibly a short Medium piece in its own right. The three sections cite each other; the thesis-level statement names what they have in common. When you eventually write that piece, the framing already exists; you would just be making the recursive structure visible. “The grammar of Nasdanika” or “How Nasdanika composes” would be candidate titles.