Blog

Besseres Shell-Scripting mit Scala-CLI

Dave Smith

Aktualisiert Oktober 15, 2025
10 Minuten

Die Notwendigkeit von "Klebstoff"-Code ist eine Tatsache im Leben eines Programmierers. Früher oder später wird die Anwendung, die Sie entwickelt haben, dieses glänzende Denkmal der Softwaretechnik, durch die Ausführung auf einem realen System beschädigt werden müssen.

Vielleicht brauchen Sie ein Boot-Skript oder Sie müssen einen Container erstellen. Vielleicht ist es ein Tool, das als Teil eines größeren Orchestrierungsprozesses aufgerufen werden muss.

Was auch immer der Grund sein mag, Sie brauchen einen Code, der die Magie in Gang setzt. Das bedeutet wahrscheinlich ein Shell-Skript, und für die meisten Leute bedeutet das wahrscheinlich die Verwendung von Bash (oder etwas Ähnlichem).

Die Bash ist in Verbindung mit den üblichen Linux-Befehlszeilen-Tools fantastisch leistungsfähig und anpassungsfähig. Sie ist aber auch ziemlich fummelig und esoterisch.

In diesem Beitrag sehen wir uns einen alternativen Ansatz mit Scala und Scala-CLI an.

Ein Hinweis zum Stil

Obwohl ich funktionale Programmierung genauso mag wie der nächste Scala-Entwickler, möchte ich mich in diesem Beitrag nicht zu sehr damit beschäftigen. FP-Scripting ist durchaus möglich, aber das ist nicht das Ziel dieses Beitrags.

Wenn wir versuchen zu suggerieren, dass Scala eine brauchbare Alternative zu Bash ist, müssen wir uns fragen, warum Bash immer noch so beliebt ist.

Wir können nicht viel tun, um mit der Allgegenwärtigkeit und allgemeinen Verfügbarkeit von Bash zu konkurrieren, da wir Scala / Scala-CLI überall dort installieren müssen, wo wir es benötigen.

Ich glaube, der Hauptgrund für die Attraktivität der Bash ist die vermeintliche Entwicklungsgeschwindigkeit. Man hat das Gefühl, dass es schnell geht, ein Bash-Skript - verzeihen Sie mir - zu schreiben und es zum Laufen zu bringen. Dennoch glaube ich, dass wir hier mithalten können.

Es stimmt zwar, dass Scala immer eine Kompilierzeit und geringe Anlaufkosten haben wird (es sei denn, Sie exportieren in ein natives GraalVM-Image!), aber mit der richtigen Auswahl an Bibliotheken können Sie meiner Meinung nach mit Scala-CLI schneller ein korrektes Skript schreiben als mit Bash, insbesondere wenn der Umfang und die Komplexität des Skripts zunehmen.

Aus diesem Grund habe ich mich für 'The Singaporean Stack' / das Haoyi Li Ökosystem entschieden, das meine absolute Lieblings-Scalabibliothek enthält: OS-lib. Diese Tools bieten uns eine Schnittstelle, die in ihrer direkten Einfachheit mit den Kommandozeilen-Tools vergleichbar ist, aber deutlich mehr Leistung bietet.

Einrichtung mitverfolgen

Sie müssen Scala-CLI installiert haben, um den Scala-Abschnitten in diesem Beitrag folgen zu können. Im Bash-Abschnitt verwenden wir curl und jq.

Motivierendes Szenario

Werfen wir einen Blick auf einen einfachen fiktiven Anwendungsfall.

Wir brauchen ein Skript, das Daten von der Star Wars API abruft und speichert.

Unsere Eingabe ist eine Datei namens planets.txt, die eine URL pro Zeile enthält, die auf Daten zu Planeten im Star Wars-Universum verweist. Hier sind unsere Beispieldaten:

https://swapi.dev/api/planets/1/
https://swapi.dev/api/planets/2/
https://swapi.dev/api/planets/3/

Unser Skript wird:

  1. Stellen Sie sicher, dass ein geeignetes Ausgabeverzeichnis zur Verfügung steht
  2. Lesen Sie die Eingabedatei
  3. Für jede URL in der Datei:
    1. Laden Sie die JSON-Daten herunter
    2. Extrahieren Sie den Namen des Planeten
    3. Speichern Sie eine JSON-Datei mit dem Namen des Planeten als Dateinamen und den JSON-Rohdaten als Inhalt.

Etwas zusammenbasteln

Nachfolgend finden Sie einen ersten Versuch, der in 28 Zeilen ziemlich knapper und dennoch gut lesbarer Bash codiert ist, großzügig kommentiert und mit Abstand.

#!/bin/bash

set -e

# Create, or if it exists already, clear and recreate the output dir
DESTINATION="./output"

if [ -d "$DESTINATION" ]; then
  rm -rf $DESTINATION
fi

mkdir $DESTINATION

# Read the file
while read -r line
do
  # Request the data
  PLANET=$(curl -s $line) # -s tells curl to work silently

  # Extract the name
  NAME=$(echo $PLANET | jq -r .name) # -r discards the quotes, so you get Yavin, not "Yavin".

  # Output the data to a json file named after the planet in the output directory
  echo $PLANET > "$DESTINATION/$NAME.json"

  # Print the name for some user feedback
  echo $NAME
done < "planets.txt"

Um dieses Skript auszuführen, speichern Sie es in einer Datei mit dem Namen demo.sh im gleichen Verzeichnis wie Ihre Datei planets.txt und führen Sie es von Ihrer Befehlszeile aus mit: bash demo.sh

Auf den ersten Blick sieht das ziemlich gut aus. Ok, ich musste zum millionsten Mal in meiner Karriere die Syntax der if-Anweisung von Bash googeln, aber das Endergebnis ist tatsächlich recht gut lesbar.

Es gibt allerdings ein paar Probleme:

  1. Es gibt eine beängstigende rm -rf, die ich immer sehr genau bedenken muss, um nicht versehentlich alle meine Daten zu löschen.
  2. Ich bin nicht sicher, ob es überhaupt funktioniert, bis ich es ausprobiert habe.
  3. Es gibt keine andere Fehlerbehandlung als das set -e Flag, und das Hinzufügen von Fehlermechanismen würde die Komplexität des Skripts ziemlich erhöhen.

Scala für Skripting

Wenn wir an Scala denken, denken wir meist an Big Data, Backend-Dienste und langsame JVM-Startzeiten. In Wirklichkeit kann Scala im Grunde alles, von nativen Anwendungen über Webanwendungen bis hin zu - ja - CLI-Tools und Skripten.

Lassen Sie uns versuchen, unser Bash-Skript nach Scala zu portieren und es über die Scala-CLI auszuführen.

//> using scala 3.3.1

//> using dep com.lihaoyi::os-lib:0.9.3
//> using dep com.lihaoyi::requests:0.8.0
//> using dep com.lihaoyi::upickle:3.1.4

// Create, or if it exists already, clear and recreate the output dir
val dest = os.pwd / "output"

if os.exists(dest) then os.remove.all(dest)

os.makeDir(dest)

// Read the file
os.read
  .lines(os.pwd / "planets.txt")
  .foreach: url =>
    // Request the data
    val planet = requests.get(url).text()

    // Extract the name
    val name = ujson.read(planet)("name").str

    // Output the data to a json file named after the planet in the output directory
    os.write(dest / s"$name.json", planet)

    // Print the name for some user feedback
    println(name)

Um dieses Skript auszuführen, speichern Sie es in einer Datei mit dem Namen demo.sc im gleichen Verzeichnis wie Ihre Datei planets.txt und führen Sie es von Ihrer Befehlszeile aus mit: scala-cli demo.sc.

Dies ist ein einfaches Scala-CLI-kompatibles Skript. Da es sich bei der Datei um eine .sc Datei handelt, dürfen alle Anweisungen auf der obersten Ebene stehen. Hätte ich stattdessen eine .scala Datei verwendet, hätte ich eine main Methode zur Verfügung stellen müssen oder ein Objekt, das App erweitert, oder ähnliches.

Am Anfang des Skripts sehen Sie einige Anweisungen, die mit //> beginnen. Diese werden'Direktiven' genannt und dienen dazu, Scala-CLI Informationen über Ihren Build zu übermitteln. Hier geben sie die Scala-Version an, die wir zusammen mit unseren Bibliotheksabhängigkeiten benötigen. Wenn Sie ein Projekt mit mehreren Dateien haben, ist es eine gute Praxis, alle Direktiven in einer einzigen Datei zu speichern.

Vergleichen Sie die Lösungen

Das Scala-Skript ist mehr oder weniger eine direkte Übersetzung der Bash-Version. Es ist mindestens genauso gut lesbar, die Kommentare und Abstände sind die gleichen und es hat sogar die gleiche Anzahl von Codezeilen wie das Original!

Haben wir irgendwelche Vorteile daraus gezogen? Nun, ich freue mich, dass ich bereits drei niedrig hängende Verbesserungen verzeichnen kann:

  1. Die große, unheimliche rm ist weg!
  2. Das Programm hat eine Tippprüfung durchgeführt, also kann ich nicht viele offensichtliche Fehler gemacht haben.
  3. Mein Redakteur konnte mir bei der Entwicklung mit API-Informationen und Dokumenten helfen.

Ersetzen von Bash

In mancher Hinsicht sind wir bereits besser als Bash, aber es ist noch kein entscheidender Sieg für Scala.

Was können wir tun, um Sie zu überzeugen, umzusteigen?

Der Umstieg von Bash auf Scala und Scala-CLI bietet eine Reihe von Vorteilen, nicht zuletzt solide Lösungen für traditionell schwierige Probleme in Bash, wie die Handhabung von Sonderzeichen und die hervorragende Unterstützung für Unit-Tests.

Die beiden Punkte, die meiner Meinung nach die unmittelbarsten Auswirkungen haben, sind:

  1. Fehlerbehandlung
  2. Ausdrucksstärke und Skalierbarkeit

Fehlerbehandlung

Schauen wir uns eine andere Version dieses Skripts an:

//> using scala 3.3.1

//> using dep com.lihaoyi::os-lib:0.9.3
//> using dep com.lihaoyi::requests:0.8.0
//> using dep com.lihaoyi::upickle:3.1.4

// Create, or if it exists already, clear and recreate the output dir
val dest = os.pwd / "output"

if os.exists(dest) then os.remove.all(dest)

os.makeDir(dest)

val filename = "planets.txt"

if os.exists(os.pwd / filename) then
  // Read the file
  os.read.lines
    .stream(os.pwd / filename) // Stream in case it's a lot of data.
    .foreach: url =>
      // Request the data
      val response = requests.get(url)
      val planet   = response.text()

      // Handle failed requests
      if response.statusCode != 200 then println(s"Error getting planet at url '$url', got '${response.statusCode}'.")
      else
        // Extract the name and handle the missing field
        ujson.read(planet)("name").strOpt match
          case None =>
            println(s"Unnamed planet at url '$url'!")

          case Some(name) =>
            // Output the data to a json file named after the planet in the output directory
            os.write.over(
              dest / s"$name.json",
              planet
            ) // Write over existing files, instead of erroring.

            // Print the name for some user feedback
            println(name)
else println(s"Could not find the expected file '$filename', in the working directory.")

Diese Version ist etwas länger, aber Sie werden feststellen, dass wir eine Reihe einfacher Verbesserungen bei der Fehlerbehandlung vorgenommen haben, die in der Bash umständlich zu handhaben gewesen wären.

Streaming von Daten

Durch den Wechsel von:

os.read.lines(os.pwd / filename)

An:

os.read.lines.stream(os.pwd / filename)

Wir streamen die Daten nun zeilenweise, so dass wir bei einem sehr großen Datensatz keine riesigen Speichermengen anhäufen würden.

Überschreiben oder Fehler

Durch den Wechsel von:

os.write(dest / s"$name.json", planet)

An:

os.write.over(dest / s"$name.json", planet)

Wir sagen jetzt ausdrücklich, dass es in Ordnung ist, vorhandene Dateien zu überschreiben. In der früheren Version haben wir eine Ausnahme erhalten, wenn eine Datei mit demselben Namen gefunden wurde.

Das bedeutet, dass wir das Verhalten von Bash nachbilden, aber wir mussten bei dieser Designentscheidung sehr genau sein.

Behandlung von Antwort-Statuscodes

Unser Code prüft, ob wir eine 200 vom Server zurückerhalten, bevor wir mit der Verarbeitung fortfahren, und meldet, wenn es ein Problem gibt.

Behandlung fehlender Felder

Mein persönlicher Favorit: Was passiert in der Bash-Version, wenn das Feld name nicht vorhanden ist? Tatsächlich erhalten Sie ein null als String, und das Skript macht trotzdem weiter!

In der Scala-Version können wir sicherstellen, dass dies nicht mit dem von allen bevorzugten null aware type, Option, passiert:

ujson.read(planet)("name").strOpt match
  case None       => ???
  case Some(name) => ???

Ausdrucksstärke und Skalierbarkeit

Es gibt eine lange Geschichte von Menschen, die andere Programmiersprachen als Ersatz für Shell-Skripte verwenden; Perl ist ein gutes Beispiel dafür. Die Hauptmotivation besteht darin, die Fähigkeit des Programmierers zu verbessern, das auszudrücken, was er erreichen möchte, und dabei vertraute Werkzeuge zu verwenden.

Scala ist die "skalierbare" Sprache, und das wird selten deutlicher als im Zusammenhang mit der Skripterstellung.

Wenn Ihre Anforderungen einfach und direkt sind, können Sie schnell ein paar Zeilen prozeduralen, seiteneffektiven, veränderbaren Code mit geringer Abstraktion schreiben, um die Aufgabe zu erledigen.

Wenn die Anforderungen an Ihr Skript wachsen, wächst Scala mit Ihnen, indem Sie einfache Abstraktionen, Funktionen und Unit-Tests einführen und so Python ähnliche Ergebnisse erzielen, die auf dem reichhaltigen, robusten und relativ unkomplizierten Bibliotheks-/Package-Ökosystem von Scala aufbauen.

Bei umfangreicheren, komplizierteren und nuancierteren Herausforderungen sorgen die standardmäßige Unveränderlichkeit von Scala, die Effektsysteme und die leistungsstarken funktionalen Konstrukte dafür, dass Sie immer auf der produktivsten Abstraktionsebene für das jeweilige Problem arbeiten, ohne die Scala-CLI jemals zu verlassen.

Die Bash kann das nicht. Das können wirklich nicht viele Sprachen.

Fazit

In diesem Beitrag habe ich Scala-CLI verwendet, um eine Reihe von Bibliotheken aus dem Haoyi Li Ökosystem zu nutzen, die mit ihren unkomplizierten APIs und geringen Abhängigkeiten meiner Meinung nach hervorragend für den Anwendungsfall Skripting geeignet sind.

Die Auswahl meiner Bibliotheken ist zwar eine Frage des persönlichen Geschmacks, aber ich hoffe, ich konnte Sie davon überzeugen, dass Scala und Scala-CLI ein mehr als würdiger Ersatz für Ihre übliche Skriptsprache sind.

Wir haben in diesem Bericht nicht einmal annähernd an der Oberfläche dessen gekratzt, was Scala-CLI alles kann, aber ich hoffe, Sie fühlen sich inspiriert, es einmal auszuprobieren!

Viel Spaß mit Scala Scripting!

Verfasst von

Dave Smith

Dave accidentally became a backend Scala developer in 2012 and has been trying to return to the frontend ever since. By insisting on dragging Scala and Scala.js with him, success levels in this endeavour have been dubious at best. He's best known as the maintainer of Indigo, a Scala.js game engine, and Tyrian, an Elm-inspired Scala.js web framework.

Contact

Let’s discuss how we can support your journey.