Yes! you can write an executable script in Java
Java's aim recently is to be more beginner friendly, but this also makes it possible to write shorter programs such as scripts
6th Nov 2024
Writing Shell Scripts in Java: Yes, Really! 🤔
Ever thought about writing a shell script in Java? No? Well, buckle up because I'm about to show you how modern Java features make this not only possible but actually pretty neat!
The Mission 🎯
I wanted to create a simple tool called nodesnitch
that hunts down all those pesky node_modules
directories eating up space on your machine. You know the ones - they're probably consuming half your hard drive right now! The script finds these directories and sorts them by size, making it easier for you to decide which ones to rm -rf
into oblivion.
Prerequisites 🛠️
You'll need JDK 23 (might work on 21, but I haven't tested it). The cool part is we'll be using Java's shell shebang feature: #!/path/to/java --source version
.
Let's Build It! 👨💻
- First, create your script file:
touch nodesnitch
- Here's where the magic happens. We'll use a shebang at the top of our file:
#!/usr/bin/env java --enable-preview --source 23
- Now for the actual code (and this is where it gets interesting):
#!/usr/bin/env java --enable-preview --source 23
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public long calculateSize(Path path) {
if (path == null || !Files.exists(path)) {
return 0L;
}
try {
if (Files.isDirectory(path)) {
try(Stream<Path> stream = Files.walk(path)) {
return stream
.filter(Files::isRegularFile)
.mapToLong(p -> {
try {
return Files.size(p);
} catch (IOException e) {
return 0L;
}
})
.sum();
}
} else {
return Files.size(path);
}
} catch (IOException e) {
throw new RuntimeException("Error calculating size for: " + path, e);
}
}
public String humanReadableSize(long bytes) {
if (bytes <= 0) {
return "0 B";
}
final String[] units = new String[] { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
int digitGroups = (int) (Math.log10(Math.max(1, bytes)) / Math.log10(1024));
// Stay within array bounds
digitGroups = Math.min(digitGroups, units.length - 1);
DecimalFormat df = new DecimalFormat("#,##0.##");
return df.format(bytes / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
public void searchDirectories(Path startPath, Predicate<Path> criteria, Consumer<Path> action) {
try {
if (startPath == null || !Files.isDirectory(startPath) ||
startPath.getFileName().toString().startsWith(".") || startPath.toString().contains("/Library")) {
return;
}
if (criteria.test(startPath)) {
action.accept(startPath);
return;
}
try (Stream<Path> stream = Files.list(startPath)) {
stream.filter(Files::isDirectory)
.forEach(subDir -> searchDirectories(subDir, criteria, action));
} catch (IOException e) {
// Just ignore it
}
} catch (Exception e) {
// Silently continue
}
}
public Path getUserHomeDirectory() {
return Path.of(System.getProperty("user.home"));
}
public boolean isNodeModulesDirectory(Path path) {
if (path == null) {
return false;
}
String dirName = path.getFileName().toString();
return Files.isDirectory(path) && "node_modules".equals(dirName);
}
void main(String... args) {
Map<Path, Long> results = new HashMap<>();;
System.out.println("Scanning for node_modules directories...");
searchDirectories(
getUserHomeDirectory(),
this::isNodeModulesDirectory,
path -> {
results.put(path, calculateSize(path));
}
);
System.out.println("Found node_modules:");
results.entrySet()
.stream()
.sorted((a, b) -> Math.toIntExact(b.getValue() - a.getValue()))
.forEach(e -> System.out.printf("%s (%s)\n", e.getKey(), humanReadableSize(e.getValue())));
long totalNodeModulesSize = results.values().stream().collect(Collectors.summarizingLong(Long::longValue)).getSum();
System.out.printf("\n\nTotal space used: %s\n", humanReadableSize(totalNodeModulesSize));
}
- Make it executable:
chmod +x nodesnitch
- Run it:
./nodesnitch
Voilà! You've just written Java(Script)! 😉
Why This Works: Java's Evolution 🌱
Here's the really cool part - Java has been quietly becoming more beginner-friendly, and these changes accidentally made it great for scripting too! Two key features make this possible:
- Lets us run Java files directly without the usual compilation step
- Perfect for quick scripts like this one
- Makes our code less verbose
- Lets us compose functions more naturally
- No more
public static void main
boilerplate!
The Takeaway 💡
While Java isn't trying to become a scripting language, these beginner-friendly features have made it surprisingly capable for quick scripts. Our nodesnitch
tool is a perfect example - it's concise, functional, and actually useful.
Want to check out the complete code? You can find it at my GitHub repo.
Happy hunting those node_modules! 🗑️✨