ubuntuusers.de

Du betrachtest eine alte Revision dieser Wikiseite.

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:

Wiki/Icons/terminal.png 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 | 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 

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 

  • 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)

Diese Revision wurde am 19. September 2016 11:17 von BillMaier erstellt.
Die folgenden Schlagworte wurden dem Artikel zugewiesen: Programmierung, Shell