Do you want to bring your java applications to your command line? Picocli is a framework that enables you to create your own rich CLI ,backed by java! In this post we will get started with an example CLI called java-genie.

To read more on picocli go to their website with documentation and other guides here.

We will use fish shell to configure an alias for our new command. You can read more about it in this previous post on fish shell. The complete code for this post can be found on github here.

Setup

The goal of our CLI will be to add a specific java class to the project you are working on. I find myself copy- and pasting this class all the time when I’m building gRPC services. So let’s try and automate this!

Create an empty maven project for our CLI and add the picocli dependency.

<dependency>
  <groupId>info.picocli</groupId>
  <artifactId>picocli</artifactId>
  <version>${picocli.version}</version>
</dependency>

Also add the annotation processor to the maven-compiler-plugin.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>${maven-compiler-plugin.version}</version>
  <configuration>
    <release>${jdk.version}</release>
    <encoding>UTF-8</encoding>
    <annotationProcessorPaths>
      <path>
        <groupId>info.picocli</groupId>
        <artifactId>picocli-codegen</artifactId>
        <version>${picocli.version}</version>
      </path>
    </annotationProcessorPaths>
  </configuration>
</plugin>

The project is ready. In the end we would like to execute something like this in the project we are working on.

<our command> grpc insert-introspection-bean

It describes what we want to do and gives it a context with the first argument. This grouping of commands can be done with subcommand as we will see later.

Your first command

As a first step we will define the grpc parent command and let it just print a message for now.

import picocli.CommandLine;

@CommandLine.Command(
        name = "grpc",
        description = "Says hello for now"
)
public class GrpcCommand implements Runnable {

    @Override
    public void run() {
        System.out.println("Hello from gRPC command");
    }

    public static void main(String[] args) {
        CommandLine.run(new GrpcCommand(), args);
    }
}

Note that the class implements the Runnable interface and the actual implementation of the command is done in the overridden run method.

To see this in action we have to build our cli application.

./mvnw clean package

This produces a runnable jar in the target directory, which we can call like so.

run java genie jar command 1024x39 - PICOCLI

And there you go! Your first command line interface executed.

Tips for easy usage

Before we go further with the implementation let us define a quicker way to access this program. We are using fish shell and are going to define an alias in the fish config (~/.config/fish/config.fish).

alias jg "java -cp '<absolute path>/target/java-genie-0.0.0-blog_picocli-SNAPSHOT.jar' nl.sybrenbolandit.genie.Application"

Note that our command will be jg for java-genie and that we made an container class Application to hold all commands. We can now access our program like this.

java genie command via alias 1024x56 - PICOCLI

That looks better.

Subcommands

We can now continue with the actual implementation. We want to copy a predefined java class to a given location. Here is a first try of the subcommand.

@CommandLine.Command(
    name = "insert-introspection-bean",
    description = "Insert an introspection bean")
public class InsertIntrospectionBean implements Callable<Integer> {

  private static final String FILE_NAME = "ServerBuilderListener.java";

  @CommandLine.Option(
      names = {"-l", "--location"},
      description = "The relative path to the target location",
      defaultValue = ".")
  private Path location;

  public static void main(String... args) {
    int exitCode = new CommandLine(new InsertIntrospectionBean()).execute(args);
    System.exit(exitCode);
  }

  @Override
  public Integer call() throws IOException {
    System.out.printf("Insert introspection bean into %s%n", location);

    InputStream sourceFile = fetchSourceFile();
    copyFile(sourceFile);

    return 0;
  }

  private InputStream fetchSourceFile() {
    return Optional.ofNullable(
            InsertIntrospectionBean.class.getResourceAsStream("/grpc/" + FILE_NAME))
        .orElseThrow(() -> new IllegalStateException("Source file not found!"));
  }

  private void copyFile(InputStream source) throws IOException {
    Files.copy(source, location.resolve(FILE_NAME), StandardCopyOption.REPLACE_EXISTING);
  }
}

Note that we can also implement Callable<Integer> to run a command and pass the exit code of the program like here.

We see that the source file is fetched from the resources folder (the content is not important right now) and copied to the given location. This location has as default value the current location but can be specified from the command line with the -l option. Let’s try it!

unknown subcommand defaults to parent command 1024x109 - PICOCLI

Hmmmm, we have to do something. This defaults to the main command. We have to define the subcommand with the parent command. So in GrpcCommand we do the following.

@CommandLine.Command(
        name = "grpc",
        description = "Says hello for now",
        subcommands = {
                InsertIntrospectionBean.class
        }
)

Now the subcommand will be executed.

Finalizing the project

Ok, we just copied a file. But we still have to update the resulting class with project specific things. To eliminate this we will substitute the package name into our source file. To specify it we will need another option.

@CommandLine.Option(
        names = {"-p", "--package"},
        description = "The package of the new class",
        defaultValue = "nl.sybrenbolandit.grpc.config")
private String packageName;

The we mark the location in the source for injection. (Here look at the first line of /resources/grpc/ServerBuilderListener.java)

package @packageName@;

import io.grpc.ServerBuilder;
import io.grpc.protobuf.services.ProtoReflectionService;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import javax.inject.Singleton;

@Singleton
public class ServerBuilderListener implements BeanCreatedEventListener<ServerBuilder<?>> {

	@Override
	public ServerBuilder<?> onCreated(BeanCreatedEvent<ServerBuilder<?>> event) {
		final ServerBuilder<?> builder = event.getBean();
		builder.addService(ProtoReflectionService.newInstance());
		return builder;
	}
}

Now lets replace the package name before we copy the file.

private InputStream setPackage(String source) {
  String evaluatedPackageName =
      Optional.ofNullable(packageName).orElse("");

  String newSource = source.replace("@packageName@", evaluatedPackageName);

  return new ByteArrayInputStream(newSource.getBytes(StandardCharsets.UTF_8));
}

Now our command is ready to be used in every project without changing this code.

final command insert introspection bean with parameters 1024x46 - PICOCLI

(As a bonus you can look at the complete code here to see how we could also evaluate the package name from the file location. We assume executing from the root directory of a standard maven project.)

Hopefully you are now able to build your own commands to make your developer-life a bit easier. Happy commanding!