Tipps und Tricks
Artikel für fortgeschrittene Anwender
Dieser Artikel erfordert mehr Erfahrung im Umgang mit Linux und ist daher nur für fortgeschrittene Benutzer gedacht.
Zum Verständnis dieses Artikels sind folgende Seiten hilfreich:
- Tipps
- Tricks
- Leeren einer Datei
- Vermeidung von read und Pipe
- Kopieren mit tar und ssh
- Speichern von Befehlen
- Befehle wiederholen: Stapelverarbeitung ...
- Tabellen zur Weiterverarbeitung umwandel...
- Zuletzt ausgeführten Befehl noch einmal ...
- Auf letzten Parameter des letzten Befehl...
- Zählung der Array-Indizes bei eins begin...
- Links
Dieser Artikel soll ein wenig Hilfe geben, "besser" mit der Shell umzugehen. Insbesondere wird ein Augenmerk darauf gelegt, übersichtlich zu bleiben und dabei möglichst wenige Prozesse zu verwenden. In Zeiten von Gigahertz-Prozessoren wirkt das vielleicht ein wenig anachronistisch, wenn allerdings ein "schlechtes" Konstrukt in einem Skript einige tausend oder gar Millionen Male ausgeführt wird, können nicht unerhebliche Laufzeiten entstehen.
Weiterhin soll hier der eine oder andere nicht offensichtliche Trick zu finden sein.
Die Shell und die Standard-Unix-Hilfsprogramme wie beispielsweise grep, sed und awk sind eine mächtige Kombination, die das Computer-Leben erleichtern kann.
Tipps¶
Sinnloses cat¶
cat wird häufig verwendet, um den Inhalt einer Datei an ein Programm zu übergeben. Das ist aber überflüssig, da man das auch mit einem Kleiner-Als-Zeichen machen kann.
Schlecht:
cat dateiname | grep "suchbegriff"
Schon besser:
grep "suchbegriff" <dateiname
Vielen Befehlen kann man die Datei aber auch direkt übergeben, darunter auch grep:
grep "suchbegriff" dateiname
Sinnloses kill -9¶
kill -9
bricht einen Prozess ohne Rücksicht auf Tochterprozesse ab. Das sollte nur dann in Erwägung gezogen werden, wenn ein kill PID
nicht mehr funktioniert.
kill PID
sendet eine "Beenden-Aufforderung (-15
/-TERM
)" an den Prozess mit der Nummer PID
, damit bekommt er die Gelegenheit, offene Dateien zu schließen, die Tochterprozesse sauber zu beenden, Socket-Verbindungen zu beenden, temporäre Dateien zu löschen, ...
pkill NAME
sendet ebenfalls eine "Beenden-Aufforderung (-15
/-TERM
)" an den Prozess, nur dass man hier den Prozessnamen bzw. einen regulären Ausdruck anstatt der PID benutzt. Siehe auch pidof mit dem korrekten Programmnamen.
Erst wenn das nicht funktioniert, weil der Prozess hängt, ist ein kill -9
angebracht.
Sinnloses echo¶
Das ist ein Spezialfall der sinnlosen Verwendung von Kommandosubstitution:
Häufig ist folgende (falsche) Verwendung zu sehen:
1 | Kommando $(echo $variable) |
Eine Shell interpretiert jede Kommandozeile zweistufig. In der ersten Stufe werden alle Variablen durch ihre Inhalte ersetzt, es wird eine Dateinamenersetzung und eine Kommandoersetzung durchgeführt. In der zweiten Stufe wird die daraus entstandene Kommandozeile ausgeführt.
Damit ist Folgendes der richtige Weg:
Kommando $variable
In der ersten Stufe wird $variable durch dessen Inhalt ersetzt. In der zweiten Stufe wird die daraus entstandene Zeile ausgeführt.
Sinnloses ls¶
Ein weiterer Spezialfall der sinnlosen Verwendung von Befehlssubstitution ist der Einsatz von ls
.
Falsch:
1 2 3 4 | for i in $(ls *) do cat $i done |
Wie im vorigen Abschnitt beschrieben, durchläuft jedes Shellkommando einen zweistufigen Interpretationsprozess. In der ersten Stufe wird eine Dateinamenersetzung durchgeführt, so dass man gerne das Folgende macht:
1 2 3 4 | for i in * do cat $i done |
Jedes Vorkommen von *
wird durch die Dateien ersetzt, die auf *
passen. Beinhalten die Dateien jedoch Leerzeichen, dann werden auch die Dateinamen einzeln interpretiert und das Leerzeichen wirkt wie ein Trenner, sie werden aufgesplittet. Um dies zu vermeiden sollte man die zu bearbeitende Variable immer in "" setzen oder eine read
-Schleife benutzen.
1 2 3 4 | for i in * do echo "$i" # Variable in " " done |
Es tritt noch ein weiteres Problem auf, bei ls *
werden auch evtl. vorhandene Unterverzeichnisse durchsucht und das Ergebnis wird verfälscht.
Um das Beispiel mit cat
zu vervollständigen: cat
erlaubt auch die Angabe mehrerer Dateien, also ist das folgende die beste Lösung um Dateiinhalte aufzulisten:
cat *
Sinnloses grep | wc -l¶
Sehr häufig sind Konstrukte anzutreffen, in denen etwas gesucht und die Anzahl der Zeilen mit Treffern ausgegeben wird.
1 | grep "suchbegriff" dateiname | wc -l |
Das Programm grep selber hat allerdings die Option die Anzahl der Trefferzeilen auszugeben.
Daher ist
grep -c "suchbegriff" dateiname
die bessere Lösung. Ausnahme:
grep -o "suchbegriff" dateiname | wc -l
Der Befehl
1 | grep -oc "suchbegriff" dateiname |
würde hier zu einem falschen Ergebnis führen. Überhaupt lohnt sich ein Blick in die Kommandozeilenoptionen von grep.
Sinnloses grep | head¶
Wenn man nur den ersten Treffer sucht, ist es sinnlos, diesen mit dem Befehl
1 | grep "suchbegriff" dateiname | head -n 1 |
zu ermitteln. Besser ist in diesem Fall die Verwendung von
grep -m 1 "suchbegriff" dateiname
da hier nicht nur das gleiche Ergebnis erzielt wird, sondern die Suche auch nach dem ersten Treffer abbricht.
Sinnloses grep | awk¶
awk wird von vielen überwiegend dazu verwendet, Spalten einer Logdatei (oder einer anderen Ausgabe) auszugeben.
Um die Zeilen zu finden, aus denen awk die Spalten ausgeben soll, wird meistens grep vorgeschaltet, so dass ein solches Konstrukt dabei entsteht:
1 | grep "suchbegriff" dateiname | awk '{ print $2 }' |
Das gibt die zweite Spalte von Zeilen aus, die den Suchbegriff enthalten.
Da awk auch suchen kann, ist es aber sinnvoller
awk '/suchbegriff/ { print $2 }' dateiname
zu verwenden.
Sinnloses grep | sed¶
Häufig werden aus Dateien mit grep die Zeilen gesucht, in denen man etwas mit sed suchen und ersetzen möchte.
1 | grep "suchbegriff" dateiname | sed 's/anderesuche/ersetzungstext/' |
Da sed auch suchen kann, ist es aber sinnvoller
sed '/suchbegriff/s/anderesuche/ersetzungstext/' dateiname
zu verwenden.
Sinnloses ps | grep¶
Gerne wird der Aufruf
1 | ps aux | grep ssh # als Beispiel wird das Programm ssh genutzt |
ausgeführt, um festzustellen ob ein Programm läuft bzw. welche PID es hat. Dieser Aufruf ist aus 2 Gründen nicht zu empfehlen: 1. Es werden zwei Programme für die Ergebnisaufstellung genutzt und 2. grep
findet sich selbst ebenfalls in der Prozessliste und zeigt dies auch an. grep
-Kenner wenden darum einen weiteren Trick an und es entstehen folgende Aufrufe
1 2 3 | ps aux | grep -v "grep" |grep ssh # als Beispiel wird das Programm ssh genutzt # oder mit der Kraft der regulären Ausdrücke ps aux | grep [s]sh |
um das grep
wieder auszublenden.
All diese Eingaben sind nicht notwendig, denn unter Ubuntu gibt es das Programm pgrep, welches das viel kürzer kann:
pgrep -fl ssh
bzw. um nur die PID zu ermitteln:
pidof ssh
oder mit ps
-eigenen Mitteln:
ps -C ssh
Sinnlose Kommandosubstitution¶
Ein Skript, in dem $( ... )
vorkommt, oder die veralteten Backticks, kann man sehr häufig optimieren.
1 2 3 | for i in $(cat dateiname) ; do ... done |
Lässt sich optimieren nach
1 2 3 | while read i ; do ... done < dateiname |
Wenn statt cat dateiname
ein komplexerer Befehl benutzt wird, ist es besser statt
1 2 3 | for i in $(komplexerer Befehl) ; do ... done |
1 2 3 | while read i ; do ... done< <(komplexer Befehl) |
zu schreiben.
Sinnloses test¶
Es ist überflüssig
1 2 3 4 5 | kommando if [ $? -ne 0 ] ; then echo "fehler" exit 255 fi |
zu schreiben, wenn doch auch
1 2 3 4 | if ! kommando ; then echo "fehler" exit 255 fi |
funktioniert.
In dem Zusammenhang noch ein gerne gemachter "Fehler" mit grep.
1 2 | grep "suchbegriff" datei > /dev/null if [ $? -ne 0 ] ; then |
ist genauso wenig optimal wie
1 | if grep "suchbegriff" datei > /dev/null ; then |
weil es dafür "-q" gibt, welches die Suche beim ersten Treffer abbricht.
if grep -q "suchbegriff" datei ; then
Sinnloses sort¶
Die Zeilen einer Datei foo sollten so zeilenweise sortiert werden:
sort -o foo foo
Folgendes funktioniert nicht, sondern führt zu einer leeren Datei foo.
sort foo > foo
Folgendes wird häufig verwendet, ist aber zu umständlich:
sort foo > bar mv bar foo
Regulärer Ausdruck und .*¶
(weitergehende Informationen gibt es unter grep)
.*
könnte man auch umschreiben mit "alles oder nichts". Das ist der reguläre Ausdruck, der immer wahr ist..
ist das Jokerzeichen, steht also für irgendein Zeichen.*
steht für kein Mal oder beliebig oft.
Ein beliebiges Zeichen, kein Mal oder beliebig oft, trifft auf alles zu. Wenn statt des Sterns *
ein Pluszeichen +
verwendet wird, ergibt es Sinn. Das +
steht für wenigstens ein Mal. Wird grep verwendet, muss noch ein Schrägstrich vor das Pluszeichen gesetzt werden: \+
, und der gesamte reguläre Ausdruck muss auch dann gequotet werden, wenn er mit dem Stern nicht gequotet werden müsste. Bei egrep mit erweiterten regulären Ausdrücken ist dieses Escapen des Metazeichens nicht nötig.
Sinnloses dd | hexdump¶
Häufig will man sich den MBR-Inhalt mittels hexdump ansehen, dabei werden dann Kombinationen mit dd und einer Pipe gebaut, das ist nicht nötig: hexdump kann man einen Startwert und ein Anzahl der auszuwerteten Bytes mitgeben. Sollte die Platte mit logischen Sektoren ungleich 512 Bytes eingerichtet sein, dann müssen die Werte natürlich auf diese abgestellt werden.
1 | sudo dd if/dev/sda count=1| hexdump -C |
Beispiele:
Andruck des MBR der Festplatte /dev/sda:
sudo hexdump -s0 -n512 -C /dev/sda
Dabei beschreibt s den Start (0 Byte) und n die Anzahl der zu druckenden Bytes (512 = MBR). Mit -C wird der Canonical-Modus eingeschlatet (Hex und mögliche Klarschrift).
Hier wird der GPT-Header (LBA 1) angedruckt:
sudo hexdump -s512 -n512 -C /dev/sda
Tricks¶
Leeren einer Datei¶
Man kann eine Datei namens dateiname mittels
> dateiname
leeren.
Vorteil: Der Dateihandle ändert sich nicht. Was bedeutet, dass ein Programm, das die Datei als Logdatei benutzt, dort weiterhin hineinschreiben kann, ohne neu gestartet werden zu müssen (Beispiel Serverdienste und deren Logdateien).
Vermeidung von read
und Pipe¶
In den meisten Shells wird jedes Kommando einer Pipe in einer separaten Shell (Sub-Shell) ausgeführt, dadurch stehen die bearbeiteten Variablen anschließend im Eltern-Prozess nicht mehr zur Verfügung. Das folgende Beispiel veranschaulicht dies:
1 2 3 4 5 6 7 8 | # printf übergibt 2 Zeilen die mittels read gezählt werden Zaehler=0 printf "%s\n" foo bar | while read -r line do Zaehler=$((Zaehler+1)) echo "Zaehler in der Schleife: $Zaehler" # Ausgabe 2 done echo "Zaehler nach der Schleife: $Zaehler" # Ausgabe 0 |
Das gleiche Problem tritt auch ohne Schleife auf
1 2 3 | Zaehler=0 echo 2 | read -r Zaehler echo "echo übergab 2, aber jetzt beinhaltet der Zaehler: $Zaehler, also 0" |
Um dieses Verhalten zu umgehen, sollte man read mittels Prozess-Substitution aufrufen.
Dabei kommt dem < eine besondere Bedeutung zu: | |
< irgendeineDatei | lesen der Daten aus der Datei irgendeineDatei |
< <(Kommando/Kommandokette) | lesen der Daten aus der Ausgabe eines Kommandos oder einer Kommandokette |
<<<"$Variable" | lesen der Daten aus einer vorher befüllten Variable |
Hier nun die korrekten Beispiele:
read
liest die Zeilen aus einer Datei¶
1 2 3 4 5 6 7 | Zaehler=0 while read -r line do Zaehler=$((Zaehler+1)) echo "Zaehler in der Schleife: $Zaehler" done < irgendeineDatei echo "Zaehler nach der Schleife: $Zaehler" |
read
liest die Zeilen aus einem Kommando bzw. Kommandokette¶
1 2 3 4 5 6 7 | Zaehler=0 while read -r line do Zaehler=$((Zaehler+1)) echo "Zaehler in der Schleife: $Zaehler" done < <(cat Datei1 Datei2) echo "Zaehler nach der Schleife: $Zaehler" |
read
liest die Zeilen aus einer vorher befüllten Variable¶
1 2 3 4 5 6 7 8 | Variable=$(printf "%s\n" foo bar usw ...) Zaehler=0 while read -r line do Zaehler=$((Zaehler+1)) echo "Zaehler in der Schleife: $Zaehler" done <<<"$Variable" echo "Zaehler nach der Schleife: $Zaehler" |
multiple Aufrufe von "<()" können dabei so aussehen:
1 | diff <(echo "foo") <(echo "fool") |
Noch ein Hinweis zum Schluss: read
liest standardmäßig Zeilen ein, reagiert also auf ein \n
(newline). Will man dies beeinflussen, kann man das mit der Option -d delim
bewirken. Mehr dazu unter man bash-builtins read
Kopieren mit tar und ssh¶
Wenig bekannt ist, dass SSH auch Standardeingabe und Standardausgabe verwenden kann. Damit ist es möglich via
tar cf - <dateien oder verzeichnisse> | ssh <user>@<zielhost> "cd <zielverzeichnis> && tar xf -"
<dateien oder verzeichnisse>
vom aktuellen Rechner ins <zielverzeichnis>
auf dem <zielhost>
als <user>
zu kopieren. Das "-"-Zeichen bei tar cf
sorgt dafür, dass tar seinen Datenstrom in Richtung Standardausgabe schickt, die wird mittels "|
" umgelenkt auf den ssh-Befehl, in dem wiederum tar xf -
die Daten von der Standardeingabe liest.
Das ist noch nichts spektakuläres. Das kann scp
auch. Allerdings überträgt scp
- auch mit der Option -p
- keine Dateieigentümerschaften und löst symbolische Links auf, anstatt sie 1:1 zu übertragen. Deswegen ist scp
für das exakte Klonen eines Verzeichnisbaums ungeeignet.
Zusammen mit dem Paket buffer (aus universe) kann man den Datentransfer außerdem gerade bei sehr vielen kleinen Dateien beschleunigen.
tar cf - <dateien oder verzeichnisse> | buffer -m 10m | ssh <user>@<zielhost> "cd <zielverzeichnis> && tar xf -"
Das buffer -m 10m
sorgt dafür, dass ein Puffer von 10 Megabytes eingerichtet wird.
Speichern von Befehlen¶
Manchmal speichert man einen Befehl in einer Variablen, um ihn später an mehreren Stellen aufzurufen:
1 2 3 4 5 6 7 8 | #!/bin/bash cmd="vim" # ... $cmd foo $cmd bar |
Das hat den Vorteil, dass man den Befehl bei Bedarf nur an einer Stelle ändern muss. Möchte man den Befehl nun um Parameter erweitern, dann geht das im einfachsten Fall gerade noch:
1 | cmd="vim -p" |
Gemäß den Regeln des „word splitting“ werden hieraus die beiden
Argumente „vim
“ und „-p
“.
Was aber, wenn man den Befehl so erweitern möchte, dass er eigentlich Anführungszeichen benötigen würde? Folgendes Beispiel funktioniert nicht:
1 2 | # Falsch! cmd="vim -c \"set number\"" |
Der Knackpunkt ist, dass die inneren Anführungszeichen hier nicht wie vielleicht erwartet ausgewertet werden. Es entsteht folgende Liste an Argumenten:
vim -c "set number"
Auch der Aufruf mit äußeren Anführungszeichen über
1 | "$cmd" foo |
schlägt fehl – in diesem Fall wird versucht, „vim -c "set number"
“
aufzurufen und zwar als ein einziges Argument gesehen. Eine Datei mit
dem Namen „vim -c "set number"
“ gibt es natürlich nicht.
Man müsste den String eigentlich zweimal auswerten, um die inneren
Anführungszeichen zu berücksichtigen (das ginge mit „eval
“), was aber
fehleranfällig ist und aus guten
Gründen 🇬🇧 vermieden werden muss.
Der bevorzugte Weg, das Problem zu lösen, ist, eine Funktion zu definieren und diese statt des eigentlichen Befehls aufzurufen:
1 2 3 4 5 6 7 8 9 10 | #!/bin/bash cmd() { vim -c "set number" "$@" } # ... cmd foo |
Funktionen sind dafür gedacht, Code zu enthalten – Variablen sollen Daten enthalten. Darüberhinaus ermöglicht dieser Ansatz auch komplexere Befehle, die Pipes oder Umleitungen enthalten:
1 2 3 4 5 6 7 8 9 10 | #!/bin/bash cmd() { du "$1" | sort -n > /tmp/verzeichnisgroessen } # ... cmd foo |
Lesenswertes dazu:
Befehle wiederholen: Stapelverarbeitung (Batch Processing)¶
Das Ausführen derselben Operation auf verschiedene Dateien ist generell nützlich, wenn das jeweilige Programm dies von Haus aus nicht beherrscht. Allgemeines Schema:
for i in *.ALT; do PROGRAMM "$i" -o "`basename "$i" .ALT` .NEU"; done
Hier findet vor dem ersten Semikolon die Auswahl der Dateien (*.ALT
) im Arbeitsverzeichnis der Shell statt. Alles nach dem "do
" und bis zum zweiten Semikolon ist die spezifische Anwendung eines Programms (PROGRAMM
) mit dessen jeweiliger Befehlssyntax. "$i
" (oder "$f
", siehe unten) stellen die zu verwendeten Dateien dar. Beispiele:
Entpacken aller .zip-Dateien im aktuellen Verzeichnis:
for f in *.ZIP; do unzip “$f”; done
Konvertieren aller .jpg-Bilder im aktuellen Verzeichnis:
for f in *.jpg; do convert "$f" -background white -chop 100x0+1320+0 -splice 100x0+1320+0 "${f%}_sauber.jpg"; done
Für eine Lösung in Kombination mit find siehe Audiodateien umwandeln (Abschnitt „Mehrere-Dateien-auf-einmal-umwandeln-mit-Unterverzeichnissen“).
Tabellen zur Weiterverarbeitung umwandeln¶
Nicht selten kommt es vor, dass Tabellen zur bündigen Darstellung keine Tabulatorzeichen verwenden, sondern der Zwischenraum mit unterschiedlich vielen Leerzeichen aufgefüllt wird (Beispiel: die Ausgabe von ls -l
). Um diese beispielsweise mit cut
weiterverarbeiten zu können, benötigt man ein Format, das zwischen den Feldern je genau einen Feldtrenner enthält. Abhilfe schafft hier der folgende Befehl:
tr -s "[:space:]"
Beispiel, mit dem man auswerten kann, an welchen Tagen in einem Verzeichnis Änderungen vorgenommen wurden:
ls -l --time-style=long-iso | tr -s "[:space:]" | cut -f6 -d" " | sort -u
Zuletzt ausgeführten Befehl noch einmal erweitert ausführen¶
Häufig kommt es vor, dass man bei dem Aufruf eines Kommandos das sudo vergisst, z.B.:
apt update
Das vorangegangene Kommando kann man mittels !!
noch einmal erweitert aufrufen. Um das "apt update
" noch einmal mit sudo aufzurufen, muss man nur folgendes eingeben:
sudo !!
Auf letzten Parameter des letzten Befehls zugreifen¶
Mittels Alt + . kann man auf den letzten Parameter des zuletzt ausgeführten Kommandos zugegriffen werden. Ein einfacher Anwendungsfall ist z.B. das Erstellen, ausfühbar Machen und Ausführen eines neuen Skripts (hier foo.sh):
vi foo.sh
Dann schreibt man "chmod +x
" und drückt
Alt +
. . Dieses führt zu
chmod +x foo.sh
Dann schreibt man "./
" und drückt
Alt +
. . Dieses führt zu
./foo.sh
Ein ähnliches Verhalten kann man durch die Variable "$_
" erreichen:
vi foo.sh chmod +x $_ ./$_
Zählung der Array-Indizes bei eins beginnen¶
Wenn man die Werte eines Array in einem Stück einliest, dann beginn die Zählung der Indizes mit 0. In einigen Fällen kann es jedoch auch sinnvoll sein, diese Zählung bei 1 beginnen zu lassen. Hier können zwei Methoden verwendet werden, um dies zu erreichen:
unset Arrayname Arrayname[0]= Arrayname+=(all diese sieben Elemente werden einzeln angefügt)
In diesem Fall wird zunächst der Index 0 mit einem leeren (oder beliebigen) Wert belegt, und die übrigen Indizes dann aufsteigend angehängt.
var='all diese sieben Elemente werden einzeln angefügt' readarray -O1 -d' ' Arrayname <<< $var
Hierbei wird eine belegte Variable in einen Array eingelesen, die Option -O1
sorgt dafür, dass der Index ab 1 hochgezählt wird.
Links¶
ShellCheck - Befehlszeilenwerkzeug zur Analyse von Shell- und Bash-Skripten
Pitfalls 🇬🇧 - Fallstricke auf Greg's Wiki
Useless Use of Cat Award 🇬🇧 (Original existiert nicht mehr, deshalb hier die letzte Version von 2014 bei archive.org; die Seite wird auch hier gehostet)