Java for Shell Scripting

 · 4 min
xiaokang Zhang on Unsplash

No matter what your daily driver is, most of us also have to write some shell scripts to automate stuff. Usually, we would use bash script, Python, Perl, or some other scripting language. But what if we could use a compiled language instead?

One of the significant advantages of shell scripts is that they are just a single file that you can execute easily. Trying to get the same easy usage from a compiled language, I started using Go, which compiles into a single executable that’s runnable without any additional framework or setup on any machine (that is was compiled for). But Java is my daily driver at work, and I’m the only one at the office trying to learn Go, it might not be the best choice.

After reading “Java for Everything”, I asked myself: “Why not use Java?”.


Java for CLI

Java wouldn’t be the best choice for CLI compared to many other options. Handling files wasn’t easy/fun until Java 7, no awk, sed, etc., no good/up-to-date curses library available, input handling wasn’t fun either, and no helpful built-in arguments library.

Why would I choose this language for my shell scripts?

Well, because it’s my daily driver, I’m fluent in it, and it has all these great advantages in other use cases: a mature, strong-typed language with a humongous amount of 3rd-party-libraries for everything you might need.

Except for curses.


Jar + Shell Script = Single Executable

The first idea was to build a Jar-file, put it somewhere safe and call it via a shell script. It’s simple, but you need to maintain two files. There has to be a better way to end up with a single file.

Then, I discovered that Java actually supports shebangs. And you can put the Jar in the shell script itself:

shell
# Create a new file 'myjavacli' with a shebang
echo "#\!/usr/bin/java -jar" > myjavacli

# Adding the Jar content
cat my-java-app.jar >> myjavacli

# Make it executable
chmod +x myjavacli

# Now you can run it!
./myjavacli

We just created a strange hybrid: A script containing an ASCII shebang followed by binary data from the Jar. If we edit that file, we’ll kill its encoding, so don’t edit it manually.


What? How?

At first, I wasn’t sure why and how this was actually working.

It’s really simple: The program loader seems to take the shebang and passes the rest of the file to the correct executable. Jar files are just Zip files, and their headers are at the end of the file, so the shebang isn’t interfering.

Making it easier

Thanks to Gradle, we can automate the building of the shell script:

groovy
ext.mainClass = "com.example.Main"

import java.nio.file.Files
import java.nio.file.Paths

task shellScript(type: Jar) {

    // Create fat Jar
    doFirst {
        from {
            (configurations.runtime).collect {
                it.isDirectory() ? it : zipTree(it)
            }
        }
    }

    // Set entry point
    manifest {
        attributes("Main-Class": mainClass)
    }

    // Connect with actual Jar task
    with jar

    // Build single executable file
    doLast {
        // Set name, location and make sure the location exists
        def executableName = jar.archiveName.replace(".jar", "")
        def executablePath = Paths.get(project.buildDir.getAbsolutePath(), "executable", executableName)
        Files.createDirectories(executablePath.getParent())

        // Write sheband to file. It will be overwritten.
        def shebang = "#!/usr/bin/java -jar\n"
        Files.write(executablePath, shebang.getBytes());

        // Write fat Jar file content to the script
        def jarPath = Paths.get(jar.archivePath.getAbsolutePath())
        def jarBytes = java.nio.file.Files.readAllBytes(jarPath)
        Files.write(executablePath, jarBytes, java.nio.file.StandardOpenOption.APPEND)

        // Make it executable
        executablePath.toFile().setExecutable(true)
    }
}

So now we just have to run ./gradlew shellScript and a single file executable will be built for us!


Caveats

As mentioned above, the file contains plain text and binary data, which a text editor cannot handle and will break your file if you open and save it.

Another caveat is a pretty obvious one: compiled code is unreadable code. With a bash script/Python/Perl/etc., you can always read and easily edit the script.


Conclusion

  • It’s great to build small tools with your preferred language instead of bash script.
  • You can easily share your own Java code and libraries between different shell scripts.
  • Developers without bash knowledge can contribute.
  • Having it contained in a single file makes it easier to distribute.