
Shebang script files with Java and Docker
The implementation of the JEP 330: Launch Single-File Source-Code Programs available from Java 11 allows us to write a script in Java and run it as a shebang file. This is very useful for developers like me, who are not very familiar with scripting with other languages like bash or python.
In this post, I’ll show you how to write a single-file script in Java and run it from the command line. As an extra topic, you’ll learn how to put this file in a Docker image for even easier distribution of your script.
- ‘Launch single-file source-code programs’ in Java 11
- Why am I scripting with Java?
- Writing a script in Java
- Running the Java script
- Dockerize your Java shebang file
- Common objections about using Java over classic scripting
- Conclusions
‘Launch single-file source-code programs’ in Java 11
This new feature introduced in the JDK 11 wants to facilitate running single-file programs in Java without needing the classic two steps: first compile, then run. On top of that, it makes possible to use your script as a shebang file by adding a line like this:
#!/path/to/java --source version
Why am I scripting with Java?
To be honest, I didn’t know about this new feature until late 2019, one year after JDK 11 was released. I didn’t need it before that. In the past, most of the times I’ve needed a quick script I’ve put some bash commands in a .sh
file and that made the trick. It’s also true that I spent a lot of time on StackOverflow looking for help to write these bash scripts.
So, when I was thinking about making a docker image with a script that could be used by CI tools to talk to the Quboo API, I first think of creating some bash code. Then I searched online for Java scripting and found this feature and some articles about it.
The main reason for me to use Java in this scenario was my familiarity with the programming language. I can just code in Java without extra help. And, in this case, my requirements were basically reading some environment variables and executing an HTTP request. All of that is possible in vanilla Java.
Writing a script in Java
The goal of the script
As introduced before, the goal of the script is to facilitate the usage of the Quboo API from the command line. If you’re curious about Quboo, just check its website.
The idea is writing a script that can be executed this way:
quboo release "New backend service 'users' released"
(out)OK. Score increased.
Therefore, we want to invoke our script by calling quboo
from the command line.

The Java file
Below is a representative extract of the Java logic in QubooScriptPlugin.java
. Most of the code is not relevant for the sake of this post, but you can get an idea of what the script does.
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Optional;
import java.util.Scanner;
public class QubooScriptPlugin {
private static final String ENV_ACCESS_KEY = "QUBOO_ACCESS_KEY";
private static final String ENV_SECRET_KEY = "QUBOO_SECRET_KEY";
private static final String ENV_QUBOO_USERNAME = "QUBOO_PLAYER_USERNAME";
private static final String ENV_GITLAB_USERNAME = "GITLAB_USER_LOGIN";
private static final String ENV_CIRCLE_USERNAME = "CIRCLE_USERNAME";
private static final String ENV_QUBOO_UNIQUE_ID = "QUBOO_UNIQUE_ID";
// Pass this variable as 'true' if you want to use always git usernames in Quboo
private static final String ENV_ALWAYS_USE_GIT = "QUBOO_CONFIG_ALWAYS_USE_GIT";
private static final String QUBOO_API_SERVER = "https://api.quboo.io";
public static void main(String[] args) {
// Check if the auth env vars are properly set
final String accessKey = System.getenv(ENV_ACCESS_KEY);
final String secretKey = System.getenv(ENV_SECRET_KEY);
if (accessKey == null || secretKey == null || accessKey.isBlank() || secretKey.isBlank()) {
System.err.println("You have to provide valid Quboo access and secret keys via environment variables " +
ENV_ACCESS_KEY + " and " + ENV_SECRET_KEY);
System.exit(1);
}
// Configuration
final boolean alwaysUseGit = Optional.ofNullable(System.getenv(ENV_ALWAYS_USE_GIT))
.map(Boolean::parseBoolean).orElse(false);
// Detect the player and unique id using the CI tool vars or manual env var
final String genericUsername = System.getenv(ENV_QUBOO_USERNAME);
final String gitlabUsername = System.getenv(ENV_GITLAB_USERNAME);
final String circleUsername = System.getenv(ENV_CIRCLE_USERNAME);
String playerName;
if (alwaysUseGit) { // if set to true, it overrides any other env var
playerName = getLastGitCommitterFromGit();
} else if (genericUsername != null) {
playerName = genericUsername;
} else if (gitlabUsername != null && !gitlabUsername.isBlank()) {
playerName = gitlabUsername;
} else if (circleUsername != null && !circleUsername.isBlank()) {
// In CircleCI it's possible that you get a blank user if the committer is not a CircleCI user as well
playerName = circleUsername;
} else {
playerName = getLastGitCommitterFromGit();
}
String uniqueId;
final String qubooUniqueId = System.getenv(ENV_QUBOO_UNIQUE_ID);
if (qubooUniqueId != null && !qubooUniqueId.isBlank()) {
uniqueId = qubooUniqueId;
} else {
uniqueId = getLastGitHashFromGit();
}
if (playerName == null || playerName.isBlank()) {
System.out.println("The script could not derive the player name from environment variables. Please use " +
ENV_QUBOO_USERNAME + " to specify the player that will get the Quboo score.");
System.exit(1);
}
if (uniqueId == null || uniqueId.isBlank()) {
uniqueId = String.valueOf(System.nanoTime());
System.out.println("WARNING: The script could not get a unique id from environment variable or git. Please use " +
ENV_QUBOO_UNIQUE_ID + ", otherwise you will be duplicating the score in future executions.");
}
// Check if args are properly set
String typeOrScore = null, description = null;
if (args.length == 2) {
typeOrScore = args[0];
description = args[1];
} else {
System.out.println("Wrong parameters. Usage: \n\n" +
"quboo [release|doc|(numeric score)] \"description\"\n\n" +
"Examples: \n" +
" - quboo release \"Backend release\"\n" +
" - quboo 50 \"Helping a buddy\"");
System.exit(1);
}
// The type of score determines the endpoint to call
final String endpoint;
boolean isScore = false;
if (typeOrScore.equalsIgnoreCase("release")) {
endpoint = QUBOO_API_SERVER + "/score/release";
} else if (typeOrScore.equalsIgnoreCase("doc")) {
endpoint = QUBOO_API_SERVER + "/score/documentation";
} else {
endpoint = QUBOO_API_SERVER + "/score/generic";
isScore = true;
}
// Calls Quboo Server
final HttpClient http = HttpClient.newHttpClient();
final HttpRequest request = HttpRequest.newBuilder().header("x-quboo-access-key", accessKey)
.header("x-quboo-secret-key", secretKey)
.header("Content-Type", "application/json")
.uri(URI.create(endpoint))
.PUT(HttpRequest.BodyPublishers.ofString(
"{ \"playerLogin\":\"" + playerName + "\"," +
" \"uniqueId\":\"" + uniqueId + "\"," +
" \"description\":\"" + description + "\"," +
" \"score\":\"" + (isScore ? typeOrScore : 1) + "\"" + // score is ignored if not generic
"}"
)).build();
try {
System.out.println("Sending request to " + request.uri());
final HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
System.out.println("Score added succesfully. Response: " + response.body());
} else if (response.statusCode() == 403) {
System.err.println("Forbidden access to Quboo server. Please verify your access and secret keys.");
} else {
System.err.println("Got error response from server (" + response.statusCode() + "). Body: " + response.body());
System.exit(1);
}
} catch (final Exception e) {
System.err.println("Could not send score to Quboo. Please check your connectivity and also proper usage of CLI.");
System.err.println("Reason: " + e.getMessage());
System.exit(1);
}
}
// [...] Some other auxiliary methods
}
There are only two important requirements that you have to fulfill if you want to execute this as a single-file script in Java:
- All the logic has to be in a single file.
- It needs a
public static void main(String[] args)
method.
As you may have guessed, you can’t use any extra libraries or frameworks. Just plain Java.
Running the Java script
Using the java command
The most boring way of running this script is using java from the command line:
java --source 13 src/QubooScriptPlugin.java \
(out)> release "An example release"
(out)You have to provide valid Quboo access and secret keys via environment
(out)variables QUBOO_ACCESS_KEY and QUBOO_SECRET_KEY
Note that you have to use the --source
parameter to run this code in script mode, without needing to compile it separately.
Convert the java file into a shebang file
Let’s make it more interesting. Edit your Java script file and add a line like the first one in this fragment (the path to the java executable can be different in your case):
#!/usr/java/openjdk-13/bin/java --source 13
import ...
public class QubooScriptPlugin {
// ...
}
This makes the Java file a shebang file, and now we can run it without specifying the path to the Java executable. If you’re running on a Windows machine this won’t work for you, but keep reading.
We also need to make the file executable and we can also get rid of the .java
extension.
chmod +x QubooScriptPlugin.java
mv QubooScriptPlugin.java quboo
And now we can run our Java shebang file just by using the quboo
command.
./quboo release "An example release"
Additionally, we could add this quboo
command to one of the default folders for executables in our system. This way we can run our new command from any folder.
cp quboo /usr/local/bin
quboo release "An example release"
This is how you can use a Java script file as a command in your system, using the feature JEP 330: Launch Single-File Source-Code Programs. But, given that the distribution of the script might be a bit tricky, the next section goes about using Docker to make it simpler.
Hey, my IDE went bananas!
At the time of writing this post, IntelliJ IDEA is not able to parse properly the shebang line when you add it to the Java file. This makes coding your script quite inconvenient since you lose a lot of the nice features like the code assistant. Not to mention the screaming red marks from the linter.
This is an extra reason to keep your script as a Java file and post-process it to package it as a shebang file. In my case, I’m wrapping everything together as a Docker image.

Dockerize your Java shebang file
If you already had a look at the Quboo CLI Github repository, you noticed that there is a classic Java file there and a Dockerfile
that transforms it when packaging it as a Docker image.
This approach is useful for several reasons:
- The environment is already setup. The script comes together with the JDK version needed and you know in advance where the java binaries are located. I used as a base image the official openjdk:13-slim Docker image.
- You can keep your script as a Java file to make it easier to edit in your preferred IDE. The Dockerfile commands will take care of transforming it into a shebang file.
FROM openjdk:13-slim
RUN apt-get update && apt-get install -y git
COPY src/QubooScriptPlugin.java /usr/src/
WORKDIR /usr/src/
RUN { echo -n '#!/usr/java/openjdk-13/bin/java --source 13 \n'; cat QubooScriptPlugin.java; } > quboo && \
chmod +x quboo && \
mv quboo /usr/local/bin
As you can see, the last RUN
command takes care of:
- Assembling the pieces by generating a new file with the
#!
line plus the Java sources, into a new file namedquboo
. - Making the
quboo
file executable. - Moving it to one of the folders in the path, so this command can be called from any folder.
With that Dockerfile
and our Java script in the src
folder, we can build the image and run it as follows:
docker build -t quboo-script:1.0 .
(out)...
(out)Successfully tagged quboo-script:1.0
docker run -it quboo-script:1.0 quboo release "An example release"
Common objections about using Java over classic scripting
If we compare the Java language with Bash or Python, it’s fair to say that Java is more verbose. You could reproduce the same functionality shown here in one of those languages using fewer lines of code (even after optimizing some parts). However, if your code has to be read and maintain mainly by Java developers, you shouldn’t care about lines of code.
Another negative point is that you need to have the JDK installed to run a Java script. Bash scripts can be executed in practically any UNIX-based system. On the other hand, if you’re running these scripts in a Docker container and you don’t mind adding JDK to your image, this becomes less of a problem.
An important issue for some people is that you’re limited to the modules included in the JDK. You can’t use any external libraries in your script. Vanilla Java is getting more powerful with every JDK release but many will miss some Apache Commons libraries, just to give an example.
Conclusions
In this post, we covered a real-life example of a shebang file using Java and its Single-File Source-Code Programs feature. We also built it as a Docker image for easier distribution.
Right now, it’s hard to find real-life examples of these scripts. Over time, scripting with Java may become more popular because of the trend in the IT industry to go more and more towards DevOps. Java developers can leverage their expertise and use simple Java scripts around CI/CD tools, inside Docker images, etc.

I hope you found this post useful. If you did, please star the Github repo. You may as well consider:
- Try Quboo, the tool behind the script shown in this post. It’s a gamification platform that helps IT organizations to encourage people to participate in processes that can be boring.
- Get one of my e-books: Practical Software Architecture or Full Reactive Stack with Spring Boot and Angular.
Comments