Recently in Shell Category

expand - convert tabs to spaces

Ich habe gerade Loadbars v0.4.0.1 (maintenance version only, keine neuen features) released. Dabei habe ich auch alle Tabs durch 4 whitespaces ersetzt. Wie geht das? Ganz einfach:

expand --tabs=4 loadbars > new
mv new loadbars

Auch wenn man schon lange mit Linux gearbeitet hat, stoesst man immer wieder auf Kleinigkeiten, die neu sind. 

Ein anderes Beispiel: Die Formatierte Ausgabe des Alters einer Datei geht auch ohne stat sondern mit date +format -r datei. 

PS: Loadbars v0.5.0 wird wohl irgendwann naechste Woche released mit vielen neuen Features (RAM und Swap usage, evtl. auch Netzauslastung, sowie online Resizing des Fensters, uvm..). 

Bash expansions

Ohne einen konkreten Anwendungdfall zu nennen, Folgendes ist toll und praktisch:

pb@titania:~$ echo {0..4}
0 1 2 3 4
pb@titania:~$ echo {00..04}
00 01 02 03 04
pb@titania:~$ echo {a..e}
a b c d e

Etwas komplizierter, aber evtl. auch Praktisch ist die folgende Expansion:

pb@titania:~$ echo \"{These,words,are,quoted}\"
"These" "words" "are" "quoted"

Oder wie wärs mit einem Kreuzprodukt?

pb@titania:~$ echo {one,two}\ :{' A',' B',' C'}
one : A one : B one : C two : A two : B two : C

Come get here

Wie in vielen anderen Skriptsprachen unterstützt auch die Bash sog. Here-Dokumente. Hier ein kleines Beispiel: 

pb@titania:~$ cat <<END
> Hallo Welt
> it's $(date)
> END
Hallo Welt
it's Fr 13. Mai 11:07:36 CEST 2011
pb@titania:~$

Neben << kennt die Bash auch den Operator <<<. Während << für Here-Dokumente reserviert ist, wird <<< für sog. Here-Strings verwendet. 

So könnte man ohne einen Here-String prüfen, ob eine Variable einen bestimmte Substring enthält:

VAR=foo; if echo "$VAR" | grep -q foo; then echo \$VAR contains foo; fi

Und so mit Here-String:

if grep -q foo <<< "$VAR"; then echo \$VAR contains foo; fi

Wie unschwer zu erkennen ist spart man sich hier einiges an Tipparbeit (ein echo und eine Pipe weniger). (PS: Das könnte man auch ohne grep, nämlich mit Bash Regexp überprüfen, aber dazu evtl. später mehr).

Here-Strings können auch in Kombination mit read angewandt werden:

pb@titania:~$ dumdidumstring="Learn you a Haskell for Great Good"
pb@titania:~$ read -a words <<< "$dumdidumstring"
pb@titania:~$ echo ${words[0]}
Learn
pb@titania:~$ echo ${words[1]}
you
pb@titania:~$ echo ${words[2]}
a

Das -a bei read bezweckt, dass words aus dem Here-String als Array befüllt werden soll. 

Mittels Here-String kann man auch eine Zeile einer Textdatei prependen:

pb@titania:/tmp$ echo for Great Good > file.txt
pb@titania:/tmp$ cat - file.txt <<<"Learn you a Haskell"
Learn you a Haskell
for Great Good

Das hat allerdings den Nachteil, dass man das Ergebnis zuerst in eine temporäre Datei oder Variable schreiben muss, bevor man die Originaldatei file.txt überschreibt. Ansonsten kommt es zu einem Fehler:

pb@titania:/tmp$ cat - file.txt <<<"Learn you a Haskell" > file.txt
cat: file.txt: Eingabedatei ist Ausgabedatei


Natürlich wäre hierbei sed sowieso das bessere Tool der Wahl:

pb@titania:/tmp$ echo for great Good > file.txt
pb@titania:/tmp$ sed -i -e '1i\
Learn you a Haskell' file.txt
pb@titania:/tmp$ cat file.txt
Learn you a Haskell
for great Good
pb@titania:/tmp$

Restricted Bash

Manchmal möchte man nicht, dass man in der Bash jedes beliebige Kommando ausführen kann. Abhilfe schafft die Option -r:

pb@titania:~$ man bash | sed -n '/ -r/ { N; p; q; }'
-r If the -r option is present, the shell becomes restricted
 (see RESTRICTED SHELL below).

Sofern die Bash mit -r gestartet wird, wird folgendes verboten:

• cd um Verzeichnisse zu wechseln
• Umgebungsvariablen $PATH, $SHELL, $BASH_ENV, oder $ENV ändern.
• Lesen oder Schreiben von $SHELLOPTS
• Output redirection.
• Befehle ausführen die ein / beinhalten
• exec darf nicht ausgeführt werden 
• Diverse andere Sachen...
• Restricted mode verlassen

Hier ein kleines Beispiel aus der Advanced Bash Scripting Guide:

pb@titania:/tmp$ cat foo.sh 
#!/bin/bash
# Starting the script with "#!/bin/bash -r"
#+ runs entire script in restricted mode.
echo
echo "Changing directory."
cd /usr/local
echo "Now in `pwd`"
echo "Coming back home."
cd
echo "Now in `pwd`"
echo
# Everything up to here in normal, unrestricted mode.
set -r
# set --restricted has same effect.
echo "==> Now in restricted mode. <=="
echo
echo
echo "Attempting directory change in restricted mode."
cd ..
echo "Still in `pwd`"
echo
echo
echo "\$SHELL = $SHELL"
echo "Attempting to change shell in restricted mode."
SHELL="/bin/ash"
echo
echo "\$SHELL= $SHELL"
echo
echo
echo "Attempting to redirect output in restricted mode."
ls -l /usr/bin > bin.files
ls -l bin.files
# Try to list attempted file creation effort.
echo
exit 0

pb@titania:/tmp$ ./foo.sh

Changing directory.
Now in /usr/local
Coming back home.
Now in /home/pb

==> Now in restricted mode. <==


Attempting directory change in restricted mode.
./foo.sh: Zeile 19: cd: gesperrt
Still in /home/pb


$SHELL = /bin/bash
Attempting to change shell in restricted mode.
./foo.sh: Zeile 25: SHELL: Schreibgeschützte Variable.

$SHELL= /bin/bash


Attempting to redirect output in restricted mode.
./foo.sh: Zeile 31: bin.files: Gesperrt: Die Ausgabe darf nicht umgeleitet werden.
ls: Zugriff auf bin.files nicht möglich: Datei oder Verzeichnis nicht gefunden

Bash Redirection

Dieses Mal wollte ich etwas über "Bash Redirection" schreiben. Jeder Linux-Benutzer sollte bereits wissen, dass es diese drei Standarddateideskriptoren gibt:

  1. 0 aka stdin (Standardeingabe)
  2. 1 aka stdout (Standardausgabe)
  3. 2 aka stderr (Standarderrorausgabe)

Die meisten Programme arbeiten unter Linux mit stdin und stdout. stderr wird meist bei Fehlern verwendet. Die Shell hat einen entsprechenden Terminal Device wozu es einen Eintrag in /dev/pts/ gibt:

pb@titania:~$ ls -l /dev/pts/
insgesamt 0
crw--w---- 1 pb tty 136, 0 2011-05-08 10:33 0
crw--w---- 1 pb tty 136, 1 2011-05-08 10:26 1
crw--w---- 1 pb tty 136, 2 2011-05-08 10:27 2
crw--w---- 1 pb tty 136, 3 2011-05-08 10:27 3
c--------- 1 root root 5, 2 2011-05-08 09:57 ptmx

Mit dem > Operator kann man stdout auf das jeweilige Device umleiten:

pb@titania:~$ echo Foo > /dev/pts/0

Foo

Mit >& besteht eine weitere Möglichkeit die Ausgaben umzuleiten:

  • Leite stderr nach stdin um: echo foo 2>&1
  • Leite stdin nach stderr um: echo foo >&2

Mehrere Umleitungen scheinen jedoch nicht aufeinmal zu funktionieren. Z.B. sollte das folgende Kommando Foo nach stderr umleiten und anschliessend sollte stderr nach /dev/null umgeleitet werden. Statt dem letzten Schritt wirds lediglich auf stderr ausgegeben:

pb@titania:~$ echo Foo 1>&2 2>/dev/null
Foo

Das kann man mit einer Subshell beheben:

pb@titania:~$ (echo Foo 1>&2) 2>/dev/null
pb@titania:~$

Damit sind dann auch Konstrukte möglich wie:

pb@titania:~$ ( ( (echo Foo 1>&2) 2>&1 ) 1>&2) 2>/dev/null
pb@titania:~$ ( ( (echo Foo 1>&2) 2>&1 ) 1>&2) 2>/dev/pts/0
Foo

Mit lsof lässt sich herausfinden welcher Prozess bestimmte Dateideskriptoren geöffnet hat:

pb@titania:~$ lsof -a -p $$ -d0,1,2
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAM
E
bash 1895 pb 0u CHR 136,0 0t0 3 /dev/pts/0
bash 1895 pb 1u CHR 136,0 0t0 3 /dev/pts/0
bash 1895 pb 2u CHR 136,0 0t0 3 /dev/pts/0


Dabei hat $$ (pid der aktuell geöffneten Bash) die 0u (stdin) 1u (stdout) und 2u (stderr) geöffnet. Der aktuelle Bash-Prozess hat /dev/pts/0 als Terminal-Device.

Neben stdin, stdout und stderr kann man auch eigene Deskriptoren erstellen dafür wird das Bash-Builtin Kommando exec benötigt.

pb@titania:/tmp$ touch foo
pb@titania:/tmp$ exec 3> foo
pb@titania:/tmp$ echo bar >&3
pb@titania:/tmp$ cat foo
bar
pb@titania:/tmp$ exec 3>&-
pb@titania:/tmp$ echo bar >&3
bash: 3: Ungültiger Dateideskriptor

Hier wird eine leere Datei foo angelegt. Anschliessend wird mit exec der Dateideskriptor 3 an die Datei foo gebunden und mit echo der String bar in diese Datei mittels Deskriptor 3 geschrieben. Der befehl exec 3>&- schliesst den Dateideskriptor wieder. 

Es besteht die Möglichkeit die Standarddeskriptoren zu überschreiben wie das folgende Skript zeigt:

pb@titania:/tmp$ cat test.sh 
#!/bin/bash
# Write a file data-file containing two lines
echo Learn You a Haskell > data-file
echo for Great Good >> data-file
# Link fd with fd 6 (saves default stdin)
exec 6<&0
# Overwrite stdin with data-file
exec < data-file
# Read the first two lines from it
read a1
read a2
# Print it
echo First line: $a1
echo Second line: $a2
# Restore default stdin and delete fd 6
exec 0<&6 6<&-
pb@titania:/tmp$ ./test.sh
First line: Learn You a Haskell
Second line: for Great Good


In der Bash kann man mittels Redirection noch weitaus Komplizierteres anstellen.

Indirect References using the Bash

Auch wenn es aus meiner Sicht kein schöner Stil ist, können "Indirect References" der Bash durchaus nützlich sein. Das Ganze kann man anhand eines Beispiels am besten veranschaulichen:

pb@titania:~$ letter_of_alphabet=z
pb@titania:~$ a=letter_of_alphabet
pb@titania:~$ echo "a = $a"
a = letter_of_alphabet
pb@titania:~$ eval a=\$$a
pb@titania:~$ echo "Now a = $a"
Now a = z

Was passiert hier also? Es werden die beiden Variablen letter_of_alphabet und a definiert und anschliessend wird der Wert von a als Variablenname verwendet indem das ganze mit eval aufgerufen wird. 

Indirect referencing in Bash is a multi-step process. First, take the name of a variable: varname. Then, reference it: $varname. Then, reference the reference: $$varname. Then, escape the first $: \$ $varname. Finally, force a reevaluation of the expression and assign it: eval newvar=\$$varname.

Ein Anwendungsfall wäre z.B. das Einlesen von Konfigurationsdateien wie in der Advanced Bash Scripting Guide angegeben.

Desweiteren kann man hiermit Call-By-Reference anstelle von Call-By-Value Funktionsaufrufe definieren.

Using declare to type variables

In der Bash kann man mit declare Variablen typisieren. Man schaue sich das folgende Beispiel an:

#!/bin/bash 

# Declare Integer N (Calculate up to N fib nums)
declare -i N=10

# Declare a read only string variable
declare -r TEXT="The result is: "

# Declare array which will hold the results
declare -a RESULT

function fib () {
  # Declare local Integer
  local -i a=$1 b=$2 n=$3
  if ((n <= N)); then
    # Push result into array
    RESULT+="$b "
    # Calculate next fib
    a=$((a == 0 ? 1 : a))
    fib $b $((a + b)) $((n + 1))
  fi
}

fib 0 0 1
echo $TEXT $RESULT
Die bekantesten Typen die die Bash beherrscht sind "Funktion", "Integer", "String" und "Array". Bei Floats muss die Bash leider passen. Der Wert 2.3 wird z.B. einfach als String interpretiert. Was auch nicht geht: Explizit festlegen dass ein Array nur Integer-Werte speichern kann. Die Typisierung ist also nur rudimentär möglich. Typisierung in depth kann man auch in der Advanced Bash Scripting Guide nachlesen.

:

Ein Feature der Bash, für welches ich bisher kaum Verwendung fand ist die Leeranweisung : (Doppelpunkt). Separat ausgeführt passiert nämlich gar nichts:

pb@titania:~$ :
pb@titania:~$

Praktisch wird die Leeranweisung bei Kontrollkonstrukten. Z.B. kann man eine Endlosschleife wie folgt realisieren:

while : ; do sleep 1; date; done

Oder man kann bei if-else Konstrukten einen Platzhalter definieren, den man später mit etwas Sinnvollem befüllt:

if foo; then : ; else echo bar ; fi

Die Leeranweisung kann man auch als Kommentar missbrauchen. Denn

: Ich bin ein Kommentar

kann man genauso gut schreiben wie

# Ich bin ein kommentar

Allerdings sollte man für Kommentare weiterhin # verwenden, denn folgendes wäre ein Kommentar welches einen Error wirft:

pb@titania:~$ : Ich bin ein Kommentar, (
bash: Syntaxfehler beim unerwarteten Wort `('

Da wo man die Leeranweisung sehr wohl verwenden könnte/sollte wäre das folgende Szenario:

pb@titania:~$ ${username=`whoami`}
pb: Befehl nicht gefunden

Hier wird geprüft, ob die Variable $username schon gesetzt ist. Wenn nicht, soll die Variable mit der Ausgabe des Befehls whoami befüllt werden. Anschließend wird jedoch von der Bash probiert, das Ergebnis wiederum als Kommando auszuführen, was zu einem Fehler führt, da es kein Kommando pb gibt.

Die Leeranweisung kann das Ausführen des Kommandos pb verhindern:

pb@titania:~$ echo -n $username
pb@titania:~$ : ${username=`whoami`}
pb@titania:~$ echo $username
pb

Ähnliche Konstrukte die einen Fehler werfen, sofern min. eine Variable nicht gesetzt ist, können auch gebaut werden:

pb@titania:~$ : ${HOSTNAME?} ${USER?} ${MAIL?}
bash: MAIL: Parameter ist Null oder nicht gesetzt.

Der : hilft auch bei der (mittlerweilen veralteten) Notation für Integerberechnungen:

pb@titania:~$ $[ n = $n + 1 ]
1: Befehl nicht gefunden
pb@titania:~$ : $[ n = $n + 1 ]
pb@titania:~$ : $[ n = $n + 1 ]
pb@titania:~$ echo $n
3

Advanced Bash Scripting Guide

Ich kann allen Linuxern und sonstigen Konsolenheinies (Konsolenheini wurde ich übrigens früher immer während meines Infostudiums von einem Maschinenbau-Hiwikollegen am Fraunhofer Institut genannt) die die Bash verwenden die Advanced Bash Scripting Guide empfehlen! Dieses über 800 seiten großes Dokument bietet selbst noch erfahrenen Bashbenutzern einiges Neues. Man kann sich die PDF auch von meinem pub-FTP herunterladen. Nun bin ich mir wirklich nicht mehr sicher, ob man wirklich noch eine ZSH benötigt. Die ZSH scheint noch mehr Features als die Bash zu besitzen, aber wenn man nichteinmal alle Features der Bash kennt.... Bleibt wohl Geschmackssache :)

Ich denke, dass ich in den nächsten Tagen ein paar nette Features hier bloggen werde.

PS: Mein Bruder empfahl mir, mal ein paar OpenBooks hier vorzustellen, die ich heimlich in meiner E-Library verlinkt habe.

TCP IP Networking using the Bash


Die Bash eignet sich neuerdings auch als rudimentären Netcat-Ersatz. Um die Zeit von time.nist.gov port 13 auszulesen, genügt der folgende Befehl:

pb@titania:~$ cat </dev/tcp/time.nist.gov/13
55675 11-04-24 09:10:50 50 0 0 0.0 UTC(NIST) * 


Einen HTTP-GET Request kann man z.B. wie folgt absetzen:

pb@titania:/dev$ exec 5<>/dev/tcp/comp.buetow.org/80
pb@titania:/dev$ echo -e "GET / HTTP/1.0\n" >&5
pb@titania:/dev$ cat <&5
HTTP/1.1 301 Moved Permanantly
Date: Sun, 24 Apr 2011 08:22:21 GMT
Server: Apache/2.2.17 (FreeBSD) mod_ssl/2.2.17 OpenSSL/0.9.8q DAV/2 PHP/5.3.5 with Suhosin-Patch SVN/1.6.16 mod_perl/2.0.4 Perl/v5.10.1
Set-Cookie: session=006059f2c8c6778c17dae9e5dfad755d; domain=; path=/
Location: http://www.buetow.org
Content-Length: 0
Connection: close
Content-Type: text/plain


Allerdings sollte man im Hinterkopf behalten, dass das Device /dev/tcp in echt gar nicht exisitert. 

pb@titania:/dev$ ls | grep dev
pb@titania:/dev$

Die Bash interpretiert die Eingabe /dev/tcp einfach entsprechend. Aus meiner Sicht wäre es schöner, wenn es dafür ein separates Pseudo-Dateisystem gäbe. Sonst ist Verwirrung vorprogrammiert. So kann man vergeblich versuchen /dev/tcp/... innerhalb anderer Sprachen/Shells zu verwenden :)

Pages

Powered by Movable Type 4.35-en

About this Archive

This page is an archive of recent entries in the Shell category.

Servers is the previous category.

Shell Scripting is the next category.

Find recent content on the main index or look in the archives to find all content.