How to build a CLI app in Java using jbang and picocli

August 13, 2020
Written by
Reviewed by

Title card: How to build a CLI app in Java using jbang and picocli

Traditionally Java applications have been used for long-running processes - web application servers can run for days or weeks at a time. The JVM handles this well: Garbage Collection is efficient over huge amounts of memory, and Profile-Guided Optimization can make your code faster the longer it runs.

However, it’s perfectly possible to write short-lived apps too, and in this post I’ll show how to build a CLI app whose total runtime is just a couple of seconds. You can build sophisticated CLI tools in Java for data processing, connection to databases, fetching data from the web, or taking advantage of any of the Java libraries that you're used to.

I’ll use jbang for packaging and running the app, and picocli to handle argument parsing and output.  The app will send an SMS using Twilio’s Messaging API, in a single Java source file less than 100 lines long.

To follow along you will need:

❗ Jbang

Jbang is a one-stop shop for making Java applications that can be run as scripts. It can download the required version of Java, add dependencies and create self-executing source files. It can also run Java code directly from the web, from github repos or even from Twitter (if you can fit your code into 280 characters).

Using SDKMAN! install jbang with: sdk install jbang.

For the impatient: Once you have jbang installed you can skip the rest of this post and run the code directly from GitHub with jbang https://github.com/mjg123/SendSmsJbang/blob/master/SendSms.java - the first time you run it, you will be reminded that running code from URLs is inherently unsafe - you can trust me here though 😉

To create a new script for a CLI app, in an empty directory run:

jbang init --template=cli SendSms.java

This creates a single file, SendSms.java which you can run directly with ./SendSms.java. This file is valid both as Java source and as a shell script, thanks to the first line:

//usr/bin/env jbang "$0" "$@" ; exit $?

This is a neat trick that works as Java since it’s a comment. As a shell script it runs jbang, passing the name of the source file and any arguments.

The second line shows how to add dependencies to a jbang script:

//DEPS info.picocli:picocli:4.2.0

This is the standard GROUP_ID:ARTIFACT_ID:VERSION format for dependencies, used by gradle and other build tools.

The rest of the file is normal Java code which is the “Hello World” for picocli, a library for creating CLI apps in Java.

The last thing to do with jbang is to create a project which works in an IDE. You can’t just open the SendSms.java file because jbang needs to download dependencies and add them to the classpath.

Find out how to launch your IDE from the command line. For me on Linux it’s idea.sh - for you it might be idea or eclipse or code or something else, depending on which IDE you like and how you installed it. The following command will create a project that your IDE can understand, open the IDE and then monitor the project for changes:

jbang edit --live=<YOUR IDE COMMAND> SendSms.java

This command will wait indefinitely, watching for changes to the project. For testing the app as you are working, I find it easiest to have another open terminal in the same directory. Make sure to open this after installing jbang to make sure that it is on your $PATH. Now let's get into the code.

⌨️ Picocli

Picocli is a library for creating CLI apps in Java. The goal for today is to modify the existing SendSms.java so that it actually sends SMS. The arguments to the script will be:

  • the to phone number (probably your cell number)
  • the from number (your Twilio number)
  • the message body can be specified at the end of the command

The phone numbers should always be in E.164 format. Running the script would look like this:

./SendSms.java --to +4477xxxxxx46 --from +1528xxxxx734 Ahoy there 👋

If the message isn't at the end of the command, it will be read from standard in, which means you can use the script in a pipeline like this:

fortune | ./SendSms.java --to +4477xxxxxx46 --from +1528xxxxx734

(fortune is a UNIX command that prints "funny" quotes)

⁉️ Command line Options and Parameters

Picocli distinguishes between options and parameters:

  • Options have names, specified by flags, and can appear in any order: --to and --from are options.
  • Parameters are positional, the words of the message will be parameters if they're given at the end of the command.

Delete the private String greeting and its annotation, and add parameters and options for --to --from and the message:

@CommandLine.Option(
   names = {"-t", "--to"},
   description = "The number you're sending the message @|bold to|@",
   required = true)
private String toPhoneNumber;

@CommandLine.Option(
   names = {"-f", "--from"},
   description = "The number you're sending the message @|bold from|@",
   required = true)
private String fromPhoneNumber;

@Parameters(
   index = "0",
   description = "The message to send",
   arity = "0..*"
)
private String[] message;

[this code on GitHub]

The first two highlighted lines include text surrounded by @|bold … |@ which tells picocli to format the words to and from.

For the message we specify the arity as 0..* which means "zero or more parameters", which will be gathered into an array of Strings. Note that if there are zero then message will be null rather than an empty array.

🚪 About exit codes

Skip over the main method - the action happens in the call method, which returns an Integer used as the script's exit code. An exit code of 0 means "success" and any other number means "failure". This script will exit with 1 for any kind of error. There are a few exit codes with special meanings, but none of those will apply to this script.

📩 Building the message text

If the message is given at the end of the command then it will be in the String[] message. If not, the array will be null and we need to read from Standard Input (stdin). Picocli doesn't have anything for reading from stdin but you can do it using a Scanner.

Remove the code from the call() method and replace it with this:

String wholeMessage;

if (message != null) {
   wholeMessage = String.join(" ", message);

} else {
   var scanner = new Scanner(System.in).useDelimiter("\\A");
   wholeMessage = "";
    if (scanner.hasNext()) {
       wholeMessage = scanner.next();
    }
}

if (wholeMessage.isBlank()){
   printlnAnsi("@|red You need to provide a message somehow|@");
   return 1;
}

[this code on GitHub]

If the message is non-null, join the parts together with spaces, otherwise use a Scanner to read from stdin. Using a delimiter of \\A with a Scanner will read the whole input as a single token - known as the Stupid Scanner Trick since at least 2004.

Finally, if there isn't a message from either source, print an error and exit with a 1.  I added a printlnAnsi method above call() for including formatted output - it looks like this:

private void printlnAnsi(String msg) {
   System.out.println(CommandLine.Help.Ansi.AUTO.string(msg));
}

[this code on GitHub]

📲 Sending the SMS

Below the call() method, add this sendSMS method:

private void sendSMS(String to, String from, String wholeMessage) {

   Twilio.init(
       System.getenv("TWILIO_ACCOUNT_SID"),
       System.getenv("TWILIO_AUTH_TOKEN"));

   Message
       .creator(new PhoneNumber(to), new PhoneNumber(from), wholeMessage)
       .create();
}

[this code on GitHub]

This is all that's needed to send an SMS with Twilio. For it to work we need to add a dependency on the Twilio Java Helper Library, so add this line to the top of the file, underneath the picocli dependency:

//DEPS com.twilio.sdk:twilio:7.54.2

🛠️ Joining it up

The last thing to do is call sendSMS from call. Adding this code a the end of the call method will do just that:

try {
   System.out.print("Sending to ..." + toPhoneNumber + ": ");
   sendSMS(toPhoneNumber, fromPhoneNumber, wholeMessage);
   printlnAnsi("@|green OK|@");

} catch (Exception e) {
   printlnAnsi("@|red FAILED|@");
   printlnAnsi("@|red " + e.getMessage() + "|@");
   return 1;
}

return 0;

[this code on GitHub]

All being well, this will send the SMS. Any exception, such as bad credentials or using a non-existent phone number will put us in the catch block where we print the error in red and exit with a 1.

That's it! You can run the final script with:

./SendSms.java --to <YOUR CELL NUMBER> --from <YOUR TWILIO NUMBER> AHOY-HOY

...and the message will arrive.

🎁 Wrapping up

We've seen that Java isn't just useful for big web applications - it works just fine for short-running CLI applications too, made easier with jbang and picocli.  This was just an example though - Twilio already has a very useful CLI already which can send SMS and do a lot more besides.

Just for fun I did also create a version of the code that would fit in a tweet.

I'd love to hear about the fun (or serious) CLI tools you're building with Java, don't worry if they're more than 280 characters. Find me on Twitter or by email:

🐦 @MaximumGilliard

📧 mgilliard@twilio.com

I can't wait to see what you build!