Matthew Gilliard
Posted on August 17, 2020
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:
- SDKMAN! - I wrote about why I love SDKMAN! Before.
- a Twilio account. If you don’t have one already, get one for free at https://twilio.com/try-twilio.
- your Twilio account credentials set as environment variables:
TWILIO_ACCOUNT_SID
andTWILIO_AUTH_TOKEN
.
❗ 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;
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;
}
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));
}
📲 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 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;
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:
I can't wait to see what you build!
Posted on August 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.