Filtern in Bash

Manchmal will man aus einer Variable oder einem Text alle Zahlen herausziehen oder verstecken. Das geht natürlich mit sed ganz gut, aber noch einfacher in der Bash selbst.

% VAR=1a2b3c4d
% echo "${VAR//[!0-9]/}"
1234
% echo "${VAR//[0-9]/}"
abcd

Man kann auch nur Buchstaben filtern, damit man Sonderzeichen mit herausfiltert. Oder oder…

% VAR="1a,2b.3c;4d"
% echo "${VAR//[!0-9]/}"
1234
% echo "${VAR//[!a-z]/}"
abcd
% echo "${VAR//[!a-z 0-9]/}"
1a2b3c4d
% echo "${VAR//[a-z 0-9]/}"
,.;
Filtern in Bash

HTML emails mit mail.mailutils

Das Programm mail.mailutils (meist einfach mail) ist auf vielen Linux-Distributionen installiert. Man kann damit über die Shell Emails verschicken, was sehr praktisch ist für scripte und cron-jobs, die den Admin per Mail informieren sollen.

Manchmal will man auch mal ein wenig mit HTML spielen und sei es auch nur, um um den Text ein <pre> zu setzen, damit er mit einer Monospace-Schrift angezeigt wird und ASCII-Art und Tabellen ordentlich dargestellt werden.

So schickt man HTML:

# generiere random boundary
BOUNDARY=$(date | md5sum | head -c8)
echo "
--_$BOUNDARY_
Content-Type: text/html; charset=\"us-ascii\"
Content-Transfer-Encoding: quoted-printable

<html><pre>
Meine Tabelle

+-------+----------+
| id    | analysis |
+-------+----------+
| 36190 |      219 |
| 36192 |      219 |
| 36273 |      219 |
| 36275 |       -1 |
| 36276 |       -1 |
| 36289 |      219 |
| 36293 |      122 |
| 36371 |      219 |
| 36388 |      219 |
| 36393 |      219 |
| 36394 |       -1 |
| 36395 |       -1 |
| 36708 |      122 |
+-------+----------+
</pre></html>
--_$BOUNDARY_--

" | mail.mailutils -s "Mein Subject" \
-a Content-Type:"multipart/alternative; boundary="\"_$BOUNDARY_\"" \
-a From:mein@absender.tld mein@empfaenger.tld

Diese Tabelle sieht jetzt sauber aus, wenn sie den Empfänger erreicht. Die Leerzeilen vor und nach dem Boundary und/oder Headern sind wichtig!

Wenn ich ein Bild in das HTML einbetten will, wird es komplizierter. Das mail-Tool kann zwar mit -A Anhänge verschicken, aber die kann ich dann nicht im HTML nutzen, da ich die dazugehörige ID nicht kenne.
Also muss ich das händisch machen, damit ich alle Parameter kontrollieren kann. Da Email ein 7-Bit Medium ist, muss ich aus dem Binär-Bild ASCII machen und dann noch dem Mailprogramm erklären, wie es das ASCII wieder in eine Binär-Bilddatei zurückverwandelt. Ich benutze hier base64 dafür, da das standardmässig installiert ist und so ziemlich jedes Mailprogramm das versteht. Ausserdem muss ich dem Bild einen Namen geben, denn der Dateiname geht verloren beim encoden in base64, und dazu eine ID, mit der ich darauf verweisen kann im HTML-Code der Mail. So gehts:

# generiere random boundary und Image ID
BOUNDARY=$(date | md5sum | head -c8)
IMAGEID=$(date | md5sum | head -c10 | rev)
# meine Bilddatei
IMAGEFILE=graph.png
( echo "
--_$BOUNDARY_
Content-Type: text/html; charset=\"us-ascii\"
Content-Transfer-Encoding: quoted-printable

<html>
Mein Graph als Bild:<br>
<img contenttype=3D\"image/png\" src=3D\"cid:$IMAGEID\"><br>
Sieht gut aus!

</pre></html>

--_$BOUNDARY_
Content-Type: image/png;name=\"$IMAGEFILE\"
Content-Disposition: attachment;filename=\"$IMAGEFILE\"
Content-ID: <$IMAGEID>
Content-Transfer-Encoding: base64 
"
base64 $IMAGEFILE 
echo "
--_$BOUNDARY_--" ) |  mail.mailutils -s "Mein Subject" \
-a Content-Type:"multipart/related; boundary="\"_$BOUNDARY_\""; type="\"multipart/alternative\""" \
-a MIME-Version:"1.0" \
-a From:mein@absender.tld mein@empfaenger.tld

Es gibt ausser mail.mailutils noch viele andere Commandline-Mailer wie etwa mutt oder mailx. Mit Hilfe der Beispiele oben kann man auch diese Mailer dazu bringen, HTML zu verschicken. Einfach mal gucken, welche Tools auf dem System installiert sind. Auch OS X kann das.

HTML emails mit mail.mailutils

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

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