DEV Community

Anatolii Kozlov
Anatolii Kozlov

Posted on • Edited on

Scripting with Java

Introduction

As a developer, you sometimes need to write scripts. If your primary expertise is in Java, you might have considered writing scripts in Java instead of Bash or Python. However, if you've tried this, you quickly realized it's not as straightforward as it seems due to Java's verbosity. In this article, I'll explain why scripting with Java now is possible and, more importantly, practical. I'll also introduce a small utility that allows you to write Java scripts that are simple and powerful.

1. It's Possible

Writing scripts in Java has been possible since Java 11. JEP 330: Launch Single-File Source-Code Programs introduced the ability to run single-file Java scripts without requiring explicit compilation. This feature also allowed adding a shebang to the beginning of the file, enabling scripts to be run directly from the command line.

Even though Java 11 made shebangs possible, nobody started writing scripts in Java because it was cumbersome. Just look at this 'Hello World' example:

#!/usr/bin/env java --source 11

public class Script {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
Enter fullscreen mode Exit fullscreen mode

This leads us logically to the next part of the article.

2. Now It's Simpler

Starting with Java 22 (in preview mode), JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) allows us to omit the class declaration and reduce the main function declaration from public static void main(String[] args) to simply void main(). This is a significant improvement.

#!/usr/bin/env java --source 22 --enable-preview

void main() {
    System.out.println("Hello World");
}
Enter fullscreen mode Exit fullscreen mode

It will become even simpler in the third iteration of this JEP. Java 23, with JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview), will allow writing print(obj), println(obj), and readln(obj) instead of System.out.println(obj), thanks to the automatic import of the java.io.IO package.

3. But It's Still Not Practical

Yeah, the newer versions of Java have reduced much of the verbosity, so writing a 'Hello World' script become easier. The problem becomes apparent when trying to do something more complex. Consider the following example:

Example 1 (HTTP request)
Let's say you want to make an HTTP request and print the result. Here's how it looks:
#!/usr/bin/env java --source 22 --enable-preview

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

void main() throws Exception {
    var url = new URL("https://httpbin.org/get");
    var con = (HttpURLConnection) url.openConnection();
    con.setRequestMethod("GET");
    var in = new BufferedReader(new InputStreamReader(con.getInputStream()));
    String inputLine;
    var content = new StringBuffer();
    while ((inputLine = in.readLine()) != null) {
        content.append(inputLine);
    }
    in.close();
    con.disconnect();
    System.out.println(content);
}
Enter fullscreen mode Exit fullscreen mode

This is a mess. 11 lines of code in main(), just to make an HTTP request and print the result, + import lines. Using HttpClient introduced in Java 11 doesn't simplify it much:

#!/usr/bin/env java --source 22 --enable-preview

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

void main() throws Exception {
    var httpClient = HttpClient.newHttpClient();
    var response = httpClient.send(HttpRequest.newBuilder()
                    .uri(URI.create("https://httpbin.org/get"))
                    .build(),
            HttpResponse.BodyHandlers.ofString());
    System.out.println(response.body());
}
Enter fullscreen mode Exit fullscreen mode

6 lines of code in main(), + import lines.


Example 2 (Terminal command)
Here's another example, invoking a terminal command:
#!/usr/bin/env java --source 22 --enable-preview

import java.io.BufferedReader;
import java.io.InputStreamReader;

void main() throws Exception {
    var process = Runtime.getRuntime().exec("ls -lah");
    var reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
    int exitCode = process.waitFor();
    System.out.println("Exited with code: " + exitCode);
}
Enter fullscreen mode Exit fullscreen mode

Example 3 (Files IO)
Or, working with files... Have you ever tried to delete a directory with all its contents? You'd struggle with walk() and nested try-catch blocks for handling numerous checked exceptions.

All this shows that while Java is powerful, it's not suitable for scripting. But what if I told you it could be?

The problem is that standard mechanisms don't provide default behavior. That's a pity because it simplifies life. Think about why Spring Boot starters became so popular. It includes default behavior. For cases that require detailed configuration, you can always define it, but for most scripting tasks, it's not necessary.

That's why I created a utility that allows you to write Java scripts concisely.

4. Introducing Scripting Utils for Java

Scripting Utils - a single Java file containing several useful wrapper classes with default behavior. Static objects of these wrappers are declared for quick access to functions in this file.

GitHub: https://github.com/AnatoliyKozlov/scripting-utils

To see how convenient this is, let's revisit the examples above using Scripting Utils.

#!/usr/bin/env java --source 22 --enable-preview --class-path /Users/toliyansky/scripting-utils

import static scripting.Utils.*;

void main() {
    var response = http.get("https://httpbin.org/get");
    log.info(response.body());
}
Enter fullscreen mode Exit fullscreen mode

As we can see, we need to add --class-path /Users/toliyansky/scripting-utils to the shebang line and a static import import static scripting.Utils.*;, then we can leverage the full power of Scripting Utils.

Just two lines for an HTTP request and logging the response.

Other examples:

#!/usr/bin/env java --source 22 --enable-preview --class-path /Users/toliyansky/scripting-utils

import static scripting.Utils.*;

void main() {
    var terminalResponse = terminal.execute("ls -lah", 100);
    log.info(terminalResponse.output);
}
Enter fullscreen mode Exit fullscreen mode

Just two lines for executing a command and logging the response.

Scripting Utils includes wrappers for:

  • http for HTTP requests
  • terminal for terminal commands
  • file for file operations
  • log for logging
  • thread for threading

Let's look at an example that utilizes the advantages of Scripting Utils.

Imagine our script needs to read a file containing lines in the format <name> <URL>. For each line, it should make an HTTP request and save the result to a file named <name>. As a bonus, let's do this in parallel, because we want to leverage Java's strengths over Bash or Python.

#!/usr/bin/env java --source 22 --enable-preview --class-path /Users/toliyansky/scripting-utils

import static scripting.Utils.*;

void main() {
    var linksFilePath = "links.txt";
    if (!file.exists(linksFilePath)) {
        log.error("File not found: " + linksFilePath);
        return;
    }
    file.readAllLines(linksFilePath)
            .forEach(line -> thread.run(() -> {
                var data = line.split(" ");
                var fileName = data[0];
                var url = data[1];
                var response = http.get(url);
                if (response.statusCode() == 200) {
                    file.rewrite(fileName, response.body());
                    log.info("File " + fileName + " updated from " + url);
                } else {
                    log.warn("File " + fileName + " was not updated. Http code: " + response.statusCode());
                }
            }));
    thread.sleepSeconds(5);
    var terminalResponse = terminal.execute("ls -lah", 100);
    log.info(terminalResponse.output);
}
Enter fullscreen mode Exit fullscreen mode

Only 20 lines of code in main() that do tons of work. It uses the files, http, log, thread, and terminal modules.

Can you imagine the monstrosity this would be without Scripting Utils? Or in Bash 😄? Or in Python?

The downside is that Scripting Utils needs to be installed. The project repository has a one-line command that downloads Utils.java, places it in a specific directory, and compiles it. This only needs to be done once. After that, you can use it in your scripts.

Conclusion

As you can see, Java is actively working towards simplifying the writing of simple programs, including scripts. The numerous JEPs I've mentioned above, and others like JEP 458: Launch Multi-File Source-Code Programs, attest to this. However, even with these simplifications, Java remains not the most convenient language for scripting. With the advent of Scripting Utils, it has become practical as well.

Top comments (7)

Collapse
 
mr_gogu_50e476aeb983675e9 profile image
Mr Gogu • Edited

A more universal way to use shebang is to specify/usr/bin/env java, instead of explicitly specifying the path to the interpreter. Depending on the distribution, it can be located either in /usr/bin or/bin or /usr/local/bin. Specifying env searches the PATH for an interpreter.
See stackoverflow.com/questions/437930...

Collapse
 
toliyansky profile image
Anatolii Kozlov

You're right, it makes sense.
I think that I will edit the article and examples in the repository taking this into account.

Collapse
 
martinhaeusler profile image
Martin Häusler

#!/usr/bin/java

... I learn something new every day, I had no idea that this was a thing :) Thanks!

Collapse
 
joao9aulo profile image
João Paulo Martins Silva

Great! looks like i am going to retire my python scripts.

Collapse
 
frelvick profile image
Viktor Frelikh

Sounds really good. I've looked in GitHub but haven't found the license description. Is it free to use in commercial cases? Do you count to publish it as a free software? Thanks in advance

Collapse
 
toliyansky profile image
Anatolii Kozlov

Hi, yeah, of course, it will be under a free license. I will add it a little bit later.

Collapse
 
djeang profile image
Jerome Angibaud • Edited

Nice article.

Just know that tools as jbang or JeKa provide better alternative as you can mention any Maven dependency in the source code (as com.google.guava:guava:33.3.1-jre ) to be automatically included in the classpath.

Moreover, JeKa provides a mean to execute regular methods (not only public static void main) since java8. It also contains similar classes for easily script http, file & string manipulation, and so on. You can use it as a tool or a single jar library.

You are welcome to try or extend it.

Disclamer: I'm the author of JeKa.