Article

Native command-line tools in Java

Write command-line tools in Java and build their native executables locally or with GitHub Actions.

Java GitHub
Intermediate
Florian Beaufumé
Florian Beaufumé LinkedIn X GitHub
Published 17 Sep 2026 - 5 min read
Native command-line tools in Java

Table of contents

Introduction

Java is very powerful and popular on the server side along with frameworks like Spring Boot, Quarkus or Micronaut.

But it is also a great choice in the terminal. You can develop terminal user interfaces (TUI), for example with TamboUI framework.

You can also easily write command-line tools. And things get really interesting when you build them as native executables: instant startups and no need to install Java on the target machines, simply copy the binary.

In this article, I will show the basics to write command-line tools in Java, and build native executables either locally or using GitHub Actions.

For the complete sources, see the greet-command sample repository.

A basic command line in Java

Let's write a very basic command-line tool that displays a greeting message:

Greet command

When called without arguments, the tool prints Hello!, and when called with an argument, it prints Hello <name>!.

The simplified main class looks like this:

public class GreetCommand {

public static void main(String[] args) {
System.out.println(greetMessage(args));
}

static String greetMessage(String... args) {
if (args.length == 0) {
return "Hello!";
} else {
return "Hello " + String.join(" ", args) + "!";
}
}
}

The code is very basic. It reads the command line arguments, and prints a greeting message accordingly. No dependency on any other library.

Add coloring

Before diving into native builds, let's make the tool a bit more interesting, for fun. You may have noticed in the command-line execution snapshot that the user name is actually displayed in cyan.

There are several ways to do that. Some Java libraries provide this kind of feature. But it's also easy to color some text directly by using ANSI escape codes. These are some specific escape codes that are understood by the terminal.

To color some text, prefix it with the chosen ANSI escape code (\033[0;96m for bright cyan) and suffix it with the reset escape code (\033[0m):

// Prints "Hello John Doe!" with "John Doe" in bright cyan
System.out.println("Hello \033[0;96mJohn Doe\033[0m!");

If you want to know more about ANSI escape codes, see ANSI escape codes - Colors.

In the sample repository, I added a ColorUtil class that supports multiple colors. If you want to see its capabilities, run its main method and see the output. It will display the supported colors. Here is a snapshot of some of these colors:

ANSI Colors

That class defines constants for the various colors: RED, GREEN_BOLD, BLUE_UNDERLINED, CYAN_BACKGROUND, etc. It also provides a colorize static method that takes a color type and a text and returns the colored text:

System.out.println("Hello " + colorize(CYAN_BRIGHT, "John Doe") + "!");

What it does under the hood is wrap the text to color with the right ANSI escape codes.

In GreetCommand, the greetMessage method actually uses the ColorUtil class to color the user name in cyan:

static String greetMessage(String... args) {
if (args.length == 0) {
return "Hello!";
} else {
return "Hello " + colorize(CYAN_BRIGHT, String.join(" ", args)) + "!";
}
}

Local native build

To build a native executable locally, you can use the GraalVM native-image tool. It comes with GraalVM, so first install GraalVM. It contains everything from the JDK, plus the native-image tool and other tools, so can replace your usual JDK.

Nota that on Windows, you also have to install Visual Studio and Microsoft C++ Build Tools (MSVC), see Installation on Windows Platforms.

Then configure your pom.xml to use the native-image plugin:

<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<imageName>greet</imageName>
<mainClass>com.adeliosys.greet.GreetCommand</mainClass>
<buildArgs>
<buildArg>--no-fallback</buildArg>
</buildArgs>
</configuration>
<executions>
<execution>
<id>build-native</id>
<phase>package</phase>
<goals>
<goal>compile-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

The native build configuration is defined in a profile, so that you can build the native executable only when needed. The native build relies on the native-maven-plugin, which is a Maven plugin that wraps the GraalVM native-image tool.

The --no-fallback option is used to stop the build and prevent the generation of a fallback image containing a JVM, if the native build fails.

The package part is used to run the native build in the right Maven phase.

The main class must be defined somewhere. In the previous example, it is defined in the native-maven-plugin configuration, but it can be defined in the maven-jar-plugin configuration instead, see sample repository.

Now, you can build the native executable with the following command:

mvn -Pnative package

This is enough for basic Java applications. But for more complex applications, you may need additional configuration for reflection, dynamic class loading, resources, proxies, or JNI.

The build should be longer than a regular Java build. The Maven output will show the usual build logs and the native-image tool execution output.

When done, the binary is generated in the target directory, and you can run it directly:

target\greet John Doe

GitHub native builds

Building the native executable for your operating system with GraalVM is nice. The next step is to build it for multiple platforms so that you can distribute it to your users.

Doing so locally is tedious and not well-supported by Java. That's where GitHub Actions can help. You can define a workflow that builds the native executable for multiple platforms using GraalVM.

name: Greet command native build

on:
workflow_dispatch:
push:
branches:
- main

jobs:
build:
name: 'Native build - $'
runs-on: $

strategy:
matrix:
include:
- os: ubuntu-latest
artifact_name: greet-command-linux-x64
executable: greet

- os: macos-latest
artifact_name: greet-command-macos-arm64
executable: greet

- os: windows-latest
artifact_name: greet-command-windows-x64
executable: greet.exe

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '25'
distribution: 'graalvm'
github-token: $
cache: maven

- name: Build native executable
run: mvn -Pnative package

- name: Upload binary
uses: actions/upload-artifact@v7
with:
name: $
path: target/$
if-no-files-found: error

The strategy matrix defines several target operating systems to run the job and produce the native executables: Linux 64-bit, Apple Silicon and Windows 64-bit. You can add more if you want.

The first three steps are straightforward: checkout the sources, set-up GraalVM and build the native executable.

The fourth step adds the generated artifacts to the build output so they can be downloaded later.

To download a given native executable:

  • Open greet-command workflow runs
  • Click on the latest run
  • Download the artifact corresponding to your platform, unzip and execute it. Note that macOS Gatekeeper may block the execution of the binary, so you may have to allow it in the Privacy & Security settings.

Native binaries

Conclusion

In this article, I showed how to write a simple command-line tool in Java and build its native executable locally or with GitHub Actions. I also showed how to add some coloring to the output using ANSI escape codes.

I hope this article will help you to write your own command-line tools in Java and distribute them as native executables.

For the complete sources, see the greet-command repository.

© 2007-2026 Florian Beaufumé