Scripting with Java
Tags: java, script, shell, tutorial
Published: 2024-06-02 12:41:53

### 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](https://openjdk.org/jeps/330) 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:

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

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

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)](https://openjdk.org/jeps/463) 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.

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

void main() {
    System.out.println("Hello World");
}
```

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)](https://openjdk.org/jeps/477), 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:

{% collapsible Example 1 (HTTP request) %}
Let's say you want to make an HTTP request and print the result. Here's how it looks:

```java
#!/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);
}
```

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:

```java
#!/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());
}
```

6 lines of code in `main()`, + import lines.
{% endcollapsible %}

{% collapsible Example 2 (Terminal command) %}
Here's another example, invoking a terminal command:

```java
#!/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);
}
```
{% endcollapsible %}

{% collapsible 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.
{% endcollapsible %}

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](https://github.com/AnatoliyKozlov/scripting-utils)

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

```java
#!/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());
}
```

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:

```java
#!/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);
}
```

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.

```java
#!/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);
}
```

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](https://openjdk.org/jeps/458), 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.