Einstieg in die Bash-Shell - Teil 2
Hinweis: Da die Benutzung der Bash teilweise kryptisch und die Syntax schwer zu merken ist, können Sie Cheatsheets für die Übungen etc. verwenden:
Hier finden Sie eine Liste der GNU Core Utils. Das sind Standardkommandos, die in den meisten Unix-Umgebungen verfügbar sind.
find
¶Mit Hilfe von find
können Sie ausgehend von einem Verzeichnis Dateien finden. Dabei können verschiedene
Filter gesetzt werden, die die gefundenen Dateien erfüllen müssen:
-type
: Typ der Datei, z.B. Verzeichnis (d
), normale Datei (regular file) f
etc.-name
: Name der Datei, hier können glob-Muster, wie *
,?
, ranges ([..]
mit Negation [^..]
verwendet werden. -iname
: wie -name
, aber case-insensitiv (d.h. insensitiv auf Groß- und Kleinschreibung)-path
(case-insensitive -ipath
): analog -name
, aber das glob-Muster bezieht sich auf den vollständige Pfad zur Datei. So kann man Pfade einschränken und den Verzeichnistrenner /
verwenden. Vergleich Manpage von find
für den Unterschied zwischen name
und path
. -mtime
Modifikationsdatum (-atime
Zugriffsdatum) in 24h Einheiten-mmin
Modifikationsdatum in Minuten-size
GrößeBeispiel: Alle Verzeichnisse ausgehend vom Home-Verzeichnis, die im Namen "HTW" enthalten: find ~ -name "*HTW*" -type d
Für weitere Optionen, wie Suche nach Datum, Benutzer etc. siehe https://wiki.ubuntuusers.de/find/
Für eine ausführliche Dokumentation, siehe https://www.gnu.org/software/findutils/manual/html_mono/find.html#Full-Name-Patterns. Aber natürlich gibt es auch eine man-Page (man find
) und https://tldr.ostera.io/find.
Etliche Beispiele finden Sie auch hier: https://alvinalexander.com/unix/edu/examples/find.shtml
Wiederholung: Beachten Sie, dass die Shell Wildcards -falls möglich- per Globbing direkt auflöst, bevor die Argumente zum
Befehl (hier find
) weitergereicht werden. Mit find ~/development -name *.py
würde das *.py
durch die im laufenden Arbeitsverzeichnis befindlichen Python-Dateien ersetzt werden, wenn hier Dateien mit Endung .py
vorhanden sind.
Daher ist unbedingt ein Quoting vozunehmen:find ~/development -iname "*.py"
-
und +
vor der Mengenangabe bedeutet kleiner bzw. größer, z.B.:
Finde alle Dateien unter dem Download Ordner, die vor weniger als 24h (1 Tag) modifiziert (oder erstellt) worden sind:
find ~/Downloads/ -mtime -1
Beachte: Finde alle Dateien unter dem Download Ordner, die genau 24h alt sind. Das will man in der Regel nicht!
find ~/Downloads/ -mtime 1
Finde alle Dateien unter dem Download Ordner, die älter als 24h sind:
find ~/Downloads/ -mtime +1
Neben mtime
gib es auch
atime
für accessed. Es wurde auf die Datei zugegriffen. ctime
für changed, d.h. der Dateistatus hat sich geändert.Finde ausgehend vom Arbeitsverzeichnis alle Verzeichnisse (vom Type directory) mit Namen "Lehre":
find ~ -name "Lehre" -type d
Finde ausgehend vom Arbeitsverzeichnis alle Verzeichnisse mit Namen "lehre" (beliebige Groß-Kleinschreibung - case insensitiv):
find ~ -iname "lehre" -type d
Finde alle regulären Dateien mit Endung ".ipynb" unter dem Home-Directory, die im Pfad ein Verzeichnis "julia" haben:
find ~ -path "*/julia/*.ipynb" -type f
Finde alle Dateien mit Endung ".tar.gz", die zwischen 200k und 5M groß sind:
find . -size +200k -size -5M -name '*.tar.gz'
Beachten Sie, dass in der Regel das Glob-Muster in Anführungszeichen gesetzt werden muss (Quoting),
damit die Shell es nicht vor dem Weiterleiten zu find
auflöst.
Finden Sie alle Verzeichnisse mit "HTW" im Verzeichnisnamen ausgehend z.B. vom laufenden Arbeits-Verzeichnis. So soll z.B. "./.../myHTW-public/" gefunden werden.
Welche von den folgenden Dateien werden bei
1.find . -name "*[0-9]*.ipynb"
2.find . -iname "[[:digit:]]*.ipynb"
gefunden?
30-Beispiel.ipynb
a-30-Beispiel.ipynb.txt
a-30.ipynb
Beispiel.ipynb
2-a.IPYNB
Annahme: Alle Dateien liegen unter dem laufenden Arbeitsverzeichnis (.
) entweder direkt oder in Unterverzeichnissen.
Probieren Sie es auch aus! Legen Sie hierzu die Dateien
30-Beispiel.ipynb a-30-Beispiel.ipynb.txt a-30.ipynb Beispiel.ipynb 2-a.IPYNB
an. Wie geht dies am einfachsten?
Finden Sie alle zip-Dateien (Endung .zip)
mit einer Größe zwischen 200k und 1M.
Finden Sie alle C++-Dateien (Annahme: erkennbar an der Endung .cpp
), die in den letzten 5 Tagen (genauer 5 $\cdot$ 24h) modifiziert wurden (ausgehend vom Arbeitsverzeichnis).
find ~/tmp -name "e*.txt" | xargs -I {} echo {}
locate
¶Hinweis: Neben find
gibt es auch den Befehl locate
zum Finden von Dateien. Dieser benutzt eine Datenbank, die ab und an aktualisiert wird. Daher ist locate
schneller. Es kann aber passieren, dass neu geänderte Datei(namen) nicht gefunden werden. Die Datenbank wird mit Hilfe eines cron-Jobs (mehr zu cron
später) in der Regel täglich aktualisiert. Sie kann auch mittels sudo updatedb
manuell aktualisiert werden.
Mehr zu locate
in der Manpage oder unter https://wiki.ubuntuusers.de/locate/
Mit Hilfe Regulärer Ausdrücke (regular expressions, kurz Regex) können Zeichenketten (Texte) auf Muster durchsucht werden oder überprüft werden etc. Bei einer Suche kann man beispielsweise als Ergebnis die Bereiche (typischerweise Zeilen) erhalten, in denen das Muster gefunden wird.
Reguläre Ausdrücke gibt es für alle gängigen Programmiersprachen und auch für viele Textanwendungen (Editoren oder Textverarbeitungsprogramme) zur Suche. Dabei gibt es oft kleine Unterschiede bzgl. der Syntax der Ausdrücke (verschiedene Regex-Dialekte). Ein weiteres Anwendungsbeispiel neben der Suche ist die Überprüfung von Usereingaben, z.B. ob in einem Webformularen eine gültige E-Mail Adresse eingegeben wurde.
Beachten Sie, dass reguläre Ausdrücke und Dateinamen-Globbing zwei unterschiedliche Dinge sind.
Hinweis: Zu Glob gibt es auch eine Manpage: man glob
grep
¶Der Befehl grep
dient zum Finden von Text-Mustern ausgedrückt durch reguläre Ausdrücke, z.B. in stdout
oder Textdateien.
Arbeiten Sie folgende Beschreibung von RegEx (eventuell parallel mit der Regex-Übung, siehe unten) und grep durch: https://wiki.ubuntuusers.de/grep/
Hinweis: Es gibt auch eine Manpage: man regex
Arbeiten Sie die Übung "Reguäre Ausdrücke" (jupyter notebook regular-expression-exercise.ipynb
) durch.
Beachten Sie, dass die gängige Regex-Syntax den extended regular expressions (posix) der Bash entspricht.
Tipp zum Testen von regulären Ausdrücken auf der Kommandozeile können Sie auch ein here-Dokument verwenden (here-Dokumente werden weiter unten genauer erklärt):
grep -E "[^a][Bb]er" << EOF
Aber heute war es toll
in Berlin.
Hier können noch
mehr Zeilen stehen,
müssen aber nicht.
EOF
In allen Dateien im laufenden Verzeichnis mit Endung .txt
, die mit a
anfangen, soll folgendes Textmuster gefunden:
ein a
gefolgt von beliebig vielen Zeichen und dann die Zeichenfolge txt
, z.B. soll folgende Zeile ausgegeben werden:
Die Dokumente werden gelöscht, aber txt-Dateien sind nicht betroffen.
Welche python-Dateien (alle unter einem bestimmten Verzeichnis inkl. alle Unterverzeichnisse) haben im Code eine Zeichenfolge TODO
? Gehen Sie davon aus, dass die Dateienamen keine Leerzeichen enthalten.
Im Ergebnis soll die Dateien (inkl. Pfad) und die Zeile, auf der das Muster "TODO" zutreffen, angezeigt werden.
Zwei mögliche Lösungswege:
-exec
Schalters von find
für grep
.find
-Kommandos und verwenden von xargs
und grep
. # Nehmen Sie an, dass das zu durchsuchende Verzeichnis (inkl. Unterverzeichnis)
# in folgender Variablen gespeichert ist. Sie sollten zum Ausprobieren den Pfad ändern.
SEARCHPATH=~/HTW-nextcloud/Lehre/dataScience/reinforcement-learning
# Variablen werden weiter unten erklärt.
Sie wollen wissen, aus wievielen Zeilen python-Code ein Softwareprojekt besteht, d.h. Sie
wollen die Zeilen aller python-Dateien (Endung .py
) unter dem Projekt-Hauptverzeichnis
zählen.
Wie können Sie dies machen?
Hinweis: Kombinieren Sie die Befehle find
, cat
und wc
.
cat
mit mehreren Dateien als Argumente gibt den Inhalt dieser Dateien gemeinsam aus.wc
hat einen Schalter zum Zählen der Zeilen.Mit Hilfe des Kommandos history
kann man die zuletzt verwendten Befehle erhalten.
Welche zuletzt verwendeten Befehle haben Sie benutzt, die find
und maxdepth
enthalten?
Randmerkung: Diese Information wird in der Datei ~/.bash_history
abgespeichert. Nutzen Sie aber nicht diese Datei direkt, sondern verwenden Sie den Befehl history
.
Wie kann man nur die Dateien im Arbeitsverzeichnis mit Endung .ipynb
anzeigen (unter der Benutzung von ls
), deren Dateinamen mindestens 2 Zahlen in Folge enthalten?
find
direkt mit regulären Ausdrücken¶Mit Hilfe des Schalters -regex
kann bei find
anstatt eines Glob-Musters auch ein
regulärer Ausdruck verwendet werden. Welcher Art (Dialekt) von regulären Ausdrücke angewendet werden soll, kann mittels -regextype
eingestellt werden.
Beispiel:
find .. -maxdepth 1 -regextype "posix-extended" -regex '.*[0-9]{1,10}.*\.ipynb'
echo
echo ------------
echo
# ist äquivalent zu
ls ../*.ipynb | grep -E '[0-9]{1,10}'
In Bash können Werte in Variablen gespeichert werden.
Um einen Wert einer Variablen zuzuordnen, wird der Zuweisungsoperator =
verwendet.
my_var=5
Beachten Sie dabei, dass hier keine Leerzeichen beim =
stehen dürfen.
Warum ist das so restriktiv? Probieren Sie es aus mit einem Leerzeichen und erklären Sie die Fehlermeldungen.
Beachten Sie das dies zu Beginn verwirrend ist und leicht zu Fehlern führen kann.
Nebenbemerkung: In der Shell-Terminologie ist eine Variable auch ein Shell Parameter.
Ein Parameter ist eine Entität, die einen Wert enthalten kann. Eine Variable ist ein Parameter mit einem Namen (hier my_var
).
Der Inhalt einer Variablen kann mit Parameter Expansion
ausgelesen werden. Parameter Expansion startet mit einem $
, z.B.:
echo $my_var
Manchmal muss man geschweifte Klammern setzen, um den Variablennamen vom Folgenden zu trennen, wie hier
echo ${my_var}er
Randbemerkung: Der grundlegende Type einer bash-Variable ist String. Mit declare
können aber Attribute gesetzt werden, wie z.B. das integer attribute.
Da im Prinzip alle Variablen Strings sind, hat die bash(-Programmiersprache) nicht wirklich Typen, vgl. Unix-Philosophie: "Schreibe Programme so, dass sie Textströme verarbeiten, denn das ist eine universelle Schnittstelle."
In diesem Sinne ist bash sehr einfach und dient oft nur einfachen administrativen Aufgaben (nahe am Betriebsystem bzw. Befehlsinterpreter).
Für komplexere adminstrative Aufgaben wird daher oft die Programmiersprache Python eingesetzt, die erheblich mächtiger ist und verschiedene Datentypen kennt.
Längere Strings mit Leerzeichen können durch die Anführungszeichen "
oder '
geschützt werden (Quoting).
greetings="Hallo Welt!"
Beachte den Unterschied:
my_var="-$greetings- ist ein Gruß." # Variablen Expansion
echo $my_var
my_var='-$greetings- ist ein Gruß.' # alle Zeichen (auch $) werden als Literale interpretiert
echo $my_var
Bei den einfachen Anführungszeichen werden die Inhalte direkt angezeigt, ohne dass z.B. Variablen expandiert werden. Erinnerung: bei doppelten Anführungszeichen wird das Globbing von der Shell nicht aufgelöst:
#`-e` enable interpretation of backslash escapes
echo -e "Quoting: *.txt \nvs. \nExpansion:" *.txt
Beachte: Einige Zeichen, wie "$" oder "&", müssen escaped werden:
echo Du schuldest mir $5! # Erklären Sie die Ausgaben!
echo "Du schuldest mir $5!"
echo "Du schuldest mir \$5!"
echo 'Du schuldest mir $5!' # escaping nicht nötig bei "single quotes"
Length=5
echo ${greetings:0:Length}
echo ${greetings: -5} # Das Leerzeichen ist wichtig!
# sonst geht es nicht:
echo ${greetings:-5}
echo ${#greetings}
Beseitigung am Anfang von Dateien:
${var#Muster}
: Beseitigt kürzesten Teil des Musters${var##Muster}
: Beseitigt längsten Teil des MustersBeseitigung am Ende von Dateien:
${var%Muster}
: Beseitigt kürzesten Teil des Musters${var%%Muster}
: Beseitigt längsten Teil des MustersErsetzungen am Anfang:
${var/#from/to}
: Ersetzt kürzesten Teil des Musters${var/##from/to}
: Ersetzt längsten Teil des MustersErsetzungen am Ende:
${var/%from/to}
: Ersetzt kürzesten Teil des Musters${var/%%from/to}
: Ersetzt längsten Teil des MustersErsetzungen allgemein:
${var/from/to}
: Ersetze ersten Treffer ${var//from/to}
: Ersetze alleBeispiele:
echo $greetings
# Ersetze "Hallo" mit "Grüße an die" in der Variable greetings
echo ${greetings/Hallo/Grüße an die}
var="http://christianherta.de/lehre/informatik/von_neumann_architektur.html"
echo "${var#*/}"
echo "${var##*/}"
var=/path/to/a/image.old.png
echo "${var%png}"jpg
echo "${var%.*}".jpg
echo "${var%%.*}".jpg
Mehr zu String-Substitutionen siehe: https://www.cyberciti.biz/tips/bash-shell-parameter-substitution-2.html
echo $greetings
andere_variable="greetings" # Speichere den Variablennamen "greetings" in einer Varaiblen
echo ${andere_variable}
echo ${!andere_variable}
echo ${foo:-"Wenn Variable 'foo' nicht gesetzt, wird dies verwendet."}
Beantworten Sie folgende Fragen:
Var="Ich bin die Königin der Kommandozeile."
eine neue Variable Var1
erstellen, wobei das bin die Königin der
durch beherrsche die
ersetzt wurde.Var
die Zeichen 19 bis zum Ende ausschneiden?Var
die Zeichen 19 bis vorletzten Zeichen ausschneiden, d.h. nicht en abschließenden Punkt?Var="Ich bin die Königin der Kommandozeile."
$(kommando)
Das Kommando innerhalb der Klammern wird in einer Subshell (siehe unten) ausgeführt und stdout des Kommandos als Rückgabewert erhalten.
Das wird auch als command substitution bezeichnet.
Beispiel:
ls -la $(which ping)
# alternativ zu
# which ping | xargs ls -la
# alternative Syntax für Command Substitution per Backticks
ls -la `which ping`
Subshells werden als Kopien (der Umgebung) von der ursprünglichen Shell gestartet (als Subprozess) - mehr zu Subshells (und Prozessen) später.
Hinweis: Subshells werden auch beim Pipen erzeugt, was zu subtilen Fehlern führen kann (siehe unten).
$
-Zeichen und Subshells¶Mit runden Klammern werden Subshells erzeugt.
$(...)
Das Kommando innerhalb der Klammern wird in einer Subshell ausgeführt und stdout des Kommandos als Rückgabewert erhalten. Das wird auch als command substitution bezeichnet.(...)
Das Kommando innerhalb der Klammern wird in einer Subshell ausgeführt (ohne Rückgabewert).echo "The current date is $(date)"
# alternative mit backticks "`"
echo "The current date is `date`"
ls -l $(which who)
Bei Kommando innerhalb geschweifter Klammern:
{...}
Führt die Kommandos in den geschweiften Klammern als Gruppe aus. Dabei muss immer ein Leerzeichen zwischen den geschweiften Klammern und den Befehlen darin stehen. Außerdem müssen alle Befehle mit einem ;
abgeschlossen werden.
So lassen sich bespielsweise alle Ausgaben einer Kette von Kommandos gemeinsam in eine Datei umleiten:
{ echo "I found all these PNGs:"; find . -iname "*.png"; echo "Within this bunch of files:"; ls; } > PNGs.txt
Beispiel aus All-about curly braces in Bash.
echo direkt
a=9 # setze die Variable in der Shell
echo $(echo $a) # in der Subshell ist die Variable verfügbar
echo PID:$BASHPID
echo -------------------------
echo Subshell
(b=10, echo PID:$BASHPID) # setze die Variable in einer Subshell
# in der aufrufenden Shell ist die Variable nicht gesetzt!
echo b ist $b
echo -------------------------
echo Gruppe
{ c=119; echo PID:$BASHPID; }
# in der aufrufenden Shell ist die Variable gesetzt!
echo c its $c
# das gilt natürlich auch für Änderungen von Variablen
a=1; (a=2; echo "inside: a=$a"); echo "outside: a=$a"
Wie kann man (einfach) den Inhalt einer Datei in eine Variable einlesen?
Wie kann man etliche Befehle ausführen und danach wieder im Ursprungsverzechnis sein? z.B.
cd ~ ; cat *.txt ; ls -l ; cd my_dir1; cat *.txt
Was ist der Unterschied zwischen Kommandos, die in runden ( .. )
und geschweiften Klammern { .. }
ausgeführt werden?
Hier finden Sie Unterschiede kurz beschrieben:
https://stackoverflow.com/questions/31255699/double-parenthesis-with-and-without-dollar
<( kommando )
erzeugt eine anonyme named pipe und verbindet stdout des Kommandos mit der named pipe. An der Stelle wird der Dateiname der named pipe zurückgegeben. Das ist sinnvoll für andere Kommandos denen typischerweise ein Dateinamen übergeben wird.
# cat bekommt einen Dateinamen als Input
# Dies demonstriert das Prinzip der process substitution
cat <(echo das landet in der Datei)
Das ist effektiv das Gleiche wie:
# hier liest cat von stdin
echo das landet in der Datei | cat
# hier wird der Dateiname (fd - file descriptor) ausgegeben
# dies ist in der Regel uninteressant - hier nur zur Demonstration
echo <( echo Hallo )
Aber es sind bei Process Substitution auch mehrere Kommandos möglich:
cat <(echo bar; echo foo)
# mittels ";" können mehrere Befehle in eine Zeile geschrieben werden
# Diese werden dann hintereinander ausgeführt.
Wir wollen die von wc
zurückgelieferten Informationen in Variablen speichern:
echo "Hallo Welt
Hello World
Hola mundo" > greetings
# Ein sinnvolles Beispiel
# greetings ist eine Textdatei!
# und wc zählt die Zeilen, Wörten und Zeichen
read lines words chars _ < <(wc greetings)
# lines words und chars sind Variablen (später mehr dazu)
# um den Inhalt von Variablen zu bekommen benötigt man das "$"
echo $lines
echo $words
echo $chars
Ausführliche Erklärung:
# zur Demonstration von wordcount-programm "wc"
# mehr zu wc siehe z.B. "man wc"
wc greetings
Erklärung von read lines words chars _ < <(wc greetings)
:
read
- Read a line from the standard input and split it into fields - (siehe help read
)<(wc greetings)
erzeugt mittels Process substitution eine named pipe temporäre Datei <
leitet den Inhalt der named pipe-temporären Datei zu read
type read
Beachte, dass folgender Code nicht funktioniert, da die Pipe eine Subshell erzeugt und die Variablen in der aufrufenden Shell (ohne Tricks) nicht mehr verfügbar sind, siehe z.B. https://stackoverflow.com/questions/46946888/bash-pipe-creates-a-subshell.
words= ; lines= ; chars= # delete the content of the variables
wc greetings | read lines words chars
echo $lines $words $chars # die Variablen sind nicht gesetzt!
Erklären Sie folgendes:
mkdir foo bar
touch {foo,bar}/{a..h}
touch foo/x bar/y
diff <(ls foo) <(ls bar)
Recherchieren Sie hierzu auch kurz das diff
-Kommando (wird später nochmal behandelt).
Beispiel für ein here document (<<
).
$wc << EOF
Das ist ein
Dokument über mehrere Zeilen
EOF
Die Zeichenkette (hier EOF
), die das Ende des mehrzeiligen Dokumentes angibt, kann beliebig gewählt werden.
# bc ist ein eigentlich ein interaktiver Calculator
# mehr siehe `man bc`
bc << End
a=3
b=5
a*b
End
# Demonstration des Dokumentes, das bc in obigem Beispiel erhält:
cat << End
a=3
b=5
a*b
End
Beipiel für ein here string (<<<
)
bc <<< 4*5
echo "12+5" | bc
cat <<< 4*5
Beachte: here documents und here strings werden an Stelle einer Datei eingesetzt.
here documents und here strings werden in bash mittels temporärer Dateien realisiert.
Mehr dazu siehe z.B.: https://askubuntu.com/questions/678915/whats-the-difference-between-and-in-bash
Eine typische Anwendung von here strings (und here documents) ist auf einem entfernten Rechner (per ssh
oder sftp
) mehrere Kommandos auszuführen, die in einem here string gesetzt werden. ssh
wird später im Kurs behandelt.
Weitere Anwendung: Einen anderen Interpreter, z.B. Python, aus der bash mit Befehlen aufrufen:
# Python Interpreter aufrufen mit "Hallo Welt"-Programm
python <<< "print ('Hallo Welt')"
# ist einfacher als die Alternative mit Process Substitution und stdout-Umleitung
python < <(echo "print('Hallo Welt')")
# mit einem here-Dokument
python << BOF
print ('Hallo Welt')
print ('sagt das mehrzeilige Python3 Programm.')
BOF
Mit Hilfe des Programms du
(disc usage) können Sie sich anzeigen lassen, wieviel
Speicher Verzeichnisse (inkl. Unterverzeichnisse) belegen, siehe man du
.
Wie kann man alle Verzeichnisse direkt unter dem eigenen Heimverzeichnis erhalten (inkl. Speicherverbrauch), die mehr als ein Gigabyte verbrauchen?
Hinweis: du
mit Schalter -h
und --max-depth
in Kombination mit grep
.
Kopieren Sie alle regulären Dateien im Downloadordner (~/Downloads/
), die in den letzen 60 Minuten modifiziert worden sind, in den Order ~/data/gene-expression/
.
Kopieren Sie alle pdf- und png-Dateien (Endungen pdf
bzw. png
case-insensitive) des laufenden Arbeitsverzeichnis, die jünger als der 8.März sind, in das Verzeichnis (./rechungen
).
Gegeben ist ein beliebiger relativer (oder absoluter) Pfad zu einer Datei, z.B.
../work/countries.txt
. Bestimmen Sie hieraus den Namen und den absoluten Pfad der Datei
und speichern Sie diese in zwei Variablen.
Der absolute Pfad sollte keinen .
oder ..
enthalten.
Hinweis: Benutzen Sie (unter anderem) die Posix-konformen Kommandos:
basename
dirname
whatis basename
whatis dirname
# Ihre Lösung sollte mit einem beliebigen relativen
# oder auch absoluten folgendem Pfad zurecht kommen, wie z.B.
myfile=../work/countries.txt
https://www.gnu.org/software/bash/manual/html_node/Quoting.html