Erstgespräch vereinbaren
Kommende Termine
4. Mai 2026
IT-Talk: Digitale Verantwortung
Details →
12. Juni 2026
Vernissage
28. September 2026
IT-Talk: Event-Driven Architecture

jbang - Shell-Skripte in Java

Mit jbang Java statt Bash für Shell-Skripte nutzen: Lokale Installation, IDE-Support und praktische Beispiele.

Laptop-Arbeitsplatz mit Code

Die meisten Projekte enthalten Aufgaben, die zu klein für eine eigene Software sind, aber häufig genug ausgeführt werden, um ein kleines Programm zu rechtfertigen. Üblicherweise werden Bash-Skripte gewählt, weil keine Installation erforderlich ist und die meisten absehbaren Probleme gelöst werden können.

Die Verwendung alternativer Programmiersprachen für diese Aufgaben war bisher zu umständlich. Java wäre ideal, da Programme auf vielen Systemen laufen und die mitgelieferten Bibliotheken umfangreiche Funktionalität bieten, um Probleme in wenigen Zeilen zu lösen. Allerdings stellten das Aufsetzen einer Entwicklungsumgebung und das Erstellen ausführbarer Programme Hürden dar. Was in wenigen Zeilen Bash gelöst werden konnte, wurde zu einem vollständigen Projekt.

Das jbang-Projekt ermöglicht es, Java anstelle von Bash-Skripten zu verwenden und dabei den Fokus auf Problemlösung und IDE-Komfort beizubehalten.

Lokale Installation

Um jbang zu nutzen, installiert man es lokal im Projektverzeichnis. Skripte aus dem Internet sollten mit Vorsicht heruntergeladen und erst nach Prüfung des Quellcodes ausgeführt werden.

Installation:

curl -Ls https://sh.jbang.dev | bash -s - wrapper install

Bestehende Installationen aktualisieren:

curl -Ls https://sh.jbang.dev | bash -s - wrapper install --force

Erste Schritte

Ein jbang-Skript erstellen:

./jbang init --template=cli fun.java

Die Ausgabe bestätigt die Initialisierung:

[jbang] File initialized. You can now run it with 'jbang fun.java' or edit it using 'jbang edit --open=[editor] fun.java'

In IntelliJ Idea öffnen:

./jbang edit --open=idea fun.java

Das Template enthält picocli für die Verarbeitung von Kommandozeilen-Argumenten:

///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.5.0

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;

import java.util.concurrent.Callable;

@Command(name = "fun", mixinStandardHelpOptions = true, version = "fun 0.1",
        description = "fun made with jbang")
class fun implements Callable<Integer> {

    @Parameters(index = "0", description = "The greeting to print", defaultValue = "World!")
    private String greeting;

    public static void main(String... args) {
        int exitCode = new CommandLine(new fun()).execute(args);
        System.exit(exitCode);
    }

    @Override
    public Integer call() throws Exception { // your business logic goes here...
        System.out.println("Hello " + greeting);
        return 0;
    }

}

Für lokale Installationen muss die erste Zeile angepasst werden:

///usr/bin/env ./jbang "$0" "$@" ; exit $?

Ausführen mit:

./fun.sh

Wo ist die aktuellste Datei?

Ein praktisches Beispiel: die neueste Datei in einem Verzeichnis finden:

./jbang init --template=cli newest.java
./jbang edit --open=idea newest.java

Die vollständige Implementierung:

///usr/bin/env ./jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.5.0

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.Optional;
import java.util.concurrent.Callable;

@Command(name = "newest", mixinStandardHelpOptions = true, version = "newest 0.1",
        description = "find the newest file inside of a directory")
class newest implements Callable<Integer> {

    @CommandLine.Spec
    CommandLine.Model.CommandSpec spec;

    private File directory;

    @Parameters(index = "0", description = "directory")
    public void setDirectory(File directory) {
        checkArgument(directory.exists(),directory+" does not exist");
        checkArgument(directory.isDirectory(),directory+" is not a directory");
        this.directory=directory;
    }

    private void checkArgument(boolean condition, String errorMessage) {
        if (!condition) throw new CommandLine.ParameterException(spec.commandLine(), errorMessage);
    }

    public static void main(String... args) {
        int exitCode = new CommandLine(new newest()).execute(args);
        System.exit(exitCode);
    }

    @Override
    public Integer call() throws Exception {
        NewestFileContainer newestFileContainer=new NewestFileContainer();

        Files.walkFileTree(directory.toPath(), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                newestFileContainer.inspect(file, attrs.lastModifiedTime());
                return FileVisitResult.CONTINUE;
            }
        });

        newestFileContainer.newestPath().ifPresent(it -> System.out.println("newest file "+it));

        return 0;
    }

    private static class NewestFileContainer {

        private PathWithModificationDate newest = null;

        public void inspect(Path file, FileTime lastModifiedTime) {
            if (newest==null) {
                newest = new PathWithModificationDate(file, lastModifiedTime);
            } else {
                newest = PathWithModificationDate.newerInstance(newest,
                    new PathWithModificationDate(file, lastModifiedTime));
            }
        }

        public Optional<Path> newestPath() {
            return Optional.ofNullable(newest).map(it -> it.file);
        }
    }

    private static class PathWithModificationDate {

        final Path file;
        final FileTime lastModifiedTime;

        public PathWithModificationDate(Path file, FileTime lastModifiedTime) {
            this.file = file;
            this.lastModifiedTime = lastModifiedTime;
        }

        public static PathWithModificationDate newerInstance(PathWithModificationDate a, PathWithModificationDate b) {
            return a.lastModifiedTime.compareTo(b.lastModifiedTime) > 0 ? a : b;
        }
    }
}

Ausführung ohne Argumente:

Missing required parameter: '<directory>'
Usage: newest [-hV] <directory>
find the newest file inside of a directory
      <directory>   directory
  -h, --help        Show this help message and exit.
  -V, --version     Print version information and exit.

Mit einem Verzeichnis als Argument:

./newest.sh .

Fazit

jbang bietet umfangreiche Möglichkeiten. Code in Java zu schreiben mit starker IDE-Unterstützung reduziert Fehler im Vergleich zu Bash-Skripten. Kompilierung und Ausführung verursachen nur vernachlässigbaren Zeitaufwand. Die IDE-Unterstützung bietet erhebliche Vorteile.

Über den Ersatz von Bash-Skripten hinaus könnte gemeinsamer Code als Abhängigkeiten Duplikation über Skripte hinweg eliminieren. Unit-Tests werden möglich, wo Bash-Skripte typischerweise keine Abdeckung haben.

Zurück zum Blog