Dies sind Musterlösungen zur Shell-Safari. Natürlich mag es für jeden der Beispielfälle noch weitere Lösungen geben als die hier vorgestellten. Insgesamt hoffe ich, dir hat die Shell-Safari wenigstens ein bisschen Spaß und Inspiration gebracht. ^^ ====== Elementar: Dateisystem ====== 'ls' Listet die Dateien auf, die im aktuellen Verzeichnis liegen. 'pwd' ('present working directory') verrät dir, in welchem Verzeichnis du dich gerade befindest. mkdir Safari ('make directory') legt ein neues Verzeichnis mit Namen 'Safari' an. Mit 'cd' ('change directory') wechselst du in ein anderes Verzeichnis. Gibt man ein Verzeichnis an, so wechselt man in dieses. Der Aufruf von cd ohne Parameter wechselt standardmäßig ins eigene Homeverzeichnis, cd .. ins übergeordnete Verzeichnis. ====== Elementar: Hilfe bekommen ====== man Ist der Befehl zum Aufruf des MANuals, dem man als Parameter den entsprechenden Befehl (oder auch eine C++-Methode) übergeben kann. man ls ruft folgerichtig die Dokumentation von 'ls' in der Shell auf. Man kann mit den Pfeiltasten navigieren, mit '/' die Manpage nach '' durchsuchen und mit 'q' die Manpage wieder verlassen. Der Parameter --help funktioniert nicht bei allen Befehlen, aber bei denen, die ihn unterstützen, gibt er eine verkürzte Hilfe aus. Zusammen erhält man ls *.pdf # Geier nach Dateinamen sortiert ls -r *.pdf # Geier nach Dateinamen in umgekehrter Reihenfolge sortiert ls -S *.pdf # Geier nach Dateigröße (größter zuerst) ls -Slh *.pdf # S --> wie oben, l --> mehr Infos, h --> human-readable ====== Elementar: Redirect, less, more, cat, grep und die Pipe ====== Die Ausgabe einer vollständigen Dateiliste erfolgt mit ls -lh *.pdf Allerdings erhalten wir hier viel mehr Information: -rw-r--r-- 1 owner group 263K Apr 26 2007 001.pdf -rw-r--r-- 1 owner group 328K Apr 26 2007 002.pdf -rw-r--r-- 1 owner group 317K Apr 26 2007 003.pdf -rw-r--r-- 1 owner group 393K Apr 26 2007 004.pdf -rw-r--r-- 1 owner group 428K Apr 26 2007 005.pdf ... Eigentlich interessieren wir uns nur für die erste, die dritte, die fünfte und die letzte Spalte. Man erreicht dies mit folgender Erweiterung fast: ls -lh *.pdf | cut -d " " -f 1,3,5,9 Ausgabe: -rw-r--r-- owner 263K 2007 -rw-r--r-- owner 328K 2007 -rw-r--r-- owner 317K 2007 -rw-r--r-- owner 393K 2007 -rw-r--r-- owner 428K 2007 ... -rw-rw-r-- owner 221K 064.pdf -rw-rw-r-- owner 105K 065.pdf -rw-rw-r-- owner 205K 066.pdf -rw-rw-r-- owner 209K 067.pdf -rw-rw-r-- owner 219K 068.pdf ... Was passiert hier? Nun, 'cut' schneidet die 1., 3., 5. und 9. Spalte (Parameter '-f') des Textes, der übergeben wird, aus und wirft alles andere weg. Das Trennzeichen für Spalten wird mit der '-d'-Option als " " (Space) festgelegt. Der unscheinbare Strich '|' (die Pipe) leitet allgemein die Standard-Ausgabe des linken Befehls (hier also das ls) an den rechten (hier das cut) weiter. cut bekommt also zunächst die komplette Ausgabe und sucht daraus die genannten Spalten. Wie bekommen wir es nun hin, dass cut wirklich immer die //letzte// Spalte (statt der 9.) ausgibt? Nun, nicht ohne weitere Tools. Eine Variante wäre, statt 'cut' 'awk' zu verwenden: ls -lh *.pdf | awk '{print $1,$3,$5,$NF}' awk hat allerdings eine recht gewöhnungsbedürftige Syntax. Eine andere Variante wäre ls -lh *.pdf | sed 's/ / /g' | cut -d " " -f 1,3,5,9 Wir sehen hier, dass man auch mehr als eine Pipe benutzen kann. (Der sed-Befehl wird später noch ausführlicher diskutiert. Hier ersetzt er schlicht zwei Spaces durch eines, bevor dann diese Ausgabe erneut gepiped wird und so zu cut gelangt. Nun soll das Ganze noch in eine Textdatei. Heißt diese filelist.txt, so hilft einer der beiden Befehle ls -lh *.pdf | sed 's/ / /g' | cut -d " " -f 1,3,5,9 > filelist.txt ls -lh *.pdf | sed 's/ / /g' | cut -d " " -f 1,3,5,9 >> filelist.txt Das '>' bzw. '>>' sind sogenannte //Redirects//, mit denen die Ausgabe in eine Textdatei "umgeleitet" wird, deren Name eben filelist.txt ist. Der Unterschied zwischen den beiden Varianten besteht darin, dass '>' immer wieder die Zieldatei überschreibt (wenn sie schon existiert), während '>>' den jeweiligen Text (hier also die Ausgabe des vorangehenden Befehls) immer am Ende einer eventuell bereits bestehenden Textdatei anhängt. Kommen wir als nächstes zu 'less' und 'more', die beide "more or less" das gleiche tun, nämlich das, was sie ausgeben sollen, seitenweise darstellen. Das bedeutet, dass sie überprüfen, wie groß aktuell das Fenster ist, in dem sich die Shell befindet und dann nur die ersten Zeilen ausgibt, die in das Fenster passen. Beide bieten die Möglichkeit, mit '/' (wie bei den Manpages) nach Strings zu suchen und beide bieten die Möglichkeit, durch die gesamte Ausgabe zu navigieren. Und genau darin, mit welchen Tasten die Steuerung geschieht, unterscheiden sich die beiden Tools. Ich persönlich verwende lieber 'less', da die Steuerung der der Manpages ähnlicher ist, aber letztlich ist das Geschmackssache. more filelist.txt less filelist.txt würden also die Textdatei filelist.txt "seitenweise" ausgeben. Um als nächstes die Dateigröße von Geier 153 auszugeben, kann man nun entweder die Textdatei mit less oder more aufrufen und mit '/' nach 153.pdf suchen, oder aber direkt mit ls -lh 153.pdf Alternativ kann man auch die Ausgabe von 'ls -lh' nach less oder more pipen: ls -lh *.pdf | less Nun kann man wieder mit '/' suchen. Natürlich gibt es noch weitere Möglichkeiten. hier ging es primär darum, dass man less (oder more) sowohl per Pipe eine Ausgabe übergeben kann, als auch beide mit nachfolgendem Parameter "Textdatei" aufrufen kann. Am Ende noch kurz zu cat, das tatsächlich auch einfache Textdateien ausgeben kann. (Aber noch viel mehr!) cat filelist.txt # Ausgabe der Textdatei filelist.txt cat filelist.txt | less # Ausgabe der Textdatei filelist.txt mit less (Hier wie 'less filelist.txt') cat filelist.txt | grep "153" # Ausgabe der Textdatei filelist.txt, aber nur der Zeilen, die den String "153" enthalten grep "153" filelist.txt # Hier wie oben Hier lernen wir den (unglaublich nützlichen) Befehl 'grep' kennen, der Textdateien (ja, mehrere!) nach Strings durchsuchen kann. Man probiere zum Beispiel cp filelist.txt filelist2.txt grep "153" *.txt Wir erhalten nun zweimal das altbekannte Ergebnis, aber grep sagt uns auch, in welcher Datei es fündig geworden ist. Damit können wir die Suche nach der Dateigröße von Geier 153 noch verschönern: ls -l | grep "153.pdf" | cut -d " " -f 5 ====== Skripting: Ein erstes kleines Shell-Skript ====== Shell-Skripte können grundsätzlich jede Dateiendung haben, aber um sie sofort zu erkennen, bietet es sich an, die Erweiterung '.sh' zu verwenden. In diesem Beispiel heißt das Shell-Skript 'quekengetter.sh' Zunächst schauen wir uns die einzelnen Elemente des Skriptes an: #!/bin/bash ist die erste Zeile des Skriptes. In ihr wird festgelegt, dass alle weiteren Zeilen mit dem Programm '/bin/bash', also der BourneAgain-SHell, ausgeführt werden sollen. Hier kann natürlich auch eine andere Shell eingetragen werden; #!/bin/zsh oder #!/bin/sh funktionieren auch, sofern die Shells installiert sind und im Verzeichnis /bin tatsächlich auch liegen. (Hier gibt es zum Teil Unterschiede je nach verwendeter Linux-Distribution. ARCH Linux hat die Shells zum Beispiel zwischenzeitlich in /usr/bin gelegt und für Kompatibilität unter /bin verlinkt.) Kommen wir nun zu den nächsten Zeilen: # Dieses Shell-Skript erstellt einen neuen Unterordner, # wechselt in diesen, lädt ein neues Archiv vom Fachschaftsserver # und entpackt dieses Diese sind einfach Kommentare; zu erkennen an der vorangestellten Raute '#'. Man beachte den Unterschied zur ersten Zeile! dort steht '#!', was die Shell definiert, während '#' Kommentare anzeigt. Nun folgen die eigentlichen Befehle: mkdir queken cd queken wget https://www.fsmpi.rwth-aachen.de/pubwiki/images/8/86/Safari_queken.zip unzip Safari_queken.zip Diese sind bereits bekannt und deswegen erspare ich mir, hierauf dezidiert einzugehen. Nun bleibt allerdings die Frage: Wie startet man so ein Skript? Man kann einfach bash quekengetter.sh zsh quekengetter.sh sh quekengetter.sh ... am Prompt eingeben, aber dann wäre es relativ sinnlos, im Skript selber spezifiziert zu haben, womit es laufen soll. Eine schönere Variante (die auch für Binärdateien und eigentlich alles ausführbare funktioniert) ist ./quekengetter.sh Allerdings muss dafür das Shell-Skript als ausführbar markiert sein, was standardmäßig nicht der Fall ist. Ein 'ls -l' würde wohl etwa folgende Ausgabe haben: -rw-rw-r-- 1 owner group 203 Apr 22 02:06 quekengetter.sh Will man das Skript ausführen, so kann man mit dem Befehl chmod u+x quekengetter.sh das Attribut 'eXecutable' hinzufügen: -rwxrw-r-- 1 owner group 203 Apr 22 02:06 quekengetter.sh Nun kann der Besitzer 'owner' (und niemand - außer root - sonst) das Skript mit ./quekengetter.sh ausführen. Will man auch der zugewiesenen Gruppe 'group' oder allen andern Rechte zum Ausführen des Skriptes einräumen, so erledigt man dies mit chmod g+x quekengetter.sh chmod o+x quekengetter.sh Man kann auch kombinieren: chmod ug+x quekengetter.sh chmod go+x quekengetter.sh chmod ugo+x quekengetter.sh ====== Skripting: Variablen ====== Hat man das Skript 'lines.sh' angelegt und ausführbar gemacht ('chmod u+x lines.sh'), kann man es aufrufen mit ./lines.sh <(Text-)Datei> Gehen wir nun genauer die einzelnen Zeilen durch. #!/bin/bash ## Kennen wir schon. ZEILEN=$(cat $1 | wc -l) ## $(...) sagt, dass '...' in einer Shell ausgeführt werden soll und der Ausdruck die Rückgabe der Shell repräsentieren soll. ## Den Befehl 'cat' kennen wir schon. $1 steht für den **ersten** Parameter, der dem Skript übergeben wurde, ## Beim Aufruf mit './lines.sh <(Text-)Datei>' ist $1 also <(Text-)Datei>. Damit wird der Befehl zu //cat <(Text-)Datei>'// ## Die folgende Pipe ('|') leitet die Ausgabe von cat dann an wc, welches die Zeilen, Leerzeilen und Zeichen in einer ## Textdatei zählt. '-l' weist wc an, nur die Zahl der Zeilen auszugeben. $(...) liefert also die Zahl der Zeilen in ## der übergebenen Datei. ## ZEILEN=... definiert eine neue (oder redefiniert eine bestehende) Variable 'ZEILEN' und weist ihr den Wert ... zu. ## Hier wird also die Zahl der Zeilen der übergebenen Datei bestimmt und in der Variable 'ZEILEN' gespeichert. echo "Die Datei $1 hat ${ZEILEN} Zeilen." ## Dies ist einfach eine Ausgabe. Man kann die Inhalte der Variablen ausgeben mit vorangehendem $-Zeichen. ## Hier muss man allerdings auf den Kontext achten. in einer ""-Ummgebung funktioniert das anders als in //-Umgebungen// ## oder wenn man den String gar nicht mit Anführungszeichen klammert. (s. oben) ## Zu diesem Thema (Escaping) folgt unten noch mehr. Kommen wir nun zum Shell-Skript, das nach den Rechten schaut. Für die erste Teilaufgabe ist eine mögliche Lösung #!/bin/bash ls -1l | sed '1d' | grep "\-$1" | awk '{print $NF}' > dummy.foo ## sed '1d' truncates first line if [ "$(cat dummy.foo | wc -l)" == 0 ] then echo "No files matching pattern found." else cat dummy.foo | grep -v "dummy.foo" fi if [ -e dummy.foo ] then rm dummy.foo fi Das ist noch recht einfach und basiert nur auf bekanntem Gedöhns deswegen kommentiere ich das jetzt nicht erheblich weiter. Man achte allerdings auf den Backslash vor dem '-' in Zeile ls -1l | sed '1d' | grep "\-$1" | awk '{print $NF}' > dummy.foo Er deutet an, dass das folgende Zeichen "escaped" werden soll, was im Wesentlichen bedeutet, dass das nächste Zeichen normaler Weise alleine als irgendein Sonder- oder Steuerzeichen interpretiert würde, und das hier eben nicht geschehen soll. Escaping funktioniert je nach Kontext ein wenig unterschiedlich und ist eine beliebte Fehlerquelle. Es würde hier zu weit führen, sie hier en detail zu erklären. Am einfachsten ist es, wenn man hier "learning-by-doing" betreibt, aber dafür muss man eben //wissen// dass es dieses Escaping gibt. Des Weiteren sollte es zwischenzeitlich möglich sein, die Funktionalität der anderen Zeilen selber zu ermitteln oder auch einfach zu verstehen. ^^ Zu Task 2: #!/bin/bash if [ "$2" == "" ] || [ "$2" == "u" ] then echo "Searching for owner permissions." GREPSTRING="\-$1" elif [ "$2" == "g" ] then echo "Identifier for group permissions found." GREPSTRING="\-...$1" ## Dots are jokers for any char. elif [ "$2" == "o" ] then echo "Identifier for others' permissions found." GREPSTRING="\-......$1" else echo "Wrong parameter passed. Using default" GREPSTRING="\-$1" fi ls -1l | sed '1d' | grep "${GREPSTRING}" | awk '{print $NF}' > dummy.foo if [ "$(cat dummy.foo | wc -l)" == 0 ] then echo "No files matching pattern found." else cat dummy.foo | grep -v "dummy.foo" fi if [ -e dummy.foo ] then rm dummy.foo fi Task 3: #!/bin/bash if [ "$3" == "not" ] then GREPCMD="grep -v" else GREPCMD="grep" fi if [ "$2" == "" ] || [ "$2" == "u" ] then echo "Searching for owner permissions." GREPSTRING="\-$1" elif [ "$2" == "g" ] then echo "Identifier for group permissions found." GREPSTRING="\-...$1" elif [ "$2" == "o" ] then echo "Identifier for others' permissions found." GREPSTRING="\-......$1" else echo "Wrong parameter passed. Using default" GREPSTRING="\-$1" fi ls -1l | sed '1d' | ${GREPCMD} "${GREPSTRING}" | awk '{print $NF}' > dummy.foo ## You can also use $VARIABLE for commands if [ "$(cat dummy.foo | wc -l)" == 0 ] then echo "No files matching pattern found." else cat dummy.foo | grep -v "dummy.foo" fi if [ -e dummy.foo ] then rm dummy.foo fi ====== Skripting: Bilder ====== convert -delay 10 baby{0..26}.png animated.gif for f in *.png do convert -density 300 -units PixelsPerInch ${f} ${f%.png}.eps done for f in *.eps ; do epstopdf ${f} ; done pdftk baby{0..26}.pdf cat output combined.pdf ====== Shell-Konfiguration und Customization -- Aliase, Shell-Voids und die .rc ====== Da diese Aufgabe weitgehend Recherche ist, beschränke ich mich nur auf den letzten Punkt. publicip(){ wget http://checkip.dyndns.com/ -O - 2>/dev/null | grep "Current IP Address" | cut -d ":" -f 2- | sed 's###g' | sed 's/ //' } definiert die Shell-Methode 'publicip', die nach Definition mit einem simplen publicip in der Shell aufgerufen werden kann. Im deinem Homeverzeichnis liegt eine (versteckte) Datei .bashrc (bzw. .zshrc, etc.) in die Aliase, Shell-Voids, Variablen und so weiter geschrieben werden können. Bei jedem Aufruf der wird die .rc ausgeführt, sodass alles, was in dieser Datei drinne steht, standardmäßig in der Shell zur Verfügung stehen. ====== Programmieren: Parameterübergabe ====== Das Programm 'intro.cpp' kann zum Beispiel mit dem Compiler 'g++' compilliert werden: g++ intro.cpp Dies liefert ein ausführbares Programm mit dem Namen 'a.out', welches mit ./a.out gestartet werden kann. Optional kann man auch g++ intro.cpp -o zum compillieren verwenden, was als Ausgabe statt der 'a.out' eine Datei mit Namen '' zur Folge hat und mit ./ aufzurufen ist. Dieses Programm gibt im Wesentlichen zwei Textzeilen aus, aber während das eine eine "normale" Ausgabe ist, ist das andere ein "Error". Um nun beide Ausgaben in eine Datei zu leiten, ist ein simples '>' (redirect) nicht ausreichend. Dieses leitet nur die Standardausgabe weiter. Das impliziert bereits, dass es noch eine andere als die Standardausgabe 'stdout' gibt, nämlich 'Standard Error' ('stderr'). Um //beide// Ausgaben zu pipen, kann man '&>' benutzen: ./ &> output.txt. Für weitere Kombinationen von Umleitungen kann man sich auf die Website [[http://www.thomas-krenn.com/de/wiki/Bash_sdtout_und_stderr_umleiten|http://www.thomas-krenn.com/de/wiki/Bash_sdtout_und_stderr_umleiten]] begeben. Das Programm 'sequenziell.cpp' compilliert man analog mit g++ sequenziell.cpp -o sequenziell Der Aufruf erfolgt mit ./sequenziell .5 100000000 Dieses Programm ist primär dazu da um zu zeigen, wie man an ein selbstgeschriebenes Programm Parameter übergeben kann. Und Überraschung! Das geht wie bei Shell-Skripten. ^^ ====== Programmieren: Compilerflags ====== Das Programm 'parallel.cpp' benutzt die freie Parralelisierungsbibliothek OpenMPI, die zunächst auf dem System installiert sein muss. Falls nicht, kannst du es mit sudo apt-get install gcc versuchen. Das reicht aber noch nicht aus. Wenn man versucht, einfach mit g++ parallel.cpp -o parallel zu compillieren, erhält man (etwa) folgenden Fehler: /tmp/ccSwDJju.o: In Funktion `main': parallel.cpp:(.text+0x42): Nicht definierter Verweis auf `omp_get_max_threads' parallel.cpp:(.text+0xd0): Nicht definierter Verweis auf `omp_get_max_threads' parallel.cpp:(.text+0x13e): Nicht definierter Verweis auf `omp_get_thread_num' parallel.cpp:(.text+0x198): Nicht definierter Verweis auf `omp_get_max_threads' collect2: error: ld returned 1 exit status Das Problem ist, dass der Compiler nicht weiß, was er mit dieser Zeile tun soll: #include Hier würden die omp-Methoden deklariert und die entsprechende Datei wird schlicht nicht gefunden. Dies ändert man mit dem **Compilerflag** '-fopenmp': g++ -fopenmp parallel.cpp -o parallel Nun kann man mit ./parallel .5 100000000 das Programm aufrufen. Wenn man nun ein zweites Terminal startet kann man zum Beispiel mit 'htop' (//sudo apt-get install htop//) überprüfen, wie die Auslastung der eigenen Kerne ist. Die Laufzeiten verrät einem der Befehl 'time': time ./parallel .5 100000000 bzw.: time ./sequenziell .5 100000000