Revisiting Java for Shell Scripting
Around 6 years ago, I wrote about using Java for shell scripts. It was a hacky and fragile way to convert some Java code into a shebanged file containing the content of a Jar file.
However, Java evolved quite a bit since that article, so it’s time to look at it again.
Table of Contents
Running Java Code Directly
The main reason for the previous approach of packing a Jar file into a plain text file was Java’s lack of running uncompiled code. Even though it “works,” it’s also super fragile, as it breaks each time an editor saves the file and screws up the encoding.
Around 19 months after my original article, Oracle released Java 11, which contained a quite important new feature regarding Java’s shell scripting capabilities: JEP 330.
This enhancement proposal is about “launching single-file source-code programs” by using java
directly instead of the old javac
/java
combo:
Instead of telling java
which .class
file to look up by the class name, we tell it to use a Java file directly.
In fact, it doesn’t even need to be a file ending in .java
.
The added argument --source
enables “source-file mode” and specifies that the file is containing source code in a specific version:
Not Your Typical Java Environment
Running a single class
without compiling it first or converting it into a jar
file is already quite a nice improvement.
However, JEP 330 isn’t only about combining the compile and run steps into a single call.
Running java
in “source-file mode”, either by providing a .java
file or specifying --source
, sets certain expectations to the context applying to compilation and running your code.
Interpreting Command Line Options
Command-line options specified before the source are respected, including --class-path
, --module-path
, --add-exports
, --add-modules
, --limit-modules
, --patch-module
, --upgrade-module-path
, --enable-preview
, and variants of those.
Any other options, like -processor
or -Werror
aren’t passed along.
Argument files (@-files
) are still supported.
Compiling the Code
The compilation process also has certain assumptions and restrictions:
- Any command-line options relevant to compilation are taken into account.
- Only the specified source file is used. No other files are found or compiled, which is equivalent to an empty source path value.
- No annotation processing, as if
-proc:none
were set. - If
--source <version>
is set, it also sets--release <
version>` implicitly. - The source file is compiled as an unnamed module.
One thing that totally breaks from the Java conventions you’re used to is the following:
- The source file can contain more than one top-level type, like classes and interfaces.
- The file isn’t forced to have the same name as any top-level type.
JLS §7.6 specifies how top-level type declarations are supposed to work.
Even though we could use nested classes, splitting them up into top-level ones keeps it more straightforward, in my opinion.
Execution Phase
The execution of the compiled code also isn’t done the usual way:
- The first top-level type found is executed, so make sure it is a class and has a
public static void main(String[])
to run. - Adding a
main
method to any other class has no effect. - A custom class loader delegates to the application’s class loader, so classes in the application classpath cannot use any types from the source file.
- In addition to any
--add-module
, the compiled code is run as if--add-modules=ALL-DEFAULT
is set. - Any arguments after the source file’s name are passed to the executed
main
method.
Now that we know how it works and what to expect, let’s take a look at how to use it in a shebang file.
Shebang Files
To make running Java as simple as other shell scripts, it needs to be able to run in a shebang file. The simplest variant is:
It works fine, but still has a hardcoded path to the java
executable, as the shebang expects it to be this way.
To mitigate, we can “abuse” then env
command to provide an environment where $PATH
is available:
That’s it!
Put in your Java code, make the file executable with chmod +x MyScriptFile
, and you can run it with ./MyScriptFile
.
Now you have a Java shell script that’s easy to edit and doesn’t require any additional steps like before!
Another thing is that the JEP anticipated using shebang files.
You can still run the script file with java --source <version> <source-file>
without worrying about the shebang.
In the case of an exception, the resulting line numbers are even adapted to make sense in the context of the file.
Java 21 Improvements
With the release of Java 21, there’s now a preview JEP available that makes the scripting approach even simpler: JEP 445: Unnamed Classes and Instance Main Methods (Preview)
I’ve written an article about it, but in essence, it simplifies the required ceremony for a minimal program:
You still need to wrap your code in the void main()
method, but you no longer require arguments, or a surrounding class.
And the best thing, this doesn’t affect your ability to add additional top-level types to the file!
To use this, though, you need to enable preview features in your java
call:
This is starting to look more and more like a real scripting file!
Why not Groovy or Kotlin?
Groovy generally has a more dynamic and “scripty” feel to it, as it doesn’t require as much boilerplate to run, and also supports shebang files.
Kotlin, supports scripting explicitly, at least as an experimental feature.
Both Groovy and Kotlin even have a big advantage over Java here, as both have built-in dependency management.
Groovy has the @Grab
annotation (also known as “Grape”), whereas Kotline has @file:...
to specify repositories and load dependencies or other local files.
To be honest, if you want to have a scripting-like experience on the JVM, Groovy or Kotlin are a better choice than Java. But using a familiar and your preferred language can easily outshine a “better” suited one.
Conclusion
Java has gained a lot of bells and whistles to better support the use case of simple scripting since my previous article on the topic, and it looks like the OpenJDK is working to simplify the minimal requirements even further in the future.
It still might not be the top contender for scripting in general, but I like to have a familiar language at my disposal if I want to, and not all of my colleagues speak as much Bash as I do.