bash_3

Einführung in Bash Skripting

Einstieg in die Bash-Shell - Teil 3

Textdateien

Standards zur Kodierung von Zeichen

ASCII-Code

Der ASCII-Code (American Standard Code for Information Interchange) ist ein (alter) Standard um Zeichen zu kodieren. ASCII beschreibt einen Sieben-BitCode. D.h. es können $2^7 = 128$ Zeichen mit ASCII-Code dargestellt werden. Steuerzeichen (wie z.B.: Zeilenvorschub) sind im ASCII-Code ebenfalls erhalten.

Beispiel: Buchstabe A : 1000001

Durch Hinzunahme des 8-ten Bit (des Bytes) können nochmal 128 Zeichen dargestellt werden. Diese können fur länderspezifische Zeichen genutzt werden, wie z.B. ISO 8859-1 (Latin-1). In unterschiedlichen Ländern sind unterschiedliche Standards üblich. Dies führt zu Inkompatibilitäten zwischen den verschiedenen Ländercodierungen bzw. Computersystemen (z.B. bei deutschen Umlauten).

Unicode

Unicode ist ein internationaler Standard, mit dem Ziel spezielle Zeichen aller Sprachen (Schriftkulturen) zu umfassen. Unicode weist jedem Zeichen eine eindeutige Nummer (Unicode-Wert) zu. Durch standardisierte Kodierungen der Unicode-Werte, wie UTF-8, sollen die Inkompatibilitäten (falsche Darstelllungen) vermieden werden.

für Interessierte

Für den Unterschied zwischen Unicode und Implementierungen wie UTF-8, siehe z.B. https://stackoverflow.com/questions/643694/what-is-the-difference-between-utf-8-and-unicode

Text-Dateien

Text-Daten können als Dateien (Files) im ASCII-Code oder UTF-8 auf einem Rechner gespeichert werden. Diese Dateien können ohne größeren Schwierigkeiten zwischen verschiedenen Computersystemen ausgetauscht werden und dort angezeigt und weiterverarbeitet werden. Dabei ist darauf zu achten, dass die Kodierung richtig erkannt bzw. eingestellt wird. Desweiteren können jedoch Probleme bei der Handhabung des Zeilenendes auftreten. Windows verwendet zur Markierung des Zeilenendes die Kombination Wagenrücklauf-Zeilenvorschub. Unix und Linux nutzen nur den Zeilenvorschub.

Beispiele für Text-Dateien: CSV, Emails, HTML-Webseiten.

file command

Das file Kommando führt mehrere Tests durch, um den Typ einer Datei zu klassifizieren. Dabei wird auch versucht die Kodierung von Textdateien genauer zu bestimmen. Mehr siehe man file.

In [30]:
file countries.txt 
echo
file ~/bin/backup.sh
countries.txt: ASCII text

/home/chris/bin/backup.sh: Bourne-Again shell script, ASCII text executable
In [25]:
file ../bash_3.ipynb
echo
file /home/chris/tmp/side_by_side_without_boarder_nearest.png
../bash_3.ipynb: UTF-8 Unicode text, with very long lines

/home/chris/tmp/side_by_side_without_boarder_nearest.png: PNG image data, 1920 x 1080, 8-bit/color RGBA, non-interlaced

MIME

MIME ist ein Standard zur Deklaration von Dateiinhalten (bzw. Dateitypen), der ürsprünglich für Emails entwickelt wurde, mehr hierzu z.B. in https://wiki.ubuntuusers.de/MIME-Typ/

In [26]:
file --mime ../bash_3.ipynb
echo
file -i /home/chris/tmp/side_by_side_without_boarder_nearest.png
../bash_3.ipynb: text/plain; charset=utf-8

/home/chris/tmp/side_by_side_without_boarder_nearest.png: image/png; charset=binary

Editoren

Übung

Wählen Sie einen Kommandozeilen-Text-Editor und arbeiten Sie sich in diesen ein. Recherchieren Sie hierzu ein geeignetes Tutorial. Gängige Text-Editoren für die Kommandozeile sind:

Shell Skripte

Shell-Skripte sind Programmcode gespeichert in Text-Dateien für die Shell-Umgebung. Shell-Skripte werden interpretiert, nicht kompiliert. D.h. während der Ausführung liest der Interpreter das Skript Zeile für Zeile und führt die Komandos aus. Ein Kompiler dagegen übersetzt ein Programm in ausführbaren Maschinencode, d.h. in eine Form, die vom Computer direkt ausgeführt werden kann.

Vorteile von Shell Skripen:

  • Für Shell-spezifische Aufgaben gut geeignet, bei denen gängige Komandozeilen-Tools bzw. Kommandozeilenprogramme eingesetzt werden.

Nachteile:

  • Komplexe Syntax, teilweise subtile Fallstricke (wie beim Erzeugen von Subshells) und somit schwierig zu erlernen.
  • keine richtigen Datentypen.

Für komplexere Skripte und mehr eignen daher sich andere Skriptsprachen, wie Python, deutlich besser.

Shebang

In Skripte, wie z.B. Bash- oder Python Skripte, wird in der Regel in der ersten Zeile der zu verwendende Interpreter mit einem Shebang angegeben. Dieser beginnt mit #! gefolgt von der Pfadangabe zum Interpreter, z.B. #!/bin/bash.

Beachten Sie, dass es aber gute Praxis ist, den Shebang mit dem env-Kommando zu verwenden. Mit env läuft ein Programm in einer modifierten Umgebung. Typischerweise liegt env im Verzeichnis /usr/bin/, sodass folgende Beispiele für den Shebang in der ersten Zeile eines Skriptes sinnvoll sind:

  • #!/usr/bin/env bash - Bash-Skripte
  • #!/usr/bin/env sh - POSIX konforme Shell-Skripte
  • #!/usr/bin/env python - Python Skripte

env verwendet die PATH Umgebungsvariable (siehe unten), um den passenden Interpreter (genauer: das Interpreterprogramm) zu finden.

Beispielskript "Hallo Welt":

In [5]:
echo '#!/usr/bin/env bash
echo Hallo Welt!' > hw.sh
chmod u+x hw.sh
In [6]:
./hw.sh
Hallo Welt!

Hinweis

Shell-Skripte haben oft die Endung .sh. Dies ist aber optional.

Spezielle Variablen

Folgende spezielle Variablen gibt es in Bash bzw. Bash-Skripten

  • $0 Aufruf Name des Skripts (inkl. Pfad)
  • $1 bis $9 sind Argumente des Skripts oder der Funktion (beim Aufruf übergeben).
  • $@ sind alle Argumente als Liste (Listen/Array werden weiter unten behandelt)
  • $# Anzahl der Argumente
  • $? - Rückgabecode des vorherigen Kommandos
  • $$ - Prozeß-ID (PID) des laufenden Skiptes (siehe unten)
  • !! - Vollständiges letztes Kommando inkl. der Argumente. Eine gängige Anwendung ist, falls das letze Kommando aufgrund von fehlenen root-Rechten nicht ausgeführt werden konnte: sudo !!
  • $_ - Letztes Argument des letzten Kommandos. In einer interaktiven Shell bekommt man dies auch über die Tastenkombination Esc gefolgt von ..
  • $! beinhaltet die Prozess ID, des zuletzt im Hintergrund ausgeführten Prozesses.

Beispiele:

In [4]:
wc not-present 2>/dev/null # fehlermeldung wird unterdrückt
echo The exit code of last command is: $?
The exit code of last command is: 1

Beachten Sie, dass Fehlermeldungen von Kommandos (Programmen) mit Zahlen codiert werden. Dabei steht die Null für "Kein-Fehler":

In [6]:
ls >/dev/null
echo The exit code of last command is: $?
The exit code of last command is: 0
In [14]:
sleep 3 & # & bewirkt Ausführung im Hintergrund
echo $!
# Hier werden Prozess IDs ausgegeben - siehe unten
[1] 12853
12853
In [228]:
# Hinweis: Wir müssen die $-Zeichen im here-Document escapen
#   damit die bash keine variable substitution vornimmt! 

cat > example_script.sh << EOF
#!/usr/bin/env bash
echo Der Name diese Skripts mit Pfad beim Aufruf: \${0}
echo Mit folgendem ersten Argument: \${1}
echo Das zweite Argument ist: \${2}
echo Die Anzahl aller Argumente ist \$# 
EOF

chmod u+x example_script.sh # ausführbar machen!
./example_script.sh foo bar
Der Name diese Skripts mit Pfad beim Aufruf: ./example_script.sh
Mit folgendem ersten Argument: foo
Das zweite Argument ist: bar
Die Anzahl aller Argumente ist 2

Prozesse

Ein Prozess) (aus dem Lateinischen procedere, „vorwärts gehen“) ist ein laufendes Programm. Dabei kann dasselbe Programm mehrfach quasi-parallel ausgeführt werden. Prozesse besitzen eine eindeutige Identifikationsnummer (ID), die Prozess-ID (PID).

Hinweis: Prozesse werden später ausführlicher behandelt.

In [18]:
# So erhält man die PID des Bash-Prozesses der Shell
echo -e  "shell:\n" '$='$$ 'BASHPID='$BASHPID
shell:
 $=23432 BASHPID=23432

Beachte (aus https://unix.stackexchange.com/questions/484442/how-can-i-get-the-pid-of-a-subshell)

  • Variable $$: Expands to the process ID of the shell. In a () subshell, it expands to the process ID of the current shell, not the subshell.
  • Variable BASHPID: Expands to the process ID of the current bash process. This differs from $$ under certain circumstances, such as subshells that do not require bash to be re-initialized.
In [19]:
#subshells
echo -e $(echo "subshell\n" '$='$$ 'BASHPID='$BASHPID)
subshell
 $=23432 BASHPID=4659

Wenn ein Shell-Skript ausgeführt wird, wird ein neuer Prozess gestartet. Die aufrufende Shell ist der Eltern-Prozess (parent process) und der Prozess des Shell-Skriptes der Kind-Prozess (child process).

In [22]:
# Auch der Shell-Prozess hat ein Parent
# So erhält man die PID des Parent, die PPID
echo $PPID
12744

Umgebungsvariablen

Umgebungsvariable (Environment Variables) sind besondere Variablen, die von Prozessen für unterschiedliche Dinge verwendet werden können (Beispiele später). Umgebungsvariablen werden bei der Prozess-Erzeugung an die Kinderprozesse vererbt. Die Kinderprozesse erhalten eine Kopie der Umgebung des Elternprozesses.

Das Kommando printenv dient dazu die gesetzen Umgebungsvariablen anzuzeigen.

In [36]:
printenv | grep -i python
VIRTUAL_ENV=/home/chris/python-virtal-envs/py3.8.0
PATH=/home/chris/python-virtal-envs/py3.8.0/bin:/home/chris/python-virtal-envs/py3.8.0/bin:/home/chris/.local/bin:/home/chris/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

Mit dem Kommando printenv können Sie direkt die Werte von Umgebungsvariablen erhalten, z.B.

In [43]:
printenv VIRTUAL_ENV # zeigt nur env-var an!
#die Werte von env-var sind natürlich auch per parameter substitution erhältlich:
#echo $VIRTUAL_ENV
/home/chris/python-virtal-envs/py3.8.0

Man kann die Kommandozeile als Umgebung sehen, die shell-Code ausführt. Kommandos sind dann shell build-ins oder weitere Programme, die ausgeführt werden können, d.h. in der Regel weitere Prozesse ergeben. Diese Programme, d.h. die ausführbaren Dateien, muss die shell finden. Dazu dient die Umgebungsvariable PATH.

So kann man sich anzeigen lassen, wo die shell nach den ausführbaren Programmen sucht:

In [44]:
printenv PATH
/home/chris/python-virtal-envs/py3.8.0/bin:/home/chris/python-virtal-envs/py3.8.0/bin:/home/chris/.local/bin:/home/chris/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

In der Variablen SHELL ist die Art des Shell-Interpreters (der laufenden Shell) gespeichert:

In [50]:
printenv SHELL
/bin/bash

export

Shell Skripte laufen in einem eigenen Prozess (erkennbar an der anderen PID). Wenn in der aufrufenden Shell eine Variable gesetzt wird, dann ist diese im Shell Skript nicht verfügbar (anderes als bei Subshells via $(...) oder Pipes). Das Skipt muss ja kein Bash-Skript sein, sodass Bash-Skripte hier nicht besonderes behandelt werden.

Variablen, die mit export gesetzt werden, sind dagegen (in den Child-Prozessen und somit auch) im Skript-Prozess gesetzt.

In [51]:
echo PID der aufrufenden Shell: $$
my_normal_var=10
export my_env_var=11
echo '#!/bin/bash 
echo Die PID dieses Skriptes ist $$
echo Der Parent-Prozess des Skriptes hat die PID $PPID # $(ps -o ppid= $$)
echo my_normal_var ist nicht sichtbar: $my_normal_var
echo my_env_var dagegen schon: $my_env_var
' > example_script.sh
chmod u+x example_script.sh
./example_script.sh
PID der aufrufenden Shell: 12753
Die PID dieses Skriptes ist 13716
Der Parent-Prozess des Skriptes hat die PID 12753
my_normal_var ist nicht sichtbar:
my_env_var dagegen schon: 11

Mit export setzt man also Variablen, die in allen Child-Prozessen des Parent-Prozess (hier der Shell) verfügbar sind.

Diese Variablen sind somit in der Umgebung gesetzt. Dies sieht man auch mittels:

In [52]:
env | grep my_env_var
my_env_var=11
Aufgabe

Passen Sie den Prompt (Umgebungsvaribale PS1) nach Ihrem Geschmack an. Wie dies geht, finden Sie hier: https://wiki.ubuntuusers.de/Bash/Prompt/

Hilfreich sind dabei auch

Konfigurationsdateien für die Shell

Konfigurationsdateien erlauben das Verhalten der Shell zu konfigurieren. Die wichtigsten sind .bashrc und .bash_profile; siehe z.B. https://wiki.ubuntuusers.de/Bash/bashrc/

.bash_profile wird bei login Shells (beim Start der Shell) ausgeführt. .bashrc wird bei interaktiven non-login Shells ausgeführt.

Hinweis: Die Dateien mit einem Punkt im Namen als erstes Zeichen sind sogenannte versteckte Dateien. Diese werden mit standardmäßig nicht angezeigt, z.B. ist bei ls die Option -a nötig, um diese auch aufzulisten.

Aufgabe

Den Standardeditor können Sie mit Hilfe der Umgebungsvariablen EDITOR setzen. Am besten setzt man dies in der Konfigurationsdatei .bashrc. Setzen Sie Ihren Lieblingseditor in der .bashrc. Wie lautet die entsprechende Befehlszeile?

beim Editor nano, z.B.: export EDITOR=/usr/bin/nano

Alias-Mechanismus

Mittels des Alias-Mechanismus können Sie sich Abkürzungen von Befehlen (inkl. Argumenten) erstellen.

Mit dem Befehl alias (ohne Argumente) bekommen Sie die definierten Aliase angezeigt:

In [31]:
alias
alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l='ls -CF'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'
alias openboard='flatpak run ch.openboard.OpenBoard'

Ein (etwas sinnloses) Beispiel für die Definition eines Aliases:

In [38]:
# myls ist der Alias für "ls -lrth *.txt"
alias myls_txt='ls -lrth *.txt'
In [39]:
# Jetzt kann der Alias verwendet werden:
myls_txt
-rw-r--r-- 1 chris chris    5 Feb  5  2021 f2.txt
-rw-r--r-- 1 chris chris    5 Feb  5  2021 f.txt
-rw-r--r-- 1 chris chris 7,8K Feb  6  2021 t.txt
-rw-r--r-- 1 chris chris  462 Mär 18  2021 ls-out.txt
-rw-r--r-- 1 chris chris   70 Apr 15  2021 rechnernamen.txt
-rw-r--r-- 1 chris chris  100 Apr 15  2021 countries_2.txt
-rw-r--r-- 1 chris chris   96 Apr 15  2021 countries_3.txt
-rw-r--r-- 1 chris chris  100 Apr 15  2021 countries_4.txt
-rw-r--r-- 1 chris chris  17K Okt 12 14:39 mylog.txt
-rw-rw-r-- 1 chris chris   23 Nov  2 14:29 countries_1.txt
lrwxrwxrwx 1 chris chris   13 Nov  3 08:21 countries_soft_link.txt -> countries.txt
-rw-rw-r-- 2 chris chris   79 Nov  3 14:54 countries.txt
-rw-rw-r-- 2 chris chris   79 Nov  3 14:54 countries_second_hard_link.txt

Typischerweise werden die Aliase in der regulären Datei ~/.bash_aliases definiert, sodass die Aliase dann beim Öffnen der Kommandozeile verfügbar sind. Dazu wird typischerweise diese Aliasdatei automatisch von einem bash-Konfigurationsskript (siehe unten) ausgeführt mittels source ~/.bash_aliases; zum Befehl (shell buildin) source (Abkürzung .) siehe help source oder help ..

mehr zum Alias-Mechanismus siehe https://de.wikibooks.org/wiki/Linux-Praxisbuch/_Bourne_Again_Shell#Der_Alias-Mechanismus

Funktionen

Die Struktur einer Funktion ist folgende:

funktionsname () {
    kommando_1 
    kommando_2
    ...
}

Für die Argumente gilt das gleiche wie bei den Skripten, d.h. es gibt $#, $1, etc.

zum Beispiel (Funktionen kann man auch direkt auf der Kommandozeile definieren):

In [20]:
# Beachte: Die Argumente sind nicht zwischen den Klammern ( ... ) explizit gelistet.
cd_ls () {
    cd $1 # das erste Argument des Funktion ist (wie beim Skript) $1 usw.
    echo "Das Verzeichnis $(pwd) hat folgenden Inhalt:"
    ls -l
}
In [21]:
cd_ls ../work
Das Verzeichnis /home/chris/HTW-nextcloud/Lehre/praktischeInformatik/bash/work hat folgenden Inhalt:
total 276
drwxrwxr-x 2 chris chris  4096 Okt 16 15:48 bar
-rw-rw-r-- 1 chris chris    23 Nov  2 14:29 countries_1.txt
-rw-r--r-- 1 chris chris   100 Apr 15  2021 countries_2.txt
-rw-r--r-- 1 chris chris    96 Apr 15  2021 countries_3.txt
-rw-r--r-- 1 chris chris    79 Apr 15  2021 countries_3.txt.orig
-rw-r--r-- 1 chris chris   359 Apr 15  2021 countries_3.txt.rej
-rw-r--r-- 1 chris chris   100 Apr 15  2021 countries_4.txt
-rw-r--r-- 1 chris chris    96 Apr 15  2021 countries_4.txt.orig
-rw-r--r-- 1 chris chris   382 Apr 15  2021 countries_4.txt.rej
-rw-r--r-- 1 chris chris   250 Apr 15  2021 countries_diff
-rw-r--r-- 1 chris chris   334 Apr 15  2021 countries.patch
-rw-r--r-- 1 chris chris   250 Apr 15  2021 countries.patch~
-rw-r--r-- 1 chris chris    79 Nov  2 14:29 countries.txt
-rw-r--r-- 1 chris chris   188 Apr 15  2021 duplicate_countries
-rw-r--r-- 1 chris chris    17 Feb 12  2021 example_scipt.sh
-rwxr--r-- 1 chris chris   186 Nov  2 11:01 example_script.sh
-rw-r--r-- 1 chris chris     5 Feb  5  2021 f2.txt
-rw-r--r-- 1 chris chris    11 Nov  1 16:48 File
drwxrwxr-x 2 chris chris  4096 Okt 16 15:48 foo
-rw-r--r-- 1 chris chris     5 Feb  5  2021 f.txt
-rw-rw-r-- 1 chris chris    47 Nov  1 16:48 greetings
-rw-r--r-- 1 chris chris   116 Mär  7  2021 grep
-rw-r--r-- 1 chris chris    47 Feb  8  2021 gruesse
-rwxr--r-- 1 chris chris    37 Nov  2 10:53 hw.sh
-rwxr--r-- 1 chris chris    86 Feb 18  2021 IFS_demo
-rwxr--r-- 1 chris chris    86 Feb 18  2021 IFS_demo~
-rw-r--r-- 1 chris chris   224 Mär 11  2021 inhalt_des_Verzeichnisses
-rw-r--r-- 1 chris chris 35227 Apr 14  2021 lo
-rw-r--r-- 1 chris chris   117 Apr 15  2021 logfile-example_.log
-rw-r--r-- 1 chris chris   179 Nov  2 14:29 logfile-example.log
-rw-rw-r-- 1 chris chris     6 Nov  1 16:48 longlist
-rw-r--r-- 1 chris chris   462 Mär 18  2021 ls-out.txt
drwxr-xr-x 3 chris chris  4096 Feb  5  2021 mydir1
-rw-r--r-- 1 chris chris 17353 Okt 12 14:39 mylog.txt
-rwxr--r-- 1 chris chris    58 Mär 30  2021 mytrivialscript.sh
-rw------- 1 chris chris     0 Mär 17  2021 nohup.out
-rwxr--r-- 1 chris chris  2324 Mär 30  2021 PDFWender.sh
-rwxr--r-- 1 chris chris  2326 Mär 30  2021 PDFWender.sh~
-rw-r--r-- 1 chris chris    47 Apr 14  2021 rechnernamen.log
-rw-r--r-- 1 chris chris    70 Apr 15  2021 rechnernamen.txt
-rw-r--r-- 1 chris chris     2 Feb  7  2021 result-file
-rwxr--r-- 1 chris chris   308 Mär 16  2021 signal-handler.py
-rwxr--r-- 1 chris chris   247 Mär 16  2021 signal-handler.py~
-rw-r--r-- 1 chris chris 34371 Feb  8  2021 st
drwxr-xr-x 2 chris chris  4096 Feb  9  2021 test1
-rwxr--r-- 1 chris chris     7 Feb 18  2021 test_script.sh
-rw-r--r-- 1 chris chris    12 Apr 15  2021 tmp
-rw-r--r-- 1 chris chris   116 Mär  7  2021 TODO
-rw-r--r-- 1 chris chris  7962 Feb  6  2021 t.txt
In [56]:
# Bei Funktionen kann man auch explizit das Schlüsselwort "function" angeben:
function silly_example () {
    echo "das ausführende 'Programm' bzw. Skript heißt $0"  
    echo "Zahl der Argumente $#"
}
silly_example bla blub
das ausführende 'Programm' bzw. Skript heißt /bin/bash
Zahl der Argumente 2
In [57]:
type function
function is a shell keyword

Arrays/Listen

Arrays (indexed arrays) werden durch runde Klammern erzeugt:

In [133]:
my_array=(first second third) 
# oder bei dem Inhalte aus einer Variablen mit Leerzeichen im Inhalt 
#my_array=($var) 
# oder als leeres Array per `declare -a my_array`

# die Zählung beginnt bei 0
echo drittes Element: "${my_array[2]}" 
echo Anzahl der Elemente "${#my_array[@]}"
drittes Element: third
Anzahl der Elemente 3
In [189]:
echo ${my_array[@]}
first second third
Übung

Schreiben Sie eine shell-Funktion denen Sie zwei Argumente übergeben:

  • erste Parameter soll ein Kommentar sein, der ausgegeben (angezeigt) wird.
  • der zweite Parameter soll ein Kommando sein, das ausgeführt wird.
  • falls es Änderungen des Arbeitsverzeichnisses gibt (ein oder mehrere cds als Kommando), soll nach Beenden der Funktion das ursprüngliche Arbeitsverzeichnis wieder gelten. Wie kann das einfach realisiert werden?

Beispiel:

comment_and_execute "Das wird bei ls -l ausgegeben:" "ls -l"

Das wird bei ls -l ausgegeben:
total 572
drwxr-.....
.....
In [60]:
comment_and_execute 'Das wird bei ls -l ausgegeben:' 'ls -l'
Das wird bei ls -l ausgegeben:
total 144
-rw-r--r-- 1 chris chris    79 Feb 18 09:36 countries.txt
-rw-r--r-- 1 chris chris    17 Feb 12 11:09 example_scipt.sh
-rwxr--r-- 1 chris chris   222 Apr 12 15:47 example_script.sh
-rw-r--r-- 1 chris chris     5 Feb  5 12:33 f2.txt
-rw-r--r-- 1 chris chris    11 Apr  9 11:00 File
-rw-r--r-- 1 chris chris     5 Feb  5 12:50 f.txt
-rw-r--r-- 1 chris chris    34 Apr 12 12:18 greetings
-rw-r--r-- 1 chris chris   116 Mär  7 08:31 grep
-rw-r--r-- 1 chris chris    47 Feb  8 13:10 gruesse
-rwxr--r-- 1 chris chris    37 Apr 12 15:05 hw.sh
-rwxr--r-- 1 chris chris    86 Feb 18 09:38 IFS_demo
-rwxr--r-- 1 chris chris    86 Feb 18 09:38 IFS_demo~
-rw-r--r-- 1 chris chris   224 Mär 11 09:31 inhalt_des_Verzeichnisses
-rw-r--r-- 1 chris chris     0 Apr  6 16:11 longlist
-rw-r--r-- 1 chris chris   462 Mär 18 08:54 ls-out.txt
drwxr-xr-x 2 chris chris  4096 Feb  6 16:37 myDir
drwxr-xr-x 3 chris chris  4096 Feb  5 12:51 mydir1
-rw-r--r-- 1 chris chris     0 Apr  6 16:11 mylog.txt
-rwxr--r-- 1 chris chris    58 Mär 30 12:26 mytrivialscript.sh
-rw------- 1 chris chris     0 Mär 17 13:03 nohup.out
-rwxr--r-- 1 chris chris  2324 Mär 30 14:20 PDFWender.sh
-rwxr--r-- 1 chris chris  2326 Mär 30 14:17 PDFWender.sh~
-rw-r--r-- 1 chris chris     2 Feb  7 13:33 result-file
-rwxr--r-- 1 chris chris   308 Mär 16 16:57 signal-handler.py
-rwxr--r-- 1 chris chris   247 Mär 16 16:48 signal-handler.py~
-rw-r--r-- 1 chris chris 34371 Feb  8 09:53 st
drwxr-xr-x 2 chris chris  4096 Feb  9 09:40 test1
-rwxr--r-- 1 chris chris     7 Feb 18 09:16 test_script.sh
-rw-r--r-- 1 chris chris   116 Mär  7 08:31 TODO
-rw-r--r-- 1 chris chris  7962 Feb  6 17:33 t.txt
In [61]:
comment_and_execute 'Das wird bei cd ~ und ls ausgegeben' 'cd ~ ; ls'
Das wird bei cd ~ und ls ausgegeben
backup                   Downloads         lehre-extern  Public
bin                      encrypted         models        python-virtal-envs
computational-graph.pdf  examples.desktop  Music         snap
data                     git-repos         notebooks     Templates
dead.letter              go                Pictures      tmp
deep.TEACHING            HTW-nextcloud     private       Videos
Desktop                  HTW-sensibel      programs      virtual-machines
Aufgabe

Schreiben Sie eine shell-Funktion, der Sie zwei Argumente übergeben:

  • erste Parameter soll eine Liste mit Variablennamen sein.
  • der zweite Parameter soll eine Zahl sein, die angibt von welcher Variable (Index) der Inhalt angezeigt wird:

Beispiel:

first="Hallo Welt"
second="Hello World"
thrid="Hola Mundo"
my_array=(first second third)

show_variable my_variable myarray 1 

Hello World.

bzw.

show_variable my_array 0

Hallo Welt

Hinweis: Indirekte Expansion von Variablen.

In [62]:
first="Hallo Welt"
second="Hello World"
third="Hola Mundo"
my_array=(first second third)
In [64]:
show_variable my_array 2
show_variable my_array 1
show_variable my_array 0
Hola Mundo
Hello World
Hallo Welt

Kontrollstrukturen

Tests

Mit Tests kann man überprüfen, ob Variablen gesetzt oder leer sind bzw. sind Werte- und Stringvergleiche möglich. Dateien können auch überprüft werdem, z.B. ob sie vorhanden sind bzw. welchen Typen sie haben.

Die Tests werten zu Wahr (true) oder Falsch (false) aus. So können damit Programm-Abzweigungen oder bedingte Ausführungen realisiert werden.

Logische Operatoren

  • &&: Und-Operator (And)
  • ||: Oder-Operator (Or)
  • !: Negation

Beide werden bedingt ausgewertet (Kurzschlussauswertung, short-circuit evaluation):

In [380]:
true || echo wird nicht geschieben

false && echo wird nicht geschieben
# ist das gleiche wie
! true && echo wird nicht geschieben

In [363]:
true && echo wird geschieben
false || echo wird geschieben
wird geschieben
wird geschieben

Beispielsweise kann mit test überprüft werden, ob Variablen gesetzt bzw. leer sind:

In [511]:
a='Hallo Welt'
test "$a" || echo Variable ist leer oder nicht gesetzt!
true || echo Variable ist leer oder nicht gesetzt!

Beachten Sie: -

  • Das Setzen mit einem nicht-leeren String entspricht true.
  • Leere und nicht-gesetzte Strings false

Typischerweise werden dafür aber eckige Klammern verwendet: [ ]. Die einfachen eckigen Klammern sind eine Referenz auf test.

In [416]:
[ "$a" ] || echo Variable ist leer oder nicht gesetzt!
Variable ist leer oder nicht gesetzt!

In Bash (nicht POSIX-konform) gibt es auch doppelte eckige Klammern [[:

In [484]:
[[ $a ]] || echo Variable ist leer oder nicht gesetzt!
Variable ist leer oder nicht gesetzt!

Zum Unterschied zwischen [ und [[ siehe http://mywiki.wooledge.org/BashFAQ/031

In [418]:
type [
type [[
[ is a shell builtin
[[ is a shell keyword
In [423]:
rm /etc/some_file.conf || echo "I couldn't remove the file"
rm: cannot remove '/etc/some_file.conf': No such file or directory
I couldn't remove the file
  • Mit der Option -z kann überprüft werden, ob die Variable leer/nicht-gesetzt ist. Liefert im Fall leer/nicht-gesetzt true zurück.
  • Mit der Option -n kann überprüft werden, ob die Variable nicht-leer/gesetzt ist. Liefert im Fall nicht-leer/gesetzt true zurück.

Für eine Liste der Vergleichsoperatoren siehe https://tldp.org/LDP/abs/html/comparison-ops.html

In [501]:
a="a"
unset a
[[ -z $a ]] && echo Variable ist leer oder nicht gesetzt!
# dasgleiche wie
[[ ! $a ]] && echo Variable ist leer oder nicht gesetzt!

[[ -n $a ]] || echo Variable ist leer oder nicht gesetzt!
# dasgleiche wie
[[ $a ]] || echo Variable ist leer oder nicht gesetzt!
Variable ist leer oder nicht gesetzt!
Variable ist leer oder nicht gesetzt!
Variable ist leer oder nicht gesetzt!
Variable ist leer oder nicht gesetzt!

Zur Unterscheidung von leer vs. gesetzt können Sie folgenden Code-Snipsel verwenden, von https://serverfault.com/questions/7503/how-to-determine-if-a-bash-variable-is-empty

In [460]:
a=""
# unset a 
if [[ -z ${a+x} ]]; then
 echo Variable nicht gesetzt
else
 echo "Variable gesetzt (kann auch leer sein)"
fi
Variable gesetzt (kann auch leer sein)

String Vergleich

In [521]:
b="$a"
#b="Hola Mundo"
# = ist hier ein Vergleich
[[ $a = $b ]] && echo beide Variablen sind gleich.
# es geht aber auch ==
[[ $a == $b ]] && echo beide Variablen sind gleich.

[[ $a != $b ]] && echo beide Variablen sind ungleich.
beide Variablen sind gleich.
beide Variablen sind gleich.

Integer Vergleiche

In [155]:
a=3
b=5
[[ $a -eq $b ]] && echo a gleich b
[[ $a -ne $b ]] && echo a ungleich b
[[ $a -gt $b ]] && echo a größer b
[[ $a -lt $b ]] && echo a kleiner b
[[ $a -ge $b ]] && echo a größer-gleich b
[[ $a -le $b ]] && echo a kleiner-gleich b

# Es geht aber auch mit einer arithmetischen Umgebung (siehe unten)
[[ (($a < $b )) ]] &&  echo a kleiner b
[[ (($a > $b )) ]] &&  echo a größer b
a ungleich b
a kleiner b
a kleiner-gleich b
a kleiner b

Für weitere Vergleichsoperatoren für Variablen siehe https://tldp.org/LDP/abs/html/comparison-ops.html

Dateivergleiche

Da man die Shell typischerweise nutzt, um Dateien zu maniplieren, existieren etliche Vergeleiche für Dateien, wie z.B.

  • -e Datei existiert
  • -fist eine reguläre Datei
  • -s Datei hat nicht Größe Null
  • -d Datei ist ein Verzeichnis
In [537]:
[[ -e "./countries.txt" ]] && echo "countries.txt existiert im Arbeitsverzeichnis"
[[ -d "." ]] && echo "Das Arbeitsverzeichnis '.' ist wenig überraschend ein Verzeichnis."
countries.txt existiert im Arbeitsverzeichnis
Das Arbeitsverzeichnis '.' ist wenig überraschend ein Verzeichnis.

Für weitere Vergleichsoperatoren für Dateien siehe https://tldp.org/LDP/abs/html/fto.html

Übung

Erklären Sie den Fehler des folgenden Code-Schnipsels und beseitigen Sie die Fehlermeldung (aber richtiger Vergleich):

In [199]:
meinName='Christian Herta' 
deinName='Jemand anderes'
[ $meinName = $deinName ] || echo zwei unterschiedliche Personen
bash: [: too many arguments
zwei unterschiedliche Personen

If, then, else

In [355]:
# example from https://program-script.com/bash/control-structures/if-then-else.php

function if_then_else(){
    number=$1
    if [ $number -gt 0 ]; then
        echo "Zahl ist positiv."
    elif [ $number -lt 0 ]; then
        echo "Zahl ist negativ."
    else 
        echo "Zahl ist Null."
    fi
}
In [639]:
if_then_else -1
Zahl ist negativ.

Rückgabewerte als Boolsche Werte

Die Rückgabewerte von Kommandos (Programmen) werden oft in bash-Skripten etc. als boolesche Werte interpretiert.

Wenn ein Programm keinen Fehler zurückgibt, ist der Returnwert 0. Beispielsweise wird in C die main-Funktion in diesem Fall mit return 0 beendet.

In [637]:
ls * 1>/dev/null; echo $?
ls this-file-does-not-exits  2>/dev/null; echo $?
0
2

Vergleichen Sie dies mit:

In [644]:
ls * 1>/dev/null && echo "Das Kommando war erfolgreich (exit code = 0)"
ls this-file-does-not-exits  2>/dev/null || echo "Das Kommando war nicht erfolgreich (exit code != 0)"
Das Kommando war erfolgreich (exit code = 0)
Das Kommando war nicht erfolgreich (exit code != 0)

Das bedeutet true ist 0 und false ist 1 im Gegensatz zu den meisten Programmiersprachen!

In [657]:
true; echo true entspricht $?
false; echo false entspricht $?
true entspricht 0
false entspricht 1
In [656]:
if who | grep chris > /dev/null
then
    echo exit code ist $?. Das ist auch der boolesche Wert für true.
    echo 'Chris ist eingeloggt.'
else 
    echo exit code ist ${?}.Das ist auch der boolesche Wert für false.
    echo 'Chris ist nicht eingeloggt.'
fi
exit code ist 0. Das ist auch der boolesche Wert für true.
Chris ist eingeloggt.

Regular Expression Tests

In [20]:
# simple regEx
VAR="Tesus"
if [[ $VAR =~ Th?esus ]] # =~ regex-check
then
    echo match
fi
match
In [65]:
# grouping:
VAR="IP address is 192.168.0.1"
regEx="[[:alpha:]]*([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)\.([[:digit:]]+)"
if [[ $VAR =~ $regEx ]] # =~ regex-check
then
  j=0
  for i in "${BASH_REMATCH[@]}"; do
    echo '${BASH_REMATCH['$j'] =' $i # ${BASH_REMATCH[$j]}
    ((j++)) #  Arithmetic Operationen siehe unten
  done  
fi
${BASH_REMATCH[0] = 192.168.0.1
${BASH_REMATCH[1] = 192
${BASH_REMATCH[2] = 168
${BASH_REMATCH[3] = 0
${BASH_REMATCH[4] = 1

Aufgabe

Sehen Sie sich die Funktionen der regEx-Übung an und analysieren Sie diese.

Arithmetische Operationen

Es verschiedene Möglichkeiten arithmetische Berechnungen durchzuführen, wie expr, let, declare und arithmetic Expansions. Dabei ist letzteres für einfache ganzzahlige Operatoren zu bevorzugen:

  • $((...)) Arithmetische Expansion mit eventueller Änderung der Shell-Variablen mit Rückgabewert.
  • ((...)) Compound Command für arithmetische Ausdrücke (arithmetic expressions), d.h. arithmetische Berechnungen mit eventueller Änderung der Shell-Variablen (ohne Rückgabewert).
In [677]:
echo 3 + 4 ist $((3+4))
3 + 4 ist 7
In [66]:
a=5
((a++)) # Beachte: kein $ vor der Variablen
echo $a
((a*=2))
echo $a
6
12
In [15]:
((a=4+3)); echo $a
echo $((a=4+4)) ist $a
7
8 ist 8

Kompliziertere mathematische Operationen können z.B. mit bc (math-library mit -l) ausgeführt werden (siehe auch man bc):

In [40]:
whatis bc
echo
a=5
x=`echo "e(.6+$a)" | bc -l`
echo "$x"
bc (1)               - An arbitrary precision calculator language

270.42640742615262815292
Übung

Erklären Sie folgenden Codeschnipsel in Hinsicht auf die booleschen Werte:

In [4]:
number=1
[ $number -gt 0 ]; echo $?
[ $number -eq 0 ]; echo $?
[ $number -lt 0 ]; echo $?

echo ------------------

((0)); echo $?
((1)); echo $?
((number==1)); echo $?
((number==2)); echo $?
0
1
1
------------------
1
0
0
1

Übung

Aufgabe

Erklären Sie folgenden Codeschnipsel und beseitigen Sie die Fehlermeldung.

In [594]:
a=4
$(( a++ )) # Ziel erhöhe a um Eins  
echo $a
4: command not found
5
In [201]:
a=4
(( a++ )) # Ziel erhöhe a um Eins  
echo $a
5

Schleifen (Loops)

While-Loop

In [620]:
z=1
while [[ $z -le 5 ]]
do
  echo $z
  ((z++)) 
done
1
2
3
4
5

Until-Loop

In [621]:
z=1
until [ $z -gt 5 ]
do
 echo $z
 ((z++))
done
1
2
3
4
5

Break und Continue

  • break - Loop verlassen
  • continue - Beende laufende Iteration
In [624]:
# ein ziemlich überkompliziertes Beispiel
# in sehr schlechtem Stil
z=0
while true
do
  [[ $z -eq 10 ]] && break 
  ((z++)) 
  if (($z % 2)); then
      continue # das ist nur zur Demonstration von continue und so nicht sinnvoll!
  fi
  echo "$z"
done
2
4
6
8
10
Aufgabe

Was ist an dem obigen Beispiel zu kritisieren?

Vereinfachen Sie den Code.

Foreach-Loops

Beispiel mit den Aufruf-Argumenten eines Skriptes

In [626]:
echo '#!/usr/bin/env bash
echo das script $0 wurde mit $# Argumenten aufgerufen.

for i in $@ # Loop over all arguments
do 
 echo $i
done
' > example_script.sh

./example_script.sh # Loop over all arguments
das script ./example_script.sh wurde mit 0 Argumenten aufgerufen.

C-Stye For-Loop

In [5]:
# erinnerung (( ... )) für arithmetiche Ausdrücke
for ((c=1; c<=5; c++)); do  
   echo "Welcome $c times"
done
Welcome 1 times
Welcome 2 times
Welcome 3 times
Welcome 4 times
Welcome 5 times

Beachten Sie: hier wurde das do in dieselbe Zeile des for gesetzt. Dafür muss ein Semikolon als Trenner verwendet werden.

Range für For-Loop

Man kann auch alles in einer Zeile schreiben, wie hier:

In [608]:
for c in {1..6} ; do echo "Welcome $c times"; done
Welcome 1 times
Welcome 2 times
Welcome 3 times
Welcome 4 times
Welcome 5 times
Welcome 6 times
In [617]:
# Range
echo {1..6}
# geht auch rückwärts mit anderer Schrittweite
echo {10..0..2}
1 2 3 4 5 6
10 8 6 4 2 0

Hinweis: Mit select für einen Loop lässt sich ein einfache Eingabe-Menü realisieren, siehe https://ryanstutorials.net/bash-scripting-tutorial/bash-loops.php#select

Loop über Dateien in Verzeichnis

Mittels bash Skripten kann man auch einfach Dateien in Verzeichnissen für die weitere Verarbeitung iterieren.

In [311]:
for file in ./*.txt
do
  echo $file 
done
./countries.txt
./f2.txt
./f.txt
./mylog.txt
./t.txt

Beachte Sie, dass das Globbing ./*.txt von der Shell aufgelöst wird, sodass effektiv dort ein Liste von Strings steht. So ähnlich, wie hier gezeigt:

In [332]:
for file in a.txt b.txt c.txt
do
  echo $file 
done
a.txt
b.txt
c.txt

Falls das Globbing nicht ausgelöst wird, d.h. keine Dateien auf das Glob-Muster zutreffen, wird das Glob-Muster direkt verwendet. Dies kann zu Fehlermeldungen führen, wie hier demonstriert:

In [340]:
for file in ./*.texte
do
  ls $file 
done
ls: cannot access './*.texte': No such file or directory

In der Bash kann das Verhalten des Globbings mit dem Befehl shopt geändert werden. shopt kann das Verhalten der Shell geändert werden (und somit auch des Globbings), mehr siehe https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html.

Mit shopt -s nullglob wird ein Globmuster leer ersetzt, falls es auf nichts zutrifft.

In [597]:
shopt -s nullglob # set (-s) von nullglob
for file in ./*.texte
do
  ls "$file"  
done

Hier setzen wird das Standardverhalten wieder zurück:

In [598]:
# unset (-u) von nullglob 
shopt -u nullglob

Mit Hilfe eines Tests (Datei existiert?) und continue lässt sich das Problem auch beseitigen:

In [600]:
for file in ./*.texte
do
  [ -f "$file" ] || continue 
  ls "$file" 
done

Datei zeilenweise lesen

Mittels read (siehe help read) kann man eine Datei zeilenweise einlesen:

In [703]:
cat countries.txt | while read line; do
  echo $line
done
France
Canada
Burkina Faso
Democratic Republic of the Congo
Russia
New Zealand

Bash Strict-Mode

Hilfreich ist es für Skripte den Strict-Mode einzuschalten. Dazu beginnt man ein bash-Skript mit folgenden Zeilen:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

Dies vermeidet subtile Fehler zur Laufzeit. Lesen Sie hierzu: unofficial-bash-strict-mode

Das Setzen des IFS erfolgt hier mit ANSI-C-Quoting

Tipp: Es gibt außerdem Checker-Werkzeuge, wie shellcheck, die Ihnen helfen Fehler zu finden.

Übungen: Skripte

Aufgabe

Schreiben Sie ein Skript, das alle MP3 Dateien in einem Ordner mit einer Zahl am Anfang, so umbenennt, dass die Zahl immer 3 Stellen hat. Hintergrund: Ein MP3-Player spielt die Dateien meist nach String-Sortierung ab.

  • z.B. wird so aus 1_Einleitung.mp3 die Datei 001_Einleitung.mp3
  • z.B. wird so aus 14_Das_Erwachen.mp3 die Datei 014_Das_Erwachen.mp3

Erzeugen Sie zum Testen Ihres Skriptes mit touch und brace expansion hundert Dateien, mit dem passenden Muster.