jbang – scripting for Java
Ever had a small task to script? Ever felt Java was not up to the task despite being your primary language? JBang is coming to the rescue.
Use case
When I migrated my blog, I had a need to change the Front Matter part of my blog entries. Blogs themselves are in pure Markdown with no tie to the website generator structure but there is some metadata at the top that tends to be specific to the platform. This structure is called Front Matter and is standardized.
---
title: Some blog entry
date: 2021-01-10
author: Emmanuel Bernard
layout: blog-layout.html.haml
header:
teaser: /images/some-teasing-iamge.png
---
And now come my _markdown_ syntax.
## why
Well, I needed an example
See the header between each ---
is the Front Matter syntax and as you see, it hosts metadata.
In my case, I needed to remove the layout
metadata piece as well as write down the date.
The layout template name is different and Jekyll has a nicer template name abstraction logic they call scope for this class of problem.
Anyways I needed a small script that was reading files, removing one specific line and adding a specific line with specific content.
Java
Java is not the stack you tend to use for that. There are multiple reasons.
Some feel that the JDK is powerful but kinda verbose for these targeted needs. It has gotten way better with a compounded advent of lambda, default methods in interfaces and the addition of more helper methods.
The main issue to me for such task that my problem fits in one Java class. Why do I need to also have:
- a
pom.xml
orbuild.gradle
to express my dependencies - a three level deep
src/main/java
directory structure - some sort of long incantation to start my script or in practice wrap that incantation in a bash file of sort
People just give up and write the migration in bash or Ruby or something else.
JBang!
Enters JBang.
JBang is a way to make a one file Java code be executable and remain a one file project:
- it lets you run java files from your terminal e.g
./hello.java
- it lets you define your dependencies at the top of your single Java file
- it lets you define your targeted Java from that same Java file
- it even installs Java if it’s not there
- it will start an IDE to let you edit your file
First, install jbang.
Second (optional), get a minimal script file created for you.
jbang init migrate.java
Third (optional), open your script in an IDE. JBang can create an environment that your IDE will understand (dependency file, src structure etc). Alternatively, you can just use a plain editor (like Vim) but most Java developers like their IDE.
# start VSCode to edit my script file
code `jbang edit migrate.java`
Forth, write your Java script (not JavaScript ahah).
Here is the migrate.java
script I wrote – the code is pretty bad but that’s almost the point, it’s a one off script!
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 15
// //DEPS <dependency1> <dependency2>
import static java.lang.System.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class migrate {
private static Pattern DATE_PATTERN = Pattern.compile("([0-9]{4})-([0-9]{2})-([0-9]{2}).*");
public static void main(String... args) throws IOException {
File source = new File("~/Sites/emmanuelbernard.com/blog");
String destinationName = "~/Sites/emmanuelbernard.com/_posts";
for (File file: new File(destinationName).listFiles())
if (!file.isDirectory())
file.delete();
for (final File fileEntry : source.listFiles( (FilenameFilter) (dir, name) -> name.endsWith(".md") || name.endsWith(".erb"))) {
out.println(fileEntry.getName());
FileSystem fileSystem = FileSystems.getDefault();
Path fileToRead = fileSystem.getPath(fileEntry.getAbsolutePath());
List<String> lines = readAllLines(fileToRead, Charset.defaultCharset());
if (!isFrontmatter(fileToRead, lines)) {
continue;
}
if (!isDatePresent(fileEntry, fileToRead, lines)) {
Matcher m = DATE_PATTERN.matcher(fileEntry.getName());
StringBuilder date = new StringBuilder("date: ");
if (!m.find()) {
// in case a file has no date
FileTime fileTime = Files.getLastModifiedTime(fileToRead);
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
date.append(dateFormat.format(fileTime.toMillis()));
lines.add(frontmatterIndex(fileToRead, lines), "draft: true");
err.println("Cannot extract date from file name: " + fileEntry.getName() + " | file timestamp: " + date.toString() + " | Mark as draft");
}
else {
date.append(m.group(1)).append("-").append(m.group(2)).append("-").append(m.group(3));
}
lines.add(frontmatterIndex(fileToRead, lines), date.toString());
}
removeLayout(fileEntry, fileToRead, lines);
//Files.copy(
// fileToRead,
// fileSystem.getPath(destinationName, fileEntry.getName()),
// StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
// convert erb files into html ones
File destination = new File(fileSystem.getPath(destinationName, fileEntry.getName()).toString().replace(".html.erb", ".html"));
PrintWriter fw = new PrintWriter(destination);
for (String line : lines) {
fw.println(line);
}
fw.close();
destination.setLastModified(fileToRead.toFile().lastModified());
}
}
private static boolean isDatePresent(final File fileEntry, Path fileToRead, List<String> lines) {
boolean isDatePresent = false;
for (int i = 1 ; i < frontmatterIndex(fileToRead, lines) ; i++) {
if (lines.get(i).startsWith("date")) {
err.println("date already present in frontmatter for " + fileEntry.getName());
isDatePresent = true;
}
}
return isDatePresent;
}
private static void removeLayout(final File fileEntry, Path fileToRead, List<String> lines) {
int layoutIndex = -1;
for (int i = 1 ; i < frontmatterIndex(fileToRead, lines) ; i++) {
if (lines.get(i).startsWith("layout")) {
layoutIndex = i;
break;
}
}
if (layoutIndex != -1) {
lines.remove(layoutIndex);
}
}
private static int frontmatterIndex(Path fileToRead, List<String> lines) {
for (int i = 1 ; i < 30 ; i++) {
if ("---".equals(lines.get(i))) return i;
}
return 0;
}
private static boolean isFrontmatter(Path fileToRead, List<String> lines) {
if (!"---".equals(lines.get(0))) {
err.println("Unable to recognize frontmatter for (first line) " + fileToRead.toString());
return false;
}
if ( 0 == frontmatterIndex(fileToRead, lines)) {
err.println("Unable to recognize frontmatter for (last line) " + fileToRead.toString());
return false;
}
return true;
}
public static List<String> readAllLines(Path path, Charset cs) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path, cs)) {
List<String> result = new ArrayList<>();
for (;;) {
String line = reader.readLine();
if (line == null)
break;
result.add(line);
}
return result;
}
}
}
You see at the top of the files, a set of comments that are interpreted by jbang. The first line will direct your environment to use Java to run that script. The second line shows which Java version I want to target (optional). The third one, I don’t use but that’s where you would put your dependencies. Here is an example of dependencies declaration.
//usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 15
//DEPS log4j:log4j:1.2.17
import static java.lang.System.out;
import org.apache.log4j.Logger;
import org.apache.log4j.BasicConfigurator;
import java.util.Arrays;
class classpath_example {
// ...
}
Finally, it is time to run the script.
jbang migrate.java
# or
chmod u+x migrate.java #do it once
./migrate.java
Voilà! You have a one line runnable Java script.
Conclusion
JBang is the perfect tool to write one file scripts in Java. It allows fluent Java developers to not have to move away from their comfy language of choice.
JBang can do a ton more than that, make sure to read the documentation:
- script repos
- multiple source files and other resource files
- debugging mode, JFR
- and many more
Install JBang and keep that tool in your toolbox.
Comments