Shell-Programmierung

Die Bourne Again Shell ist ein POSIX-konformer Interpreter für Programme wie dieses:

:(){ :|: & };:

Wenn die bash(1) als interaktive Shell in einer Terminal-Emulation startet, dann liest sie nur die globale Konfiguration /etc/bash.bashrc und die benutzereigene ~/.bashrc.

Beispiel

Ein Skript besteht aus einer Folge von Kommandos. Die erste Zeile kann (und sollte) einen speziellen Kommentar (shebang) enthalten, welches den zu verwendenen Interpreter enthält.

hello.sh
#!/usr/bin/env bash if [ $# -eq 0 ] then echo Hello, World\! else for arg do echo Hello, $arg\! done fi

Ausführbar machen und ausführen mit:

chmod +x hello.sh && ./hello.sh
Hello, World!

Argumente übergeben

Ein Kommando besteht aus dem Namen einer Funktion oder eines Programms gefolgt von einer Liste mit Argumenten. In Shellskripten wird die Argumentliste in den Positionsparametern gespeichert. In C-Programmen greift man auf die Argumentliste über den Parameter argv der Funktion main zu.

Programm Option Option Parameter Operand
Kommandozeile mkdir -p -m 755 ~/foo/bar
Positionsparameter $0 $1 $2 $3 $4
Argumentliste in C argv[0] argv[1] argv[2] argv[3] argv[4]

Ein Argument, das mit einem - beginnt, nennt man Option. Manche Optionen haben wiederum Parameter (optarg). Sie steuern üblicherweise das Verhalten des Programms. Das Programm getopts extrahiert alle Optionen aus der Argumentliste. Übrig bleiben die Operanden, mit denen das Programm arbeitet.

Umgebung auslesen

Die Shell verwaltet (wie jeder Prozess) eine Liste von Variablen, die sogenannte Umgebung (environment). Exportierte Variablen vererbt sie an Kindprozesse. Folgende Variablen stehen in jedem Skript zur Verfügung:

0…9
Positionsparamter für Argumente
*
Alle Positionsparamter als Token, wie "$1 $2 …"
@
Alle Positionsparamter als Liste, wie "$1" "$2" …
#
Anzahl der Positionsparamter
?
Exit-Status des letzten Kommandos
-
Flags beim Aufruf der Shell
$
Eigene Prozessnummer
!
Prozessnummer des zuletzt gestarteten Kindprozesses
_
Das letzte übergebene Argument
RANDOM
Pseudo-Zufallszahl (nur Bash)

Einige Umgebungsvariablen setzt bereits das Anmeldeprogramm login(1), andere werden durch Konfigurationsdateien festgelegt und manche dynamisch angepasst, zum Beispiel wenn sich die Größe des Terminal-Fensters ändert. Viele Programme wie zum Beispiel man(1) oder crontab(1) honorieren bestimmte Variablen, um ihr Verhalten anzupassen.

Umgebungsvariablen
Variable Beispiel Quelle Beschreibung
LOGNAME phrank /etc/passwd Benutzername (System V)
USER phrank " Benutzername (BSD)
HOME /home/phrank " Benutzerverzeichnis
SHELL /bin/bash " Kommandointerpreter
LANG de_DE.UTF-8 pam_env.so Internationalisierung
PATH /usr/local/bin:… /etc/profile Programmsuchpfad
PS1 \u@\h:\w\$ /etc/bash.bashrc Eingabeaufforderung
PAGER less ~/.bashrc Anzeigeprogramm
EDITOR joe " Texteditor
MAILER thunderbird " Elektropost
BROWSER firefox " Navigator
TERM xterm xterm Terminaltyp
LINES 25 " Zeilenhöhe
COLUMNS 80 " Spaltenbreite
PWD $(pwd) bash Arbeitsverzeichnis

Variablen substituieren

Eine Variable hat einen Namen und einen Wert. Eine Reihung (array) verhält sich wie eine Streutabelle (hashmap) mit numerischen Schlüsseln. Die Bash erlaubt seit Version 4 auch Zeichenketten als Schlüssel.

null=
path="/foo/bar.txt"
array=(foo bar fnord)
declare -A hash=([foo]=fnord [bar]=snafu)

Das Symbol $ steht für Substitution. Findet die Shell diesen Operator, expandiert sie die entsprechende Variable nach den unten beschriebenen Regeln, bevor sie das Kommando ausführt.

Variable Beispiel Ausgabe Expansion
$Name $path /foo/bar.txt Wert der Variable
${Name} ${path}_ /foo/bar.txt_ Dito
${Name:Pos} ${path:4} bar.txt Ab dem sovielten Zeichen
${Name:Pos:Len} ${path:4:3} bar Ab dem sovielten Zeichen mit Länge
${#Name} ${#path} 12 Länge des Werts
Ersetzung
${Name#Prefix} ${path#*/} foo/bar.txt Präfix abschneiden
${Name##Prefix} ${path##*/} bar.txt Präfix gierig abschneiden (basename)
${Name%Suffix} ${path%/*} /foo Suffix abschneiden (dirname)
${Name%%Suffix} ${path%%/*} Suffix gierig abschneiden
${Name/Glob/Repl} ${path/o/l} /flo/bar.txt Einmal ersetzen
${Name//Glob/Repl}${path//o/u} /fuu/bar.txt Alle ersetzen
${Name/#Glob/Repl}${path/#\/foo/fnord} fnord/bar.txt Vorne ersetzen
${Name/%Glob/Repl}${path/%txt/jpg} /foo/bar.jpg Hinten ersetzen
Null-Variable
${Name:+Text} ${path:+hello} Alternative oder gar nichts.
${Name:-Text} ${path:-hello} /foo/bar.txt Defaultwert
${Name:=Text} ${null:=hello} hello Defaultwert mit Zuweisung an Variable
${Name:?Text} ${null:?hello} hello Meldung auf stderr; beendet die Shell
Variablennamen
${!Prefix*} ${!p*} path Variablennamen mit Präfix als Token
${!Prefix@} ${!p@} path Variablennamen mit Präfix als Liste
Reihung
${Array[*]} ${array[*]} foo bar fnord Werte als Token
${Array[@]} ${array[@]} foo
bar
fnord
Werte als Liste
${!Array[*]} ${!array[*]} 1 2 3 Schlüssel als Token
${!Array[@]} ${!array[@]} 1
2
3
Schlüssel als Liste
${#Array[@]} ${#array[@]} 3 Anzahl der Elemente
${#Array[Key]} ${#array[2]} 5 Länge des Elements
${Array[Key]} ${array[2]} fnord Wert des Elements
Kommando-Substitution
`Command` `uname` Linux Ausgabe des Kommandos, veraltet
$(Command) $(uname) Linux Ausgabe des Kommandos
<(Command) <(uname) Ausgabe von Kommando Umleiten
>(Command) >(uname) Ausgabe zu Kommando Umleiten
Arithmetik
$((Expression)) $((x + 3)) 8 Arithmetische Erweiterung
Wert-Expansion (Klammer, Tilde, Datei)
{Char..Char} foo{a..c} fooa foob fooc Kombinationen mit einer Spanne von Zeichen
{String,…} foo{bar,baz} foobar foobaz Kombinationen mit einer Liste von Zeichenketten
~ ~ /home/bob Benutzerverzeichnis des Prozessbesitzers
~User ~alice /home/alice Benutzerverzeichnis eines anderen Benutzers
* foo* foo foo.txt Beliebige Zeichenkette
? ?oo foo xoo zoo Beliebiges Zeichen
[List] [a-m,x,y]oo foo xoo Ein Zeichen aus einer Gruppe

Ablauf steuern

Command && Command || Command
Ternärer Operator
if [[ Expression ]]; then …; [ elif [[ Expression ]]; then …; else …; ] fi
Verzweigung
case Name in Glob) …;; esac
Mehrfachverzweigung
while [[ Expression ]]; do …; done
Schleife mit Forsetzungsbedingung
until [[ Expression ]]; do …; done
Schleife mit Abbruchbedingung
for Name; do …; done
Iteration über Positionsparameter
for Name in List; do …; done
Iteration über Liste
for ((Expr1; Expr2; Expr3)); do …; done
Iteration wie in C
break
Verlässt die for-, while- oder until-Schleife
continue
Setzt die Schleife beim nächsten Durchlauf fort
Name() { … }
Definiert eine Funktion
return ExitCode
Verlässt die Funktion
trap Command Signal
Registriert Funktion für Signalbehandlung

Logik auswerten

[[ Expression ]]
Operand Operator Operand Wahr, wenn
Lexikographisch
-z String Zeichenkette leer ist
-n String Zeichenkette mindestens ein Zeichen enthält
String = String Zeichenketten identisch sind
String == String Dito
String != String Zeichenketten sich unterscheiden
String < String Linke lexikographisch kleiner als rechte Zeichenkette
String > String Linke lexikographisch größer als rechte Zeichenkette
Arithmetisch
! Expression Negation, Ausdruck ist falsch
Number -eq Number Gleichheit
Number -ne Number Ungleichheit
Number -lt Number Linke Zahl kleiner als rechte Zahl
Number -le Number Linke Zahl kleiner oder gleich rechte Zahl
Number -gt Number Linke Zahl größer als rechte Zahl
Number -ge Number Linke Zahl größer oder gleich rechte Zahl

Arithmetisch rechnen

(( Expression ))
Operand Operator Operand Wahr, wenn
(…) Präzedenz
Number = Number Zuweisung
Number += Number Zuweisung mit Addition
++ Number Increment
-- Number Decrement
~ Number Bitkomplement
Number & Number Bitweise UND verknüpfen
Number ^ Number Bitweise ODER verknüpfen
Number ** Number Exponent
Number * Number Multiplikation
Number / Number Division
Number % Number Modulo
Number + Number Addition
Number - Number Subtraktion

Datenströme umleiten

Eine Pipe verbindet die Standardausgabe eines Prozesses mit der Standardeingabe des Nächsten. Typische Unix-Programme sind dafür ausgelegt, entweder als Quelle (cat, ls), als Filter (grep, sort) oder als Senke (less, lpr) zu agieren.

Source | Filter | Sink
(Standard)eingabe
Command [n]< File Aus Datei lesen
Command [n]<< Delimiter Bis zum Trenner lesen (Here-Dokument)
Command [n]<<- Delimiter Dito, aber Einrückung ignorieren
Command [n]<&[n] Deskriptor duplizieren
Command [n]<&- Deskriptor schließen
(Standard)ausgabe
Command [n]> File In Datei schreiben
Command [n]>| File Datei überschreiben, selbst wenn noclobber gesetzt ist
Command [n]>> File An Datei anhängen
Command [n]>&[n] Deskriptor duplizieren
Command [n]>&- Deskriptor schließen
Command [n]<> File Deskriptor zum Lesen und Schreiben öffnen
0 Standardeingabe
1 Standardausgabe
2 Standardfehlerausgabe

Standardfehlerausgabe im Pager betrachten

Command 2>&1 | less

Der Operator > leitet die Ausgabe von Kommandos und Funktionen in Dateien um.

for i in {1..10}; do echo "Hello $i"; done > File

Umgekehrt leitet < den Inhalt von Dateien in die Standardeingabe um. Um zum Beispiel eine Datei zeilenweise zu lesen:

while read line; do echo $((++n)) $line; done < File

Und so leitet man die Standardausgabe eines Kommandos in die Standardeingabe um:

while read line; do echo $line; done < <(Command)

Umleitung eines Here-Dokuments in ein Kommando:

cat <<- .
the quick brown fox jumps
over the lazy dog
.

Terminal steuern

Per Kontrollsequenz lassen sich die Position der Eingabemarke sowie typografische Merkmale wie Fettdruck, Unterstreichungen, Vorder- und Hintergrundfarben ändern.

echo -e "\e[32;1mHallo, Welt\e[0m"
Hallo, Welt
Code Name ASCII Beschreibung
\a BEL 7 Alarm
\b BS 8 Backspace
\t HT 9 Horizontaler Tabulator
\n LF 10 Newline
\v VT 11 Vertikaler Tabulator
\f FF 12 Form feed
\r CR 13 Carriage return
\e ESC 27 Escape
\\ 92 Backslash ausgeben
\c Weitere Ausgabe unterdrücken
\0{0…7}{0…7} Oktal
\x{0…f}{0…f} Hexadezimal
\u{0…f}..{0…f} Unicode
\U{0…f}......{0…f} Unicode erweitert

Der CSI (ESC+[) leitet Befehle zur Steuerung der Eingabemarke ein.

Sequenz Name Beschreibung
\e[UpA CUU Schritt nach Oben
\e[DownB CUD Schritt nach Unten
\e[RightC CUF Schritt nach Rechts
\e[LeftD CUB Schritt nach Links
\e[LineE CNL Sprung an Zeilenanfang weiter unten
\e[LineF CPL Sprung an Zeilenanfang weiter oben
\e[ColumnG CHA Setzen auf Spalte
\e[Row;ColumnHCUP Setzen auf absolute Position
\e[0J ED Löschen ab Eingabemarke bis Seitenende
\e[1J Löschen ab Seitenanfang bis Eingabemarke
\e[2J Löschen der ganzen Seite
\e[0K EL Löschen ab Eingabemarke bis Zeilenende
\e[1K Löschen ab Zeilenanfang bis Eingabemarke
\e[2K Löschen der ganzen Zeile
\e[LinesS SU Seite nach oben rollen
\e[LinesT SD Seite nach unten rollen
\e[Row;ColumnfHVP Position der Eingabemarke setzen
\e[Codem SGR Farben ändern, siehe unten
\e[s SCP Position der Eingabemarke merken
\e[u RCP Position der Eingabemarke wiederherstellen
\e[?25l DECTCEM Eingabemarke verbergen
\e[?25h DECTCEM Eingabemarke anzeigen

SGR-Sequenzen ändern Stil und Farbe der Schrift.

Sequenz Berechnung Werte Farbschema
\e[1m … \e[22m Fett
\e[2m … \e[22m Dünn
\e[3m … \e[23m Kursiv
\e[4m … \e[24m Unterstrichen
\e[5m … \e[25m Blinkend
\e[6m … \e[26m Flackernd
\e[7m … \e[27m Invers
\e[8m … \e[28m Unsichtbar
\e[9m … \e[29m Durchgestrichen
\e[30…37;40…47m Blue*4+Green*2+Red+30 8 ANSI Farben
\e[38;5;Fore;48;5;Backm Red*36+Green*6+Blue+16 0…5 216 SGR-Farben
232+{0…23} 0…23 24 SGR Graustufen
\e[38;2;Red;Green;Blue;m 0…255 16 Millionen Echtfarben
\e[48;2;Red;Green;Blue;m 0…255 16 Millionen Hintergründe
#BGRFarbe
30000Schwarz
31001Rot
32010Grün
33011Gelb
34100Blau
35101Magenta
36110Cyan
37111Weiß

OSC-Sequenzen (ESC+]) können unter anderem das Icon und den Titel eines grafischen Terminals setzen.

\e]0;Icon Title\e\\
\e]1;Icon\e\\
\e]2;Title\e\\

Praktischerweise führt die Bash bei der Anzeige der Eingabeaufforderung (prompt) immer das in der Variablen PROMPT_COMMAND gespeicherte Kommando aus, so dass man zum Beispiel immer das Arbeitsverzeichnis ausgeben kann.

PROMPT_COMMAND='echo -ne "\e]2;$LOGNAME@$HOSTNAME:$PWD\e\\"'

Fortgeschrittene Terminal-Emulationen (VT240 aufwärts) unterstützen das DEC SIXEL Format und die ReGIS Vektorsprache.

apt install mlterm libsixel-bin
mlterm
img2sixel debian.png

Literatur

  1. Robbins, Beebe: Klassische Shell-Programmierung, O'Reilly
  2. Ken Thompson: The UNIX Command Language, Bell Labs, 1976
  3. David Pashley: Writing Robust Bash Shell Scripts
  4. Mendel Cooper: Advanced Bash-Scripting Guide
  5. Joshua Levy: The Art of Command Line