[[Vorlage(Fortgeschritten)]] {{{#!vorlage Wissen [:Terminal: Ein Terminal öffnen] [:Editor: Einen Editor öffnen] }}} [[Inhaltsverzeichnis(2)]] [[Bild(Wiki/Icons/terminal.png, 48, align=left)]] 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: {{{#!vorlage Befehl cat dateiname | grep "suchbegriff" }}} Schon besser: {{{#!vorlage Befehl grep "suchbegriff" /dev/null if [ $? -ne 0 ] ; then }}} ist genauso wenig optimal wie {{{#!code bash if grep "suchbegriff" datei > /dev/null ; then }}} weil es dafür "-q" gibt, welches die Suche beim ersten Treffer abbricht. {{{#!vorlage Befehl 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 [:Shell/dd: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. {{{#!code bash sudo dd if/dev/sda count=1| hexdump -C }}} Beispiele: Andruck des MBR der Festplatte '''/dev/sda''': {{{#!vorlage befehl 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: {{{#!vorlage befehl sudo hexdump -s512 -n512 -C /dev/sda }}} = Tricks = == Leeren einer Datei == Man kann eine Datei namens '''dateiname''' mittels {{{#!vorlage Befehl > 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: {{{#!code bash # 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 {{{#!code bash 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. {{{#!vorlage Tabelle <-2 rowclass="titel"> 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 === {{{#!code bash 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 === {{{#!code bash 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" }}} [[Anker(read-Variable)]] === `read` liest die Zeilen aus einer vorher befüllten Variable === {{{#!code bash 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: {{{#!code bash 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 {{{#!vorlage Befehl tar cf - | ssh @ "cd && tar xf -" }}} `` vom aktuellen Rechner ins `` auf dem `` als `` 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. {{{#!vorlage Befehl tar cf - | buffer -m 10m | ssh @ "cd && 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: {{{#!code bash #!/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: {{{#!code bash 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'': {{{#!code bash # 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 {{{#!code bash "$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 [http://mywiki.wooledge.org/BashFAQ/048 aus guten Gründen] {en} vermieden werden muss. Der bevorzugte Weg, das Problem zu lösen, ist, eine Funktion zu definieren und diese statt des eigentlichen Befehls aufzurufen: {{{#!code bash #!/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: {{{#!code bash #!/bin/bash cmd() { du "$1" | sort -n > /tmp/verzeichnisgroessen } # ... cmd foo }}} Lesenswertes dazu: * [http://mywiki.wooledge.org/BashFAQ/050 Eintrag in Greg's Wiki zu dem Thema] {en} * [http://www.gnu.org/software/bash/manual/bashref.html#Shell-Functions Funktionen im Bash-Handbuch] {en} == 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: {{{#!vorlage Befehl 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: {{{#!vorlage Befehl for f in *.ZIP; do unzip “$f”; done }}} * Konvertieren aller '''.jpg'''-Bilder im aktuellen Verzeichnis: {{{#!vorlage Befehl 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#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: {{{#!vorlage Befehl tr -s "[:space:]" }}} Beispiel, mit dem man auswerten kann, an welchen Tagen in einem Verzeichnis Änderungen vorgenommen wurden: {{{#!vorlage Befehl 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.: {{{#!vorlage Befehl 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: {{{#!vorlage Befehl sudo !!}}} == Auf letzten Parameter des letzten Befehls zugreifen == Mittels [[Vorlage(Tasten, "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): {{{#!vorlage Befehl vi foo.sh}}} Dann schreibt man "`chmod +x `" und drückt [[Vorlage(Tasten, "Alt+.")]]. Dieses führt zu {{{#!vorlage Befehl chmod +x foo.sh}}} Dann schreibt man "`./`" und drückt [[Vorlage(Tasten, "Alt+.")]]. Dieses führt zu {{{#!vorlage Befehl ./foo.sh}}} Ein ähnliches Verhalten kann man durch die Variable "`$_`" erreichen: {{{#!vorlage Befehl 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: {{{#!vorlage Befehl 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. {{{#!vorlage Befehl 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 * [http://mywiki.wooledge.org/BashPitfalls Pitfalls] {en} - Fallstricke auf Greg's Wiki * [http://web.archive.org/web/20140415205935/http://partmaps.org/era/unix/award.html Useless Use of Cat Award] {en} (Original existiert nicht mehr, deshalb hier die letzte Version von 2014 bei archive.org; die Seite wird auch [http://porkmail.org/era/unix/award.html hier] gehostet) # tag: Shell, Programmierung