Benutzer-Werkzeuge

Webseiten-Werkzeuge


kiss:shell-safari_losungen

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 '/<STRING>' die Manpage nach '<STRING>' durchsuchen und mit 'q' die Manpage wieder verlassen.

Der Parameter

  1. -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:

  1. rw-r–r– 1 owner group 263K Apr 26 2007 001.pdf
  2. rw-r–r– 1 owner group 328K Apr 26 2007 002.pdf
  3. rw-r–r– 1 owner group 317K Apr 26 2007 003.pdf
  4. rw-r–r– 1 owner group 393K Apr 26 2007 004.pdf
  5. 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:

  1. rw-r–r– owner 263K 2007
  2. rw-r–r– owner 328K 2007
  3. rw-r–r– owner 317K 2007
  4. rw-r–r– owner 393K 2007
  5. rw-r–r– owner 428K 2007

  1. rw-rw-r– owner 221K 064.pdf
  2. rw-rw-r– owner 105K 065.pdf
  3. rw-rw-r– owner 205K 066.pdf
  4. rw-rw-r– owner 209K 067.pdf
  5. 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:

  1. 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:

  1. 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 .<SHELL>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#</body></html>##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 <SHELL> wird die .<SHELL>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 <OUTPUTFILE>

zum compillieren verwenden, was als Ausgabe statt der 'a.out' eine Datei mit Namen '<OUTPUTFILE>' zur Folge hat und mit

./<OUTPUTFILE>

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:

./<OUTPUTFILE> &> 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 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 <omp.h>

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

kiss/shell-safari_losungen.txt · Zuletzt geändert: 2014/04/24 18:56 von jbergner