find nur eins

Ich musste in einem Ordner alle Dateien durchsuchen nach einem String, interessierte mich aber nur dafür, ob es überhaupt eine Datei gibt, die diesen String hat. Leider waren es zu viele Dateien für ein fgrep *, also musste ich mit find und exec arbeiten. Aber da find eine Schleife durch alle gefundenen Dateien macht, muss man es irgendwie nach dem ersten Treffer abwürgen. So gehts:

find . -type f -exec bash -c 'fgrep -lv <string> {} && kill $PPID ' \;
find nur eins

sudo-matisch

Mit sudo kann man mit den Rechten eines anderen Users Befehle ausführen. Üblicherweise ist das root und wird gebraucht, um ohne root zu sein, root-Rechte zu bekommen. Andere User gehen aber so auch: sudo -u username <command>

Welche Benutzer mit sudo andere Rechte erlangen dürfen, steht in der Datei /etc/sudoers, die man nur mit dem Befehl visudo bearbeiten sollte. Je nach OS und Distribution findet man hier andere Voreinstellungen. Auf meinem Mac habe ich mir etwa einen unpriviligierten User angelegt (ist sicherer), aber mir in der sudoers-Datei trotzdem alle Rechte zugestanden (ist komfortabler).

# User privilege specification
eliyah ALL=(ALL:ALL) ALL

Aber sudo lässt sich auch für andere Dinge nutzen, etwa Scripte. Man will nicht immer ein Script, das für nur einen Befehl root-Rechte benötigt, komplett als root laufen lassen. Etwa, weil es von einem anderen Prozess, der selbst keine root Rechte besitzt, getriggert wird. Ein Beispiel sind cgi’s für den Webserver. Ein anderes sind per ssh remote ausgeführte Kommandos.

Ausserdem will man in scripten oder remote kein Passwort eintippen müssen, wenn man per sudo Befehle ausführt, will aber in seinem System nicht gleich für alle Tür und Tor öffnen.

Der Trick ist, Befehle zu definieren, die ein User ohne Passwort ausführen darf. Das geht über die Cmnd_Alias Funktion. Hier im Beispiel eine Befehlsdefinition, der es einem User erlaubt, Passworte für andere User zu setzen und einer, der erlaubt, per tcpdump Traffic mitzulesen:

# Cmnd alias specification
Cmnd_Alias PASSWD = /usr/bin/passwd [A-z]*, !/usr/bin/passwd root
Cmnd_Alias TCPDUMP = /usr/sbin/tcpdump

Was man hier schön sehen kann ist, dass man nicht nur Programme, sondern auch Parameter bestimmen kann, die erlaubt sind. Mit dem Ausrufezeichen (!) kann man auch Dinge wieder ausschliessen, hier eben das Ändern des Passwortes für den User root.

Wenn der Befehlalias definiert ist, kann man bestimmten Usern diese Befehle erlauben, sogar ohne Eingabe des Passwortes.

# User privilege specification
eliyah ALL=(ALL:ALL) PASSWD
eliyah ALL=(ALL) NOPASSWD: TCPDUMP

Die verschiedenen ALLs erlauben, von wo aus man das darf. Wie im Detail, weiss ich nicht, interessiert mich auch nicht. Denn ich kann jetzt ein Script schreiben, das als User eliyah läuft und mit Hilfe von sudo ohne Passwortabfrage einen tcpdump machen kann.

sudo-matisch

OS X: Wer lauscht denn da?

Unter GNU Linux gibt es den Befehl netstat, mit dem man sich nicht nur anzeigen lassen kann, welche Ports gerade aktiv sind. Mit dem Parameter -pl (p für Prozess-ID und l für listen) sieht man auch, welcher Prozess gerade eine Tür geöffnet hat. Unter OS X ist das nicht ganz so einfach. Dort gibt es die BSD Variante von netstat, und die funktioniert anders. Mit folgendem Befehl sieht man, welche Ports geöffnet sind:

netstat -a | fgrep LISTEN

Bei mir erscheint da unter anderem etwa folgender Eintrag:

tcp4 0 0 192.168.2.2.54045 *.* LISTEN

Wer macht denn da Port 54045 auf und habe ich das erlaubt? Ich muss feststellen. Ja habe ich:

% lsof -nP -iTCP:54045 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Skype 36967 eliyah 30u IPv4 0xf42576a198a9cfbb 0t0 TCP 192.168.2.2:54045 (LISTEN)

Denn ich habe Skype installiert, und das macht bekanntlich, was es will. Der Befehl lsof (List Open Files) bringt es zum Vorschein.

Ich habe bei meinem Mac auch ssh aktiviert, und wenn ich schaue, welcher Prozess diesen Dienst bereit stellt, kommt überraschendes zu Tage:

% sudo lsof -nP -iTCP:22 -sTCP:LISTEN
Password:
COMMAND PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
launchd   1 root   50u  IPv6 0xf42576a168a5fb6b      0t0  TCP *:22 (LISTEN)
launchd   1 root   51u  IPv4 0xf42576a168a676eb      0t0  TCP *:22 (LISTEN)
launchd   1 root   53u  IPv6 0xf42576a168a5fb6b      0t0  TCP *:22 (LISTEN)
launchd   1 root   54u  IPv4 0xf42576a168a676eb      0t0  TCP *:22 (LISTEN)

Das sudo ist nötig, da hier ein Port unter 1023 abgefragt wird. Der ssh-Dienst wird vom launchd selbst bereitgestellt. Das funktioniert ähnlich wie inetd oder xinetd unter Linux, nur dass launchd deutlich weitreichendere Aufgaben hat, was man schon an seiner Prozess-ID 1 erkennt.

OS X: Wer lauscht denn da?

Alles so schön bunt hier!

Um in der Shell bunten Text auszugeben, kann man echo oder printf nutzen. Bei ersterem muss man die erweiterten Fähigkeiten mit -e aufrufen.

Hier ein Beispiel für farbigen Text mit fetten Buchstaben und normalen Buchstaben ohne Farbe:

echo -e "\e[1;31mRot\e[0m Normal \e[1;32mGruen \e[0m"

Das Ergebnis ist:

Rot Normal Gruen

Hier ein Beispiel für gelben Text blau hinterlegt und hunterstrichen:

echo -e "\e[4;44;33mGelb blau hinterlegt und unterstrichen\e[0m"

Das Ergebnis ist:

Gelb blau hinterlegt und unterstrichen

Zur Erklärung:

Mit \e[ leitet man die Farb und Formatierungscodes ein. Danach kommen in beliebiger reihenfolge Codes für Zeichen- und Hintergrundfarbe und Formatierung durch Semikolon unterteilt. Danach ein m zum Abschluss. Am Ende des Textes folgt noch ein \e[0m um alle Formatierungen wieder aufzuheben, da sonst aller nachfolgender Text, also auch die übliche Console, eingefärbt bleibt. Hier die Tabellen für die Farben und Formatierungen:

 

Farbe Zeichen Hintergrund
Schwarz 30 40
Rot 31 41
Grün 32 42
Gelb 33 43
Blau 34 44
Magenta 35 45
Cyan 36 46
Weiss 37 47

 

Code Formatierung
0 Normal, Aufheben von allem
1 Fett
2 Gedimmt
3 Kursiv
4 Unterstrichen
5 Blinkend (funktioniert nicht überall)
7 Farbumdrehung (einfach mal ausprobieren!)
8 Unsichtbar
9 Durchgestrichen
Alles so schön bunt hier!

Spielen mit Variablen

Variablen machen ein Script schon fast zu einem Programm. Daher sollte man sich ein wenig mit den Möglichkeiten auseinandersetzen. Man spart sich dann Aufrufe von Hilfsprogrammen wie awk, sed oder cut.

  1. Default-Wert: Wenn man eine Variable etwa in einer Operation verwendet und die Operation einen Wert erwartet, die Variable aber u.U. leer sein kann, dann muss man einen Default-Wert definieren. Ich mache das üblicherweise so, dass ich die Variable vorher mit dem Default-Wert setze, damit sie in jedem Fall existiert. Aber: Das muss man nicht! Man kann die Variable auch so ausgeben:
    $ echo $N
    
    $ echo ${N-0}
    0
    $ N=1
    $ echo ${N-0}
    1

    Hier wird der Default-Wert ausgegeben, ohne die Variable selbst zu ändern. Will man in dem Fall, dass die Variable ungesetzt ist, sie mit einem Wert setzen, geht das mit :=

    $ echo $N
    
    $ echo ${N:=0}
    0
    $ echo $N
    0
    
  2. Manchmal benötigt man nur einen Teil einer Variable, etwa die ersten drei Zeichen. Man kann dann mit ‚cut‚ arbeiten oder andere komplizierte Lösungen finden, oder man nutzt einfach einen entsprechenden Aufruf der Variable:
    $ N=123456789
    $ echo $N
    123456789
    $ echo ${N:0:3}
    123
    $ echo ${N:2:3}
    345
    $ echo ${N:2:-3}
    3456

    Der erste Fall gibt drei Zeichen der Variable, beginnend mit Position 0 (Anfang) und der zweite Fall beginnend mit Position 2 (drittes Zeichen). Der dritte zeigt alle Variablen ab Stelle 2 und bis zieht die letzten drei ab.

  3. Die Länge einer Variable ist auch oft interessant:
    $ N=123456789
    $ echo ${#N}
    9
    

    Und damit kann man auch den Anfang einer Variable nach Beispiel in 2. beschneiden (auch wenn #N hier sogar zu groß ist, aber es ist niemals zu klein):

    $ N=123456789
    $ echo ${N:2:${#N}}
    3456789
  4. Suchen und Ersetzen/Löschen geht auch:
    $ N=123456789
    $ echo ${N//2/3}
    133456789
    $ echo ${N//2/}
    13456789
    

Falls ich über weitere spannende Variablenspielerreien stolpere, werde ich darüber berichten!

Spielen mit Variablen

Sekunden umrechnen und anzeigen

Ich habe ein Script mit einer Schleife geschrieben, das ziemlich lange läuft und auch vorher weiss, wie lange es ungefähr laufen wird. Ich will dem Anwender sagen, wie viel Geduld er noch aufbringen muss.

Erst schreibe ich eine Funktion, die die Umrechnung in Tage, Stunden und Minuten macht:

function DisplayTime {
  local T=$1
  local D=$((T/60/60/24))
  local H=$((T/60/60%24))
  local M=$((T/60%60))
  local S=$((T%60))
  [[ $D > 0 ]] && printf '%d days ' $D
  [[ $H > 0 ]] && printf '%d hours ' $H
  [[ $M > 0 ]] && printf '%d minutes ' $M
  [[ $D > 0 || $H > 0 || $M > 0 ]] && printf 'and '
  printf '%d seconds\n' $S
}

Ich nutze in der Benamsung von Funktionen immer eine Kombination aus Groß- und Kleinbuchstaben, damit ich sie als Funktion erkenne und sie nicht mit einem Systembefehl oder einer Variable verwechsle.

Dann schreibe ich einen Echo-Befehl in die Schleife, der seinen Output selbst ersetzt und benutze dafür die obige Funktion. Voraussetzung ist natürlich, dass ich die Variable $SEKUNDEN vorher im Skript irgendwie errechnet habe.

echo -ne "time remaining: $(DisplayTime $SEKUNDEN)                        \r"

Das „\r“ mit dem -e zusammen sorgt dafür, dass die Zeile überschrieben wird. Die Leerzeichen sind nötig, da der Output von der Funktion mit der Zeit kürzer wird und die Zeile sonst nicht  komplett überschrieben wird. Wenn jemand eine bessere Lösung als die Leerzeichen hat, dann her damit!

Sekunden umrechnen und anzeigen

output von mehreren Kommandos durch eine Pipe

Wenn man den output von mehreren Kommandos gemeinsam durch eine Pipe (|) senden will, muss man keine komischen Konstrukte mit dem Befehl cat bauen (kann man aber), sondern es reichen dafür geschweifte Klammern:

$ { echo 1; echo 2; echo 3 ; }
1
2
3
$ { echo 1; echo 2; echo 3 ; } | fgrep 3
3
$ { echo 1; echo 2; echo 3 ; } | fgrep 2
2
$ { echo 1; echo 2; echo 3 ; } | fgrep 1
1

Man kann anstelle der Semikolon (;) auch neue Zeilen beginnen. Wichtig ist, dass vor der letzten Klammer entweder eine neue Zeile oder eben ein Semikolon steht.

output von mehreren Kommandos durch eine Pipe

trap ctrl-c in bash

Manche Scripte laufen lang, manche sogar so lange, bis sie extern unterbrochen werden, etwa durch ein ctrl-c, das ein SIGINT an das Script sendet. Dieses Script tut gar nichts und läuft und läuft und läuft…

while true; do :; done

Mit einem ctrl-c beende ich den Spuk. Wenn ich aber vor dem Beenden noch einen Befehl absenden will, muss ich das SIGINT abfangen. Und am einfachsten fängt man mit einer Falle, einem trap:

#!/bin/bash

trap BeendeMich SIGINT

function BeendeMich {
echo ""
echo "Du hast ctrl-c gedrueckt! Ich verschwinde..."
exit 0
}

while true; do :; done

Das ist der output des Beispiels:

^C
Du hast ctrl-c gedrueckt! Ich verschwinde...

Man muss keine Funktion aufrufen, man kann auch direkt  Befehle hinter trap eingeben von Anführungszeichen umgeben. Da aber immer mindestens zwei Befehle übergeben werden sollten (Neben dem eigenen noch ein abschliessendes exit, denn sonst beendet sich das script nicht), ist eine Funktion sauberer. Die kann man etwa nutzen, um Lockfiles oder andere temporäre Dateien zu löschen, bevor das Script beendet wird.

trap kann auch noch mehr SIGs abfangen. Einfach mal folgenden Befehl eingeben und man bekommt eine Liste mit SIGs angezeigt:

trap -l

Man kann mehrere SIGs hinter das trap schreiben. Wenn man etwa alle üblichen Unterbrecher abfangen will, dann geht das im obigen Beispiel so:

trap BeendeMich SIGINT SIGTERM SIGHUP
trap ctrl-c in bash

Exit code in einer pipe

Zugegeben, der Titel klingt nach Drogenmissbrauch, aber es geht um etwas anderes: Um exit codes oder auch status codes genannt.

Programme beenden sich in der Regel mit einem exit code. Das ist im Normalfall 0 für „OK“ und 1 für „Fehler“. Manche Programme kennen noch viele andere exit codes, die beispielsweise die Art des Fehlers dokumentieren. Man kann das selbst in Scripts benutzen und mit dem Befehl exit N, wobei N für den exit code steht, setzen.

Den exit code eines Programms kann man sich mit echo $? anzeigen lassen. Diese Variable gilt immer nur für das letzte beendete Programm. Beispiele:

$ true; echo $?
0
$ false; echo $?
1

Was aber, wenn man den exit code eines Programms haben will, dessen output man durch eine Pipe (|) schickt, etwa um ihn mit grep zu filtern. Das Problem ist dann, dass man den exit code von grep bekommt, der in der Regel 0 ist und nicht den des Programmes am Anfang der Pipe, der einen interessiert. Beispiele:

$ true | true; echo $?
0
$ false | true; echo $?
0

Die Lösung, zumindest in der bash, ist die Variable $PIPESTATUS. Sie ist ein Array mit allen exit codes einer Pipe. Beispiele:

$ false | true | false | true; echo "${PIPESTATUS[0]}"
1
$ false | true | false | true; echo "${PIPESTATUS[3]}"
0
$ false | true | false | true; echo "${PIPESTATUS[@]}"
1 0 1 0
Exit code in einer pipe

dynamische Variablen mit eval

Ich habe ein Script geschrieben, das sequenziell Variablennamen vergibt. Es ging mir darum, eine Schleife zu bauen und für jeden Durchlauf der Schleife eine neue Variable zu setzen. Später will ich diese Variablen natürlich wieder auslesen. Das klingt einfacher, als es ist. Damit das in einer bash funktioniert, benötigt man den Befehl eval.

Das Dumme ist nur, der Befehl hat gar keine man page, auf der man nachlesen könnte, was er macht und wie er es macht. In einem Satz kann man sagen: eval evaluiert deinen Ausdruck ein weiteres Mal vor der Ausführung.

Angenommen, ich möchte die Variablen X0 bis X3 setzen und wieder auslesen. So geht es nicht:

for N in $(seq 0 3); do
  X$N=hallo$N
  echo $X$N
done

Der output davon ist:

X0=hallo0: command not found
0
X1=hallo1: command not found
1
X2=hallo2: command not found
2
X3=hallo3: command not found
3

Mache ich aber ein eval davor, ein paar Klammern und Slashes dazu, dann klappt es:

for N in $(seq 0 3) ; do
  eval X$N=hallo$N
  eval echo \${X$N}
done

Der Output ist:

hallo0
hallo1
hallo2
hallo3

Das funktioniert übrigens auch mit arrays. Die dafür nötigen eckigen Klammern [] müssen vor die Abschliessende geschweifte bei der Ausgabe.

dynamische Variablen mit eval