28. März 2024, 21:30
Lesezeit: ca. 8 Min

Arbeiten in der Kommandozeile - Teil II

Der zweite Teil der Serie stellt Kommandozeilen-Tools für Textdateien vor. Von kleinen Konfigurations-Dateien in den unterschiedlichsten Formaten bis zu größeren XML- und CSV-Dateien reicht die Bandbreite. Das letztgenannte CSV-Format1 stellt oftmals die kleinste verbliebene Kompatibilitäts-Brücke zu älteren Anwendungen.

Menschen nutzen die falschen Werkzeuge. Gängige grafische Office-Programme sind im Umgang mit CSV-Dateien erschreckend schlecht. Von abgeschnittenen Dateien, veränderten Inhalten, Encoding- und Darstellungsproblem bis zu auffallend langsamer Verarbeitungsgeschwindigkeit reichen die wiederkehrenden Probleme. Ein Beispiel, wie dramatisch kaputt Microsoft Excel ist, gab es vor vier Jahren: Wissenschaftler mussten 27 menschliche Genome umbenennen, weil diese immer als Datumswerte interpretiert wurden. Datenverluste und massenhaft falsche Forschungsergebnisse waren die Folgen.2 Kleine Konfigurationsdateien werden mit aufgeblähten und unsicheren3 Electron4 Editoren bearbeitet. Umfangreiche statistische Datenbestände werden mühsam zu Online-Diensten wie zum Beispiel Datawrapper5 hochgeladen und aus der Hand gegeben.

Hier wird deutlich, warum ich im ersten Teil6 etwas weiter ausholen musste und auf die Unix-Philosophie7 (und somit auch Linux) eingegangen bin. Diese Systeme sind für den Umgang mit (Text-)Dateien prädestiniert. Das Zusammenwirken einer programmierbaren Shell wie der Bash8, das “Alles ist eine Datei” Konzept9, das Konzept von Pipes10 machen in der Summe den Unterschied.

Vorgestellt wird der Editor mcedit, Bestandteil des Midnight Commanders11. Es folgen grep12 und der Stream Editor sed13. Einige kleinere Tools wie fold14, paste15, head16 und tail17 kommen links und rechts des Weges. Den Abschluss bildet Visidata18, eine TUI19 Tabellenkalkulation, die Ihre Stärke bei umfangreichen CSV-Daten ausspielt.

Verständlicherweise kann ich hier nur an der Oberfläche kratzen und ich hoffe, einen leichten Einstieg zu geben. Der weitere Weg in die Tiefe führt über die Manpages20. Meist folgt der Aufbau einer eigenen Snippet-Sammlung21 bis irgendwann grafische Programme langsam und seltsam vorkommen. Wenige Zeilen in der Kommandozeile ersetzen längeres Rumklicken. Your milage may vary und wie immer gilt: Kein Anspruch auf Vollständigkeit oder Richtigkeit.

Einstieg mit mcedit

Noch bevor grafische Oberflächen in den Mainstream kamen habe ich mich ab Mitte der 80er mit dem Norton Commander22 und MSDOS digital sozialisiert. Dadurch pflege ich eine ganz besondere Beziehung zum freien GNU Midnight Commander, der den Norton nachahmt. Wer heute aus der grafischen Welt kommt, bekommt im Midnight Commander den Abstand zur Kommandozeile ein wenig verkürzt, ohne auf Komfortfunktionen wie Mausbedienung oder Menü-Bar23 verzichten zu müssen.

Zur Oberfläche von mcedit bedarf es nur wenig Erklärungen. In der ersten Zeile befindet sich das Hauptmenü mit aufklappbaren Drop-Down-Untermenüs. In der letzten eine F-Tasten Leiste. Dazwischen der Texteingabebereich. Eher unbekannt ist die Fähigkeit von mcedit mehrere Dateien gleichzeitig zu öffnen und sie in “floating windows” halten zu können. Noch unbekannter sind die Hex-Editor Darstellung und das Schwesterprogramm mcdiff zum Vergleich von zwei Textdateien.

mcedit in Aktion mit zwei offenen Textdateien

Inhalte lassen sich zwischen diesen mit vorherigem Markieren und Betätigen der entsprechenden F-Taste ausschneiden, kopieren oder entfernen. Empfehlenswert ist die Einblendung von Zeilennummern und Einschalten der Syntax-Farbhervorhebung.

Erzeugen einer CSV-Referenzdatei

Für die nächsten Schritte brauchen wir eine große Textdatei. Zum Erstellen dient nachfolgendes Bash-Skript, welches anhand der übergebenen Parameter eine kommaseparierte Datei mit zufälligen Zellinhalten erstellt:

#!/bin/bash
# Erzeugt eine CSV Datei mit Randomwerten

# Überprüfen der CLI Argumente
if [ "$#" -ne 3 ]; then
 echo "Verwendung: $0 <Zeilen> <Spalten> <Ausgabedatei.csv>"
 exit 1
fi

rows=$1
columns=$2
output_file=$3

# Check auf valide Werte
if ! [[ $rows =~ ^[0-9]+$ && $columns =~ ^[0-9]+$ && $rows -ge 1 && $columns -ge 1 ]]; then
 echo "Zeilen und Spalten müssen Ganzzahlen und >= 1 sein."
 exit 1
fi

# Header
header=$(printf "Col_%d," $(seq 1 $columns))

# Inhalt(e)
echo "Erstelle CSV-Datei mit $rows Zeilen und $columns Spalten..."
echo "$header" > "$output_file"

for ((i=1; i<=$rows; i++)); do
  openssl rand -hex $((8 * $columns)) | fold -w 16 | paste -sd ',' >> "$output_file"
done

Eine fertige Datei steht hilfsweise gezippt zusammen mit dem Skript hier zum Download.

fold und paste

Die ersten Unix-Texttools fold und paste werden in einer Pipeline in dieser Zeile sichtbar:

openssl rand -hex $((8 * $columns)) | fold -w 16 | paste -sd ',' >> "$output_file"

Das Programm openssl generiert zufällige Zeichen in hexadezimaler Schreibform. Die Länge entspricht 8 Bytes multipliziert mit der Anzahl der Spalten aus der Variable $columns, die wir dem Aufruf mitgeben. Das Ergebnis entspricht genau einer Zeile der späteren CSV-Datei. Alles noch “an einem Stück” ohne Trennzeichen. Das erledigen die nächsten Programme in der Pipe.

fold wird mit dem Parameter -w 16 aufgerufen. Das bricht die übergebene Zeichenkette nach jeweils 16 Zeichen Breite (w=width) in eine neue Zeile. Das Ergebnis wird in der Pipe weiter an das Programm paste gereicht. Dessen Parameter -sd ‘,’ fügt nach jeder neuen Zeile ein Komma als Trennzeichen ein. Das Resultat wird in die Datei $output_file geschrieben.

Wir haben eine kommaseparierte CSV-Zeile mit Zufallsinhalten in genau der gewünschten Spaltenanzahl. Das Ganze wiederholt sich in einer for-Schleife solange, bis die Anzahl von Zeilen aus der Variable $rows erreicht ist.

grep

Als Nächstes bitte die erzeugte Referenzdatei mit der gewohnten Tabellenkalkulation öffnen. Die Importeinstellungen bitte so wählen, wie im nachfolgenden LibreOffice Calc Importfenster sichtbar:

CSV-Import Einstellungen in Libreoffice Calc

Ist die Datei geöffnet, bitte irgendwohin in die Dateimitte scrollen und zufällig einen beliebigen Zelleninhalt auswählen. Mit diesem möchten wir arbeiten und mit grep danach suchen. Der Syntax ist übersichtlich:

grep <option(en)> <suchbegriff(e)> <dateiname(n)>

In der Kommandozeile bitte in das gleiche Verzeichnis von test.csv wechseln und den nachfolgenden Befehl eingeben, selbstredend mit dem von Ihnen zuvor ausgesuchten Suchbegriff:

grep "6f70639a741b7571" test.csv

Das Ergebnis zeigt alle Zeilen, in welchen der Suchbegriff enthalten ist. In unserem Fall sollte es genau eine sein. Zur besseren Verdeutlichung sind bei mir die nachfolgenden Ausgaben in den Screenshots farblich hervor gehoben. Das erfolgt mit der Option –color.

Was fehlt ist noch Angabe der Zeile, in der unser Suchbegriff gefunden wurde. Diese erhalten wir mit –line-number oder -n, ebenfalls farblich hervorgehoben:

grep -n "6f70639a741b7571" test.csv

Um den Kontext eines Suchergebnis besser zu sehen, können mit der Option -C 2 jeweils die letzten beiden vorherigen und nachfolgenden Zeilen ausgegeben werden:

grep -n  -C 2 "6f70639a741b7571" test.csv

Die Ausgabe des Ganzen ergibt dieses Bild:

grep Suchausgabe mit Einfärbung und vorhergehenden bzw. nachfolgenden Zeilen

Soll eine (Text-)CSV-Datei mit mehreren Suchbegriffen durchsucht werden kann das mit der Option -e erfolgen. Mehrere Suchbegriffe werden Wiederholung der Option -e hintereinander kaskadiert:

grep -n -e "abc123" -e "a12345" test.csv

Das Ergebnis dieser Suche sieht so aus:

grep Suche mit mehreren Suchbegriffen und Ausgabe

Die Mächtigkeit von grep wird deutlich, wenn ein Suche über mehrere Dateien reichen soll. Wildcards24 und Pattern25 sowie die Option –recursive oder -r durchsuchen ganze Verzeichnisstrukturen. Die Ergebnisse lassen sich genauso nacheinander in einer Pipe kaskadieren, wie im Eingangsbeispiel mit fold und paste gezeigt.

Die nachfolgende Zeile sucht sowohl in test.csv, test2.csv als auch in allen Dateien, die im Namen “demo” oder “trial” und die Dateierweiterung .csv aufweisen. Die Dateinamen der Treffer stehen jeweils vor den Zeilenangaben:

grep -n -e "abc123" -e "a12345" test.csv test2.csv *@(demo|trial)*.csv

grep Suche über mehrere Dateien

Der Grund zur Entwicklung und gleichzeitig Namensgebung für grep ist die Verwendung von regulären Ausdrücken.26 Bislang waren unsere Suchbegriffe relativ statisch. Reguläre Ausdrücke machen diese ähnlich wie Wildcards und Pattern variabel. Wir stehen vor der Tür in ein anderes Universum, in das wir hineinschauen aber nicht betreten werden.

sed

Steuert grep die Ausgabe, so verändert sed die Inhalte. Zusammen awk27 zählt es zu den Unix-Haupttools zur Manipulation von Textdateien. Die nachfolgende Zeile:

sed -i "s/suchwort/ersatzwort/" test.csv

Sucht mit dem Befehl “s” (=substitute) in der Datei test.csv nach “suchwort” und ersetzt es mit “ersatzwort”. Die Option -i bedeutet “in-place” direkt im Stream, so dass die angegebene Datei mit dem Resulat überschrieben wird.

Im nachfolgenden Befehl fügt sed mit dem Befehl “i” (wie insert) in der zweiten Zeile von test.csv eine neue Zeile mit drei Bindestrichen ein. Die Angabe “zweite Zeile” ist für sed eine Adresse, die mit der Zahl 2 repräsentiert wird:

sed -i "2 i\---" test.csv 

Damit sed das in jede zehnte Zeile einer Datei einfügt, nutzen wir als Adresse die Startzeile 2 und dann jeweils den zehnten Wert (2~10):

sed -i "2~10 i\---" test.csv

Das Ergebnis sollte wie folgt aussehen:

sed hat in jede 10. Zeile Zwischenzeilen eingefügt

Zum rückgängig machen gibt es den sed Befehl “d” wie delete. Die gesuchten Adressen können jetzt nur noch mit Hilfe eines regulären Ausdruckes beschrieben werden, was aber sehr einfach ist: Wir suchen alle Zeilen, die mit “—” beginnen und wieder enden. In RegEx übersetzt entspricht das “^—$” und wird vor dem /d Befehl gesetzt:

sed -i "/^---$/d" test.csv

head und tail

Den letzten Screenshot genau betrachtet? Zur Darstellung von (Text-)Dateien, gibt es die beiden Befehle head und tail. Ohne Parameter werden jeweils die ersten zehn bzw. letzten zehn Zeilen einer Datei ausgeben. Zur Ausgabe der ersten 30 Zeilen wird die Zahl einfach als Option mit -30 eingegeben. Die Stärke von beiden Befehlen ist die Verwendung in Pipes. Zum Ermitteln einer jüngsten oder ältesten Datei lässt sich ein nach Datum sortiertes Verzeichnis mit ls -t ausgeben und anschliessend head bzw. tail das erste oder letzte Element ausgeben.

ls -t | head -1

Der Befehl tail hat eine weitere interessante Option -F (follow and retry). Es wartet auf weitere Inhalte am Ende einer Datei und gibt diese aus, was bei großen, anwachsenden Logdaten nützlich ist, die “live” beobachtet werden können.

tail -F test.csv

visidata

Zum Abschluss die Vorstellung einer kleinen Geheimwaffe im Umgang mit besonders umfangreichen CSV-Daten. Die Rede ist von mehreren hunderttausenden Zeilen, wie diese in Warenwirtschaftssystemen oder Logdaten vorkommen. Ob Pivot-Tabellen, das Filtern oder Gruppieren, die Analyse von Trends oder Mustern - alle diese gängigen Operationen erledigt Visidata ohne sichtbare Leistungseinbußen.

Ansicht der Referenzdatei in visidata

Auch wenn das Erscheinungsbild vertraut vorkommt, ist das Bedienkonzept komplett anderes wenn zuvor nur mit Excel oder Calc gearbeitet wurde. Es lohnt sich, das Einstiegstutorial28 von Jeremy Singer-Vine und die typischen Tasten zur Navigation kennenzulernen.

Besonders erwähnenswert: Visicalc kann gezippte Daten direkt öffnen, ohne dass diese vorher extrahiert werden müssen. Zahlreiche externe PyPI-Bibliotheken sorgen für direkte Verbindungen mit gängigen Datenbank-Servern und machen Visicalc sogar zu einem kleinen Reporting-Tool mit festen (Macro-)Abläufen.

In diesem Sinne,
Tomas Jakobs


  1. https://de.wikipedia.org/wiki/CSV_(Dateiformat) ↩︎

  2. https://www.theverge.com/2020/8/6/21355674/human-genes-renamed ↩︎

  3. https://blog.jakobs.systems/micro/20220117-electron-cef-developer/ ↩︎

  4. https://en.wikipedia.org/wiki/Electron_(software_framework) ↩︎

  5. https://www.datawrapper.de/ ↩︎

  6. https://blog.jakobs.systems/blog/20230628-linux-cli/ ↩︎

  7. https://de.wikipedia.org/wiki/Unix-Philosophie ↩︎

  8. https://de.wikipedia.org/wiki/Bash_(Shell) ↩︎

  9. https://de.wikipedia.org/wiki/Everything_is_a_file ↩︎

  10. https://de.wikipedia.org/wiki/Pipe_(Informatik) ↩︎

  11. https://en.wikipedia.org/wiki/Midnight_Commander ↩︎

  12. https://en.wikipedia.org/wiki/Grep ↩︎

  13. https://en.wikipedia.org/wiki/Sed_(Unix) ↩︎

  14. https://en.wikipedia.org/wiki/Fold_(Unix) ↩︎

  15. https://en.wikipedia.org/wiki/Paste_(Unix) ↩︎

  16. https://en.wikipedia.org/wiki/Head_(Unix) ↩︎

  17. https://en.wikipedia.org/wiki/Tail_(Unix) ↩︎

  18. https://www.visidata.org/ ↩︎

  19. https://en.wikipedia.org/wiki/Text-based_user_interface ↩︎

  20. https://en.wikipedia.org/wiki/Manpage ↩︎

  21. https://en.wikipedia.org/wiki/Snippet_(programming) ↩︎

  22. https://en.wikipedia.org/wiki/Norton_Commander ↩︎

  23. https://en.wikipedia.org/wiki/Menu_bar ↩︎

  24. https://de.wikipedia.org/wiki/Wildcard_%28Informatik%29 ↩︎

  25. https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html ↩︎

  26. https://de.wikipedia.org/wiki/Regul%C3%A4rer_Ausdruck ↩︎

  27. https://de.wikipedia.org/wiki/Awk ↩︎

  28. https://jsvine.github.io/intro-to-visidata/ ↩︎

© 2024 Tomas Jakobs - Impressum und Datenschutzhinweis

Unterstütze diesen Blog - Spende einen Kaffee