[0x0B] Die Kommandozeile und ihre Lieblingsbefehle

Bei unserem 11ten Meetup haben wir uns mit den gängigsten Kommandozeilen-Befehlen auseinandergesetzt. katrin hat uns dafür einen Input geliefert und auch ein paar Kommandozeilenschmankerl herausgesucht. Dabei konnten wir die vorgestellten Befehle alle gleich auch selbst ausprobieren. In diesem Blog-Beitrag findet ihr die von uns ausprobierten Befehle samt Erklärung dazu, was sie machen.

Zu Beginn stand aber gleich einmal die Frage im Raum was denn eigentlich der Unterschied zwischen der shell, dem Terminal, der Konsole und der Kommandozeile ist. Praktisch gesehen verwenden wir diese Begriffe meist synonym. Und das funktioniert auch ganz gut. Mensch kann hier aber auch genauer differenzieren und in die Geschichte der Computerentwicklung abtauchen. Das sparen wir hier im Detail mal aus, verweisen aber auf ein paar nicht unspannende Wikipedia-Artikel:

An dieser Stelle ist uns nur wichtig zu sagen, dass wir mit dem Computer bzw. dem darauf laufenden Betriebssystem durch Eingabe von Befehls- bzw. Kommandozeilen interagieren können. Dazu benötigen wir ein Programm, das diese eingegebenen Kommandozeilen interpretiert. Das ist dann der Kommandozeileninterpreter, bzw. die Shell. Das Terminal bzw. die Konsole waren historisch einfach Geräte, die eine Verbindung mit einem Großrechner herstellten um überhaupt erst mal Ein- und Ausgabe zu ermöglichen, die Shell lief dabei direkt am Großrechner.

Beispielbefehle

Bevor wir hier gleich mit den Befehlen loslegen, noch eine kurze Erklärung wie denn solche Befehle bzw. ganze Befehlszeilen oder eben Kommandozeilen aufgebaut sind. Im Prinzip ist von der jeweiligen Shell und ihrer Syntax abhängt. Bei den meisten gängigen Shells unter Linux gibt es aber ein Muster wie so eine Kommandozeile ausschaut. Die einfachste Variante ist ein einzelner Befehle, oder irgendwelche weiteren Optionen:

bash befehlsname

Der befehlsname kann nun der Name von einem Programm sein, das ich ausführen möchte, oder auch ein eingbauter Shell-Befehl sein. Oft wollten wir aber dem Programm oder Kommando noch weitere Optionen mitgeben. Das können wir wie in folgendem Beispiel über flags und Argumente machen:

befehlsname -a -b -c --lange-option argument1 argument2

Hier sind -a, -b und -c sogenannte flags, die den Befehl modifizieren. --lange-option ist in dieser Variante im Prinzip auch ein flag, nur eben für uns besser les- und merkbar, und wird dann oft als long option bezeichnet. argument1 und argument2 sind dann zwei allgemeine Argumente, mit denen der Befehl etwas tun soll. Was all die flags, Optionen und Argumente jeweils tun, hängt vom Befehl ab.

Wir können uns direkt in der Shell eine Liste mit flags zum jeweiligen Befehl mit man befehlsname ausgeben lassen, das ist dann die sogenannte manual page. Oder wir suchen mit der Suchmaschine unserer Wahl im Web danach, oder schauen zum feminist linux meetup 😉

Nun aber genug Theorie, los gehts mit den Befehlen:

Navigieren & Explorieren

Mit cd (change directory) können wir in ein untergeordnetes Verzeichnis wechseln:

cd testFolder1

Ohne zusätzlichen Parameter wechselt man in das home directory, ganz unabhängig davon, wo wir uns gerade eben befinden:

cd

Mit - als Parameter wechselt man in das zuletzt besuchte Verzeichnis:

cd -

Mit .. wechselt man in das übergeordnete Verzeichnis:

cd ..

ls gibt eine Liste der Files und Verzeichnissen im aktuellen Verzeichnis aus. Mit verschiedenen flags können wir die Liste anpassen:

  • -l (long listing format) zeigt Berechtigungen, Owner, Group, Größe, Änderungsdatum
  • -a bzw. --all zeigt auch versteckte Dateien
  • -t nach Änderungsdatum sortiert (neuestes zuerst)
    flags können je nach Fragestellung beliebig kombiniert werden:
ls -l
ls -la
ls -lat
ls -lt

Es gibt noch viel mehr flags! Wir können uns in der Shell eine auch eine Liste mit flags zu einem bestimmten Befehl mit man (manual) ausgeben lassen.

man ls

Erstellen, verändern, kopieren

mkdir (make directory) erzeugt einen neuen Ordner im aktuellen Verzeichnis, falls es diesen noch nicht gibt. Der Name des Ordners muss angegeben werden:

mkdir folder1

Mit rm (remove) kann eine Datei im aktuellen Verzeichnis gelöscht werden:

rm file1

Mit rmdir (remove directory) kann ein Verzeichnis gelöscht werden, aber nur wenn es leer ist:

rmdir folder1

Wenn wir ein Verzichnis mit allen Unterverzeichnissen und Dateien löschen wollen, verwenden wir rm -r (recursive). Aber Vorsicht! Das kann nicht rückgängig gemacht werden und es gibt keine Warnung vom System!

rm -r folder1

Um eine Datei zu kopieren verwenden wir cp (copy). Dazu geben wir die zu kopierende Datei und die Zieldatei an.

cp example.txt /home/user/tmp.txt

Es wurde hier die example.txt Datei aus dem Verzeichnis in dem wir uns gerade befinden kopiert. Falls wir keine Datei aus dem aktuellen Verzeichnis kopieren wollen, müssen wir den absoluten Pfad für die Datei angeben:

cp /home/user/example.txt /home/user/tmp.txt

Anstatt der Zieldatei, kann auch ein Zielordner angegeben werden. Oder anders gesprochen: falls das angegebene Ziel bereits als Ordner exisitert, wird die zu kopierende Datei in diesen Ordner kopiert (und behält dabei ihren ursprünglichen Namen):

# /home/user/tmp ist ein bereits bestehender Ordner
cp example.txt /home/user/tmp
# Nun sollte sich in diesem Ordner eine example.txt befinden
ls -l /home/user/tmp/example.txt

Mit der recursive flag -r kann cp auch auf ganze Verzeichnisse angewendet werden und es wird der gesamte Inhalt mitkopiert.

cp -r Beispiel_Verzeichnis Kopie_von_Beispiel_Verzeichnis

Dateien verschieben funktioniert ähnlich. Mit mv (move) wird eine Datei (oder auch ein Ordner) verschoben.

# Wir verschieben die example.txt Datei nach tmp.txt
# Das kommt einer einfachen Umbenennung der Datei gleich
mv example.txt tmp.txt
# Nun verschieben wir die Datei tmp.txt in den Folder tmp
mv tmp.txt tmp
ls -l tmp/tmp.txt
# Jetzt bennen wir den Folder tmp in tmpFolder um
mv tmp tmpFolder

Schätze aus der Historienkisten holen

Die Shell schreibt für uns auch eine Geschichte, in dem sie alle Befehle die wir ausführen für eine gewisse Zeit aufbewahrt. Welche Befehle wir in der Vergangenheit ausgeführt haben können wir uns zum Beispiel mit folgendem einfachen Befehl ansehen:

history

Wir können die history aber auch interaktiv durchsuchen um Befehle direkt in unsere Kommandozeile zu holen. Dazu drücken wir zuerst Strg + R und tippen dann wonach wir suchen wollen. Es wird dann der erste Befehl angezeigt, der in der history rückwärts vorkommt, wo die gesuchte Textstelle gefunden wird. Also wenn ich sudo eintippe, wird der letzte Befehl angezeigt den ich verwendet habe, in dem sudo vorkommt. Wenn wir nun nochmal Strg + R drücken, dann wird der nächstältere Befehl angezeigt, in dem sudo vorkommt. Und so weiter, also mit jedem weiteren Strg + R noch ein älterer Befehl, solange bis keine weiteren mit diesem Suchbegriff gefunden werden. Wenn ich einen Befehl gefunden habe, den ich wieder verwenden (oder verändern) möchte, habe ich folgende Möglichkeiten:

  • Ich drücke gleich die Enter-Taste und der Befehl wird so wie er ist neu ausgeführt
  • Ich verwenden die Ende– oder Pos1-Taste um ans Ende oder den Anfang des Behfels zu springen und kann den Befehl dann noch verändern, befor ich ihn tatsächlich ausführe

Wenn ich mit meiner Suche gar nicht zufrieden bin und lieber abbrechen möchte, kann ich das mit Strg -C jederzeit tun.

Nun bietet die Shell aber auch noch viele history expansion und history substitution Möglichkeiten, die auch sehr praktisch sein können. Eine davon ist !!, was einfach für den zuletzt ausgeführten Befehl steht. Der kann so in einen neuen Befehl eingebaut werden. Besonders praktisch kann das mit komplizierteren Befehlen sein, für die ich eigentlich sudo Rechte benötige, was mir aber erst auffällt, nachdem ich den Befehl schon ausgeführt habe. Anstatt also den Befehl erst mit der Pfeiltaste nach oben nochmal anzusteuern und dann an den Anfang zu gehen um noch sudo hinzuzufügen können wir auch einfach folgendes tun:

sudo !!

So wird einfach die zuletzt ausgeführte Kommandozeile an ein sudo angehängt und damit der letzte Befehl mit Administrator*innenrechten nochmal ausgeführt.

Das ist aber auch mit dem vor- oder vorvorletzten Befehl möglich. So gibt es eine generische Syntax um Befehle in die Vergangenheit zu referenzieren: !-n. Dabei gibt n die Nummer an, also 1 für den letzten Befehl, 2 für den vorletzten, und so weiter. Wenn ich also den vorletzten Befehl nochmal mit asführen möchte, kann ich folgendes machen:

!-2

Wie oben auch, könnte ich auch einen anderen Befehl (z.B. ein sudo) davor geben. Das !! oben ist somit gleich wie ein !-1.

Viele umfassendere Möglichkeiten der history expansion und subsitution finden sich in der man history erklärt. Wie so oft ists aber gar nicht so zielführend zu versuchen sich das alles zu merken. Ab und an kann ein Blick dort hinein aber neue praktische Ideen und Hilfestellungen mit sich bringen.

Ein besonderes Schmankerl zum Abschluss, den uns katrin aus dem Web gesucht hat (die Quelle für diesen Fund scheint leider nicht mehr aktiv zu sein), ist folgende Aneinanderreihung von Befehlen, die uns aus der history die Top 10 unserer meistbenutzten Befehle inklusive Prozentangaben raussucht:

history | awk '{CMD[$2]++;count++;}END { for (a in CMD)print CMD[a] " " CMD[a]/count*100 "% " a;}' | grep -v "./" | column -c3 -s " " -t | sort -nr | nl |  head -n10

So, das wars dann auch mit unserer basic shell intro. Das Schmankerl grad eben deutet aber darauf hin, dass sich noch viel speziellere Dinge mit der Shell machen lassen. Das kann aber auch schnell ganz undurchsichtig und überfordernd werden, wenn eins gerade erst mal anfängt sich mit der Shell auseinanderzusetzen. Nachfolgend geben wir daher noch ein Beispiel, wie mensch an so etwas rangehen kann, für alle die jetzt noch Lust auf mehr haben.

Sollte dein Kopf aber gerade schon vor Befehlswirrwarr brummen, lass es doch vielleicht erstmal sickern und schau ein ander mal wieder vorbei. Der Blogartikel bleibt auf jeden Fall hier und wartet auf dich, wann auch immer du zu Besuch kommen magst.

Komplexes Anwendungsbeispiel

Mit einzelnen Shell-Befehlen kann oft schon ganz viel gemacht werden. Die wirklich magisch mächtig wirkenden Dinge werden aber oft erreicht, indem verschiedene Shell-Befehle kombiniert werden. Dazu ist vor allem ein Konzept wichtig, die Pipe: „|„. Das Zeichen wird deswegen Pipe genannt, weil es verwendet werden kann um die Ausgabe von einem Befehl als Eingabe in den nächsten Befehl hineinzufüttern. Zwei Befehle werden also quasi wie über ein Rohr miteinander verbunden.

Das ist mitunter im History-Schmankerl oben viel verwendet worden. Hier wollen wir nun versuchen anhand einer konkreten Frage-/Aufgabenstellung so eine Kombination zu generieren. Und zwar lautet die Frage: Wie kann ich mit einem Befehl eine Kopie des neuesten Files in einem Folder generieren?

Der Kopiervorgang wäre ja einfach, wenn wir sagen könnten: cp neuestes-file.dat kopie.dat

Wir müssen also versuchen in der oberen Zeile neuestes-file.dat durch den Dateinamen dieses neuesten Files zu ersetzen. Hier kommt noch ein anderes wichtiges Konzept ins Spiel, die command substitution: „$()„. Wenn ich irgendwo in einer Kommandozeile ein $(befehlszeile) verwende, dann wird die befehlszeile ausgeführt und deren Ausgabe an die Stelle in meiner eigentlichen Kommandozeile gesetzt, wo ich $(befehlszeile) stehen habe.

Das ändert unseren obigen Befehl in cp $(…) kopie.dat ab. Nur dass wir anstatt nun eine Kommandozeile finden müssen, die uns den Dateinamen des zuletzt geänderten Files ausspuckt.

Schauen wir uns also erstmal an, wie wir alle Files im Folder nach Datum sortiert auflisten können:

ls -lt

Die Ausgabe von dem Befehl schaut dann z.B. so aus:

total 24
-rw-r--r-- 1 jackie jackie   82 Jän 22  2021 Dockerfile
-rw-rw-r-- 1 jackie jackie  770 Jän  6  2021 snippet.py
-rw-rw-r-- 1 jackie jackie  426 Sep 26  2020 backgroundtest.css
-rw-rw-r-- 1 jackie jackie  284 Sep 26  2020 backgroundtest.html
-rw-r--r-- 1 jackie jackie 7739 Mär  9  2020 debug-gpu-hangs.txt

Wir bekommen also eine Liste an Dateien, wo die erste davon die jüngste, also zuletzt veränderte ist. Wir interessieren uns also nur für die zweite Zeile in dieser Ausgabe. Wir können hier mit den Befehlen head und tail arbeiten. Füttern wir also die Ausgabe in einen head Befehl um uns erstmal nur die ersten beiden Zeilen ausgeben zu lassen:

ls -lt | head -n2

Das führt zu folgender Ausgabe:

total 24
-rw-r--r-- 1 jackie jackie   82 Jän 22  2021 Dockerfile

Wir können diese Ausgabe nun weiter in einen tail Befehl füttern, um uns nur die letzte Zeile ausgeben zu lassen:

ls -lt | head -n2 | tail -n1

Nun sind wir bei folgender Ausgabe gelandet:

-rw-r--r-- 1 jackie jackie   82 Jän 22  2021 Dockerfile

Davon interessiert uns nun aber nur das letzte Wort, also der Dateiname, in diesem Fall Dockerfile. Hier können wir den Befehl awk verwenden, um die letzte Spalte in dieser Zeile herauszufiltern. Nebenbemerkung: awk ist eigentlich ein Interpreter für eine eigene Sprache, die AWK programming language, die sich besonders für „pattern scanning and processing“ eignet. Aber wir müssen hier gar nicht erst noch eine eigene Sprache lernen, weil das Web genügend praktische Beispiele zur Verfügung hat. Nach etwas Suche lässt sich daher schnell rausfinden, dass ich folgenden Befehl verwenden kann um die 9te Spalte aus obiger Textzeile zu filtern: awk '{ print $9 }'. Füttern wir also die obere Ausgabe in diesen Befehl hinein:

ls -lt | head -n2 | tail -n1 | awk '{ print $9 }'

Die Ausgabe schaut dann ganz nachdem aus, was wir in unserem eigentliche copy Befehl oben verwenden möchten:

Dockerfile

Fügen wir also nun diesen Befehl in unseren urpsrünglichen Befehl cp $(…) kopie.dat ein.

cp $(ls -lt | head -n2 | tail -n1 | awk '{ print $9 }') kopie.dat

Die Ausgabe von ls -lt ist nun also um eine neuere Kopie des Dockerfile ergänzt:

total 28
-rw-r--r-- 1 jackie jackie   82 Okt 23 09:38 kopie.dat
-rw-r--r-- 1 jackie jackie   82 Jän 22  2021 Dockerfile
-rw-rw-r-- 1 jackie jackie  770 Jän  6  2021 snippet.py
-rw-rw-r-- 1 jackie jackie  426 Sep 26  2020 backgroundtest.css
-rw-rw-r-- 1 jackie jackie  284 Sep 26  2020 backgroundtest.html
-rw-r--r-- 1 jackie jackie 7739 Mär  9  2020 debug-gpu-hangs.txt

Wunderbar! Nun gibt es aber (mindestens) einen Haken. Der ganze Befehl funktioniert zum Beispiel nicht mehr, wenn das neueste „file“ in der Auflistung ein Verzeichnis ist. Wir müssten also eine Dateiauflistung finden, die Verzeichnisse ausschließt. Hier gibt es wieder unterschiedliche Ansätze. Ich könnte z.B. vor der Weiterverarbeitung der Liste alle Zeilen herausfiltern, die mit einem d beginnen. Oder ich verwende anstatt ls den Befehl find um nur Dateien vom Typ „file“ zu finden. Und wie so oft gibt es wahrscheinlich noch mehrere Möglichkeiten, den Befehl noch besser zu machen. Dazu muss ich aber nicht schon jeglisches shell-fu beherrschen und einfach nur fest genug nachdenken. In der Regel ist es zielführender eine andere Person oder das Web zu befragen. Eine Suche in DuckDuckGo nach bash ls ignore directories gibt als zweiten Treffer z.B. einen Unix Stackexchange thread mit dem Titel How to list files without directories, and filter by name (ls options) zurück. Da hat eine Person ein ähnliches Problem und es findet sich schon eine Antwort, wie das mit find gelöst werden kann.

Wenn ich aber in unserem Beispiel davon ausgehen kann, dass in meinem Folder nur Dateien sein werden, und keine Subfolder, dann reicht mit der Befehl oben ja auch und ich kann mir die Perfektionierung vorerst auch sparen. Wobei auch hier hilft der Austausch mit anderen Personen, weils oft noch einfacher geht. So hat z.B. bei der Erstellung des Artikels lorb über das Beispiel drübergeschaut und geschrieben, dass ls ja schon Optionen hat, mit denen wir uns die Verwendung von tail und awk gleich sparen können:

cp $(ls -tw1 | head -n1) kopie.dat

Das wars für jetzt. Hoffentlich konnten wir einen guten Vorgeschmack darauf machen, was mit der Shell alles möglich ist. Wichtig war es uns aber auch zu zeigen, dass so kreative Lösungen nicht einfach aus dem Nichts oder irgendeiner genialen Regung entstehen, sondern sich durch Erfahrung und Verbesserung nach und nach ergeben können.

Dieser Artikel wurden von kathi, jaz und jackie erarbeitet, und mit Feedback aus unserer Community angereichert 💜