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! 🗑️✨