end | enc | end | end | Inhalt | Uebungen | Complete | Kommentar

one_file

1.  Shell-Programmierung

Hans-Peter Bischof
Fachbereich Mathematik-Informatik
Universität Osnabrück.

Diese Vorlesung ist eine Einführung in die Benutzung von UNIX (Terminal, tty, Dateien und Prozesse) auf der Ebene von Kommandosprachen wie der Bourne-Shell, unter Berücksichtigung der wichtigsten Dienstprogramme.

Dieser Band enthält Kopien der OH-Folien, die in der Vorlesung verwendet wurden. Diese Information steht außerdem im Web (http://zeus/~sp) online zur Verfügung; sie ist in sich und mit der Systemdokumentation über das WWW verbunden.

Die Beispielprogramme werden maschinell aus diesem Text extrahiert. Der Band stellt kein komplettes Manuskript der Vorlesung dar. Zum Selbststudium müßten zusätzlich Bücher über das UNIX-System und speziell über Shell-Programmierung konsultiert werden.

Das Skript hält sich an das Skript von Prof. Axel T. Schreiner aus dem Jahre 1994, größere Passagen wurden daraus entnommen.

Enthaltene Fehler sind ausschließlich mir anzulasten. Es wäre schön, wenn diese an mich berichtet werden würden.

1.1.  Termine

Vorlesung:     Dienstag     10:15 - 12.00   31/449a   Hans-Peter Bischof
               Donnerstag   10.15 - 12.00   31/449a
Sprechstunde   Dienstag     8.30  - 10.00   31/318b
Übungen        Montag       10:15 - 12.00   31/449a   Bernd Kühl

1.2.  Wer

Vorlesung:

Hans-Peter Bischof
Büro:	31/318b
email:	bischof@informatik.uni-osnabrueck.de
Telefon:	0541 / 969 2534
Web:	http://thor.informatik.uni-osnabrueck.de/bischof/hp.d.html
Übung:

Bernd Kühl
Büro:	31/317
email:	bernd@informatik.uni-osnabrueck.de
Telefon:	0541 / 969 2487
Web:	http://thor.informatik.uni-osnabrueck.de/bischof/bernd.d.html

1.3.  Kalender

15/4
Einführung
17/4
Nan. C. Schaller trägt im Rahmen des IFC's vor.
22/4
login, /etc/passwd(5), NIS, Prozeßgruppen, rsh, rlogin, telnet, ttytab(5), und u.a. ftp.
24/4
Ports, ftp im Batch-Betrieb passwd, newgrp, NIS versus lokal, cal, date, Verwendungsmöglichkeit der Kommandos für Graphik-Programme. df, du und Plattenlayout. id, who, logname.
29/4
mail und wie man sie fälschen kann; mesg, talk und news; who, last; chmod, chown, chgrp, sticky-Bit, s-Bit; symbolische und harte Links (ln); Fifos (mknod), charakter- und Blockgeräte, Sockets;
1/5
1. Mai
6/5
Aufbau eines Shell-Skripts, exit-Code etc; rm, mv, cp, touch, find, compress.
8/5
Christi Himmelfahrt
13/5
Archivkommandos (cpio, tar); Implementierung eines kleinen Backup-Skriptes disk dump (dd); diff; Patchwork mit ed
15/5
lnterschiede in Text-Dateien: (diff, comm);
Pattern finden in Dateien: ([ef]?grep);
Zersplitten: (csplit, split, cut)
Zusammenfügen: (join, paste)
at
20/5
Pfingstferien
22/5
Pfingstferien
27/5
Prozesse, kill, Prozeßverwaltung im Kern, Harware/Software-interrupts, nice, time ps, vmstat, und die Möglichkeiten Prozesse/dem Betriebssystem Information zu entlocken.

expr, und test.

29/5
Dateisysteme, Peripherie-Geräte, Platten-Aufteilung, Plattenverwaltung, Adreßverwaltung, inodes, mount, /etc/fstab, /etc/fstab, automount, fsck.
3/6
Muster sed etc.
5/6
Was ist ein Kommando?, Textersatz, IFS, Dateinamen, set Argumentbearbeitung, spezielle Shellvariablen, Signale und Shellskripte,
10/6
Signale, Kontrollstrukturen, eingebaute Kommandos.
12/6
Archivierung von Dateien (u_in, u_ls, u_info, u_get, u_edit), sccs, rcs.
17/6
awk
19/6
awk und mk_gen.
24/6
Aussicht in Richtung Betriebssysteme
26/6
Beispiele I
1/7
Beispiele II
3/7
??
8/7
imake
10/7
wird laufend ergänzt.

1.4.  Literatur

Diese Folien befinden sich

in PostSkript in:   /home/sp/PS/c.[01][0-9]ps
in HTML in:         /home/sp/public_html/*
gedruckt im:        Semesterapparat.

Die Programme finden sich unter /home/sp/Skript/Src

Es gibt heute sehr viele Bücher über UNIX und Shell-Programmierung. Entscheidend ist dabei weniger, daß ein Buch möglichst viele Kommandos erklärt, sondern, daß die Möglichkeiten der Zusammenarbeit erläutert werden. Die folgenden Bücher sind nützlich. Soweit vorhanden, befinden sie sich in der Lehrsammlung.

Bourne               The UNIX System   (viele Auflagen)
Gulbins              3-540-13242-2     Unix
Kerninghan/Ritchie   3-446-15497-3     Programmieren in C
Kernighan/Pike       3-446-14273-8     Der UNIX-Werkzeugkasten
Zu einzelnen Themen:

Aho/Kernighan/Weinberger   0-201-07981-X   The AWK Programming Language
Schreiner                  3-446-14894-9   UNIX Sprechstunde
Wall/Schwartz              0-937175-64-1   Programming perl

Modernere Aspekte der Bourne-Shell wurden in Artikeln in der unix/mail behandelt.

Die Manual-Seiten befinden sich online und können mit dem Kommando man(1) oder auf einer NeXT mit dem Librarian betrachtet werden.

1.5.  Standards

``Das Schöne an UNIX-Standards ist, daß es so viele gibt, daß man sich seinen eigenen aussuchen kann.''

Berkeley

kein Standard; von der University of California at Berkeley zu SUN.
POSIX
(Portable Operating System Interface)
USA.
SVID

System V Interface Definition von AT&T
XPG

X/OPEN Portability Guide, vor allem im europäischen Bereich.

Die drei letzteren sind sich bei naiverer Benutzung ziemlich ähnlich.

2.  Terminal

Früher wurde ein Rechner von einem separaten Terminal aus benutzt, das über eine serielle Schnittstelle (wenigstens 3 Drähte für full-duplex Betrieb) angeschlossen war. Heute ist am PC oder an der Workstation der Bildschirminhalt meistens direkt im Hauptspeicher und wir verwenden ein Programm zur Terminal-Emulation, aber es gibt nach wie vor serielle Anschlüsse für Modems oder an Terminal-Servern:

Kommandos ruft man zunächst im Dialog auf. Dazu hat man entweder ein Terminal oder ein Fenster mit einer Terminal-Emulation. Letztlich holt das Betriebssystem Zeichen von der Tastatur ab und schreibt Zeichen auf den Bildschirm. Eine Shell, das heißt, ein Kommando-Interpreter, -- z.B. ksh(1), bash(1), sh(1), csh(1) oder ... -- liest Zeilen und führt die darin beschriebenen Kommandos meistens als eigene Prozesse aus. Siehe auch tty(4).

Eine primitive Shell könnte wie folgt aussehen:

 1      #include <stdio.h>
 2      
 3      int read_line(char * buf)
 4      {       char * help = buf;
 5              printf("$ ");
 6              while ( ( ( *help = getchar() ) != EOF ) && 
 7                      ( *help != '\n'                   )    
 8                    )         help ++;
 9      
10              *help = (char )'\0';
11              return ( help == buf ? 0 : 1 );
12      }
13      
14      int main(void)  {
15      char cmd[1024];
16              while ( read_line(cmd) )        {
17                      switch ( fork () )      {
18                              case    0:      execl(cmd, cmd,
19                                                 (char *) 0);         
20                                              exit(0);
21                              case    -1:     perror("fork failed.");
22                                              exit(-1);
23                              default:        wait(NULL);
24                      }
25              }
26              exit(0);
27      }

artemis Src 105 simple_shell
$ /bin/ls
hello           hello.c         simple_shell    simple_shell.c
$ /bin/date
Mon Apr  7 18:17:32 MET DST 1997
$ who
$ /bin/who
bischof  console Apr  7 15:17
bischof  ttyp1   Apr  7 17:14
bischof  ttyp3   Apr  7 17:44

Normalerweise kann man voraustippen und auch eine Zeile noch korrigieren, bevor sie ein Prozeß sieht. Bestimmte Tasten haben dabei meistens eine besondere Bedeutung, die über das Kommando stty gesteuert werden kann. Um das zu verstehen, muß man sich das Modell des Terminal-Treibers klarmachen:

RETURN
schließt einen Block in der raw queue ab und erscheint dem Prozeß als Zeilentrenner. Der Block wird in die cooked queue übertragen und kann von dort vom Prozeß gelesen werden. Siehe auch tty(4). Ein Prozeß erhält pro read-Systemaufruf(2) maximal einen Block aus der cooked queue.
$ sleep 10; dd bs=1024 count=1
hp
date
hp
0+1 records in
0+1 records out
$ date
Thu Mar 20 18:36:28 MET 1997
Siehe auch sleep(1), dd(1) und date(1). Vorsicht: aus einer Datei oder einer Pipeline liest der read-Systemaufruf(2) so viel wie möglich, deshalb kann man in einem Shell-Skript praktisch kaum die Standard-Eingabe zwischen Kommandos aufteilen:
$ { echo hpb
>   echo date
> } | dd bs=1024 count=1
hpb
date
0+1 records in
0+1 records out

CONTROL
gibt 32 Zeichen (ab @) Sonderbedeutung. Muß (wie SHIFT) gleichzeitig gedrückt werden.
    0     8    16    24    32     40   48   56   64   72   80    88   96    104   112   120
0   nul   bs   dle   can   leer   (    0    8    @    H    P     X    `     h     p     x
1   soh   ht   dc1   em    !      )    1    9    AQ   Y    a     i    q     y
2   stx   nl   dc2   sub   "      *    2    B    :    J    R     Z    b     j     r     z
3   etx   vt   dc3   esc   #      +    3    ;    C    K    S     [    c     k     s     {
4   eot   np   dc4   fs    $      ,    4    <    D    L    T     \    d     l     t     |
5   enq   cr   nak   gs    %      -    5    =    E    M    U     ]    e     m     u     }
6   ack   so   syn   rs    &      .    6    >    F    N    V     ^    f     n     v     ~
7   bel   si   etb   us    '      /    7    ?    G    O    W     _    g     o     w     del
------------------------------------------------------------------------------------------------
    1     2    3     4     ...                   i    ii   iii   iv   ...

Siehe auch ascii(7).

ctl-d
Abschluß einer Eingabe: Vorher kann man die Eingabe ändern, damit geht die Eingabe an das/ein Programm. ctl-d erscheint nicht beim Programm. Nach Konvention gilt eine Eingabe der Länge Null als Eingabe-Ende (also schließt ctl-d am Zeilenende typischerweise die Eingabe ab).
ctl-s
Anhalten der Ausgabe. Weiter geht's mit ctl-q oder auch mit einer beliebigen Taste. ctl-s und ctl-q erscheinen nicht beim Programm.
\
nimmt (manchem) nächstem Zeichen bei Erzeugen der Eingabe die Sonderbedeutung.
erase-Taste
löscht das vorhergehende Zeichen in der gleichen Eingabe. Kann eingestellt werden:

$ stty -crterase -crtbs		# am NeXT
$ stty erase X
$ dudXXate
Thu Apr 19 05:46:07 MEZ 1990

kill-Taste
löscht alle vorhergehenden Zeichen in der gleichen Eingabe. Kann eingestellt werden:

$ stty -crtkill			# am NeXT
$ stty kill Y
$ dateY
date
Thu Apr 19 05:46:27 MEZ 1990

interrupt-Taste
schickt interrupt-Signal an alle Programme, die dieses Terminal ``kontrolliert''. Oft brechen Programme dadurch ab. Kann eingestellt werden:

$ stty intr A
$ dd
A0+0 records in
0+0 records out
$ dudeAdate
Mon Apr  7 18:23:50 MET DST 1997

quit-Taste
schickt quit-Signal an alle Programme, die dieses Terminal ``kontrolliert''. Häufig brechen Programme dadurch ab. Meistens erhält man einen Speicherauszug core. Kann eingestellt werden:

$ stty quit B
$ dudeBdate
Mon Apr  7 18:24:22 MET DST 1997

stop-Taste
(Berkeley) normalerweise ctl-z. Hält Programm an. Weiter mit fg oder bg.

2.1.  Terminal-Einstellungen -- stty

In diesem Bereich differieren die verschiedenen UNIX-Varianten sehr stark. Nach der SVID sollte folgendes gelten :

$ stty 
speed 9600 baud; -parity hupcl 
brkint -inpck -istrip icrnl onlcr cr0 nl0 tab3 bs0 vt0 ff0 
echo echoe echok 
$ stty -a
...

$ stty -g
d06:1805:4bd:3b:7f:3:8:15:4:0:0:0
$ stty sane

Ohne Argumente zeigt das Kommando stty die Einstellungen. Mit Argumenten ändert stty Einstellungen und kann durchaus ein Terminal unbrauchbar machen! Problematisch ist, daß stty sowohl die serielle Verbindung als auch die logische Verarbeitung im Treiber steuert.

-a           everything   alle aktuellen Einstellungen zeigen
zahl                      Geschwindigkeit
evenp oddp                Parität
cs7          -pass8       Anzahl Datenbits
sane         -            relativ normale Werte
erase x                   Zeichen löschen
intr x                    abbrechen
kill x                    Zeile löschen
quit x                    abbrechen mit Speicherauszug
echo                      Echo zum Schirm
echoe        crtbs        erase mit bs darstellen
echok        -crtkill     bei kill Zeilentrenner ausgeben
lcase                     groß als klein ansehen
nl                        nur ctl-j beendet Zeile
tabs                      Tabulatorzeichen ausgeben
icanon       -cbreak      zeilenweise Eingabe
-icanon      cbreak       zeichenweise Eingabe
min n        -            Anzahl Zeichen abzuwarten
time n       -            Zeit abzuwarten
raw                       jedes Zeichen geht sofort zum Programm, keine Signale

Bedingungen wie lcase kann man durch ein Minuszeichen davor umkehren. [Details: Literatur]

I. a. R. wird eine Terminal von einem anderen Terminal aus eingestellt.

Terminal ttyp0:

verdi bischof 111 PS1="`tty` "
/dev/ttyp0 uname -a
SunOS verdi 4.1.3_U1 5 sun4m
/dev/ttyp0 stty -a | sed 2q
speed 9600 baud; rows 10; columns 80; line = 2;
intr = ^C; quit = ^; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
$ echo "anderes Terminal ..."


/dev/ttyp0 stty -a | sed 2q
speed 9600 baud; rows 23; columns 80; line = 2;
intr = ^C; quit = ^; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;

Terminal ttyp3:

verdi bischof 111 PS1="`tty` " 
/dev/ttyp3 uname -a
SunOS verdi 4.1.3_U1 5 sun4m
/dev/ttyp3 stty rows 23 < /dev/ttyp0

2.2.  Systemstart -- getty

Wenn man erst einmal Kommandos absetzen kann, kann man mit stty das Terminal zähmen. Damit das Terminal aber überhaupt Kommandos annimmt, muß man sich schon angemeldet haben. Der Systemstart geht etwa so vor sich:

Das Betriebssystem wird in den Speicher geladen, gestartet, und ein allererster Prozeß führt das Programm /etc/init aus.

Unter Sun OS 4.1.3 kontrollieren /etc/rc.boot, /etc/rc, /etc/rc.local welche Prozesse gestartet werden.

Unter Sun OS 5.x kontrollieren die Skripte in /etc/rc.d/* welche Prozess gestartet werden. Siehe auch inittab(4).

/etc/init startet nach Maßgabe einer Tabelle /etc/inittab oder /etc/ttys Prozesse für die Terminal-Geräte (oder -Simulationen), an denen eine Anmeldung erfolgen soll.

Jeder derartige Prozeß führt zuerst das Programm /etc/getty aus, das mit Hilfe seiner Argumente und einer Tabelle /etc/gettytab das Terminal einstellt. Speziell für Modems können dabei auch die Baud-Raten zyklisch gewechselt werden, bis eine Eingabe erkannt wird.

getty fordert den login-Namen an und führt dann im gleichen Prozeß das Programm /bin/login aus, das ein Paßwort abfragt.

Das Paßwort wird in der Datei /etc/passwd (oder /etc/shadow oder in der Trusted Computing Base oder bei der Netzdatenbank) verifiziert; anschließend startet login im gleichen Prozeß die Shell, die für den Benutzer vorgesehen ist, setzt den Heimatkatalog, usw.

Den Anfangszustand der Übertragungsparameter kontrolliert also getty.

2.3.  Prozeßgruppe und Session

Prozeßgruppen

Jeder Prozeß gehört zu einer Prozeßgruppe, die einen Anführer haben kann -- für ihn sind Prozeß- und Prozeßgruppennummer gleich.

Jede Prozeßgruppe enthält wenigstens einen Prozeß, der aber nicht unbedingt der Anführer sein muß.

Sessions

Sessions gliedern Prozeßgruppen zur Verteilung von Signalen und zur Zugehörigkeit zu einem Kontroll-Terminal. Jeder Prozeß gehört zu einer Session. Jede Session enthält wenigstens eine Prozeßgruppe. Die Prozeßgruppen können in eine Vorder- und viele Hintergrundprozeßgruppen eingeteilt werden.

Eine Session kann maximal ein kontrollierendes Terminal haben. Der Session leader etabliert die Verbindung zum kontrollierenden Terminal, man nennt ihn dann auch controlling process.

Falls eine Session ein kontrollierendes Terminal hat, gehen am Terminal erzeugten Interrupts an die Prozesse der Vordergrundprozeßgruppe -- sprich Signale gehen vom Terminal-Treiber nur an die Vordergrund-Prozeßgruppe des Kontroll-Terminals.

Falls das Modem abgeschaltet wird, oder das Pseudoterminal stirbt, wird an den Session leader ein hangup-Signal geschickt. Der dieses Signal dann an die Mitglieder der Vordergrundprozeßgruppe weiter schickt.

Falls der Session leader stirbt, wird dieses Signal an alle Mitglieder der Vordergrundprozeßgruppe geschickt.

Man schützt Hintergrundprozesse durch Aufruf unter nohup.

Siehe auch Advanced Programming in the Unix Environment, W. Richard Stevens, Addison-Wesley, ISBN: 0-201-56317-7.

3.  Kommandos

Übersicht

Nach X/OPEN und der SVID sind folgende Kommandos einigermaßen Standard:

Art                Beispiele
-------------------------------------------------------
Identifizieren     login newgrp passwd
Information        cal date df du id logname man tty
Kommunikation      mail mesg news who write
Vernetzung         rlogin, tip etc.
Systemverwaltung   acct, crontab, dump, mount etc.
Dateien            chgrp chmod chown ln ls mv rm touch
Kataloge           find mkdir rmdir
Kompaktieren       pack pcat unpack
Kopieren           cp cpio dd tar tee
Betrachten         cat file od pg tail
Anordnen           pr sort uniq
Untersuchen        cmp comm diff egrep fgrep grep wc
Text aendern       csplit cut join paste split tr
Service            at calendar cancel lp lpstat stty
Prozesse           kill nice nohup ps time
Rechnen            basename dirname expr
Programmieren      env false sleep test true
Subsysteme         awk make sed sh vi

Die Klassifizierung ist nicht strikt. Manche Kommandos passen in mehrere Kategorien. Die Liste beinhaltet nicht:

Art                       Beispiele
---------------------------------------------
Shell-interne Kommandos   cd, echo, pwd etc.
Programmiersysteme        C, Fortran etc.
Prozeßkommunikation       ipcs, mkfifo etc.

Die folgenden Beschreibungen sind auf einigermaßen Nützliches reduziert. Die GNU-Implementierungen der Kommandos liefern in der Regel eine Gebrauchsanweisung, wenn das Argument --help angegeben wird.

Die Verweise in diesem Kapitel zeigen auf nützliche Manualseiten.

3.1.  Identifizieren

Login: (login)

$ login
login: bischof
Password:
...

Der Benutzername legt Benutzernummer und -gruppe fest. Der Name kann mit einem Paßwort gesichert sein. Als Datenbasis dient /etc/passwd und /etc/group bzw. NIS (ypcat).

Telnet: (telnet)

$ telnet cs.oswego.edu
Trying 129.3.20.253... Connected to cs.oswego.edu.
Escape character is '^]'.


UNIX(r) System V Release 4.0 (altair)

login: 
...

$ telnet 129.3.20.253
Trying 129.3.20.253... Connected to 129.3.20.253.
...
$

Mit telnet kann man sich auf dem lokalen oder auf einem entfernten Rechner anmelden. Als Datenbasis für den login-Vorgang wird die Datenbasis aus /etc/passwd bzw. NIS. des entsprechenden Rechners verwendet.

telnet kann auch dazu verwendet werden einen Port auf einem Rechner anzusprechen. Siehe auch services(5)

$ telnet cs.oswego.edu 13
Trying 129.3.20.253... Connected to cs.oswego.edu.
Escape character is '^]'.
Mon Apr 21 08:08:22 1997
Connection closed by foreign host.

$ telnet cs.oswego.edu 79 
Trying 129.3.20.253... Connected to cs.oswego.edu.
Escape character is '^]'.
tymann
Login       Name               TTY         Idle    When    Where
ltymann  Lisa Tymann           pts/8        <Mar 17 12:51> localhost           
tymann   Paul Tymann           term/a       <Apr 21 06:27>
Connection closed by foreign host.

$ telnet cs.oswego.edu 21
Trying 129.3.20.253... Connected to cs.oswego.edu.
Escape character is '^]'.
help
220 altair FTP server (Version wu-2.4(2) Fri Jan 17 16:30:39 EST 1997) ready.
214-The following commands are recognized (* =>'s unimplemented).
   USER    PORT    STOR    MSAM*   RNTO    NLST    MKD     CDUP 
   PASS    PASV    APPE    MRSQ*   ABOR    SITE    XMKD    XCUP 
   ACCT*   TYPE    MLFL*   MRCP*   DELE    SYST    RMD     STOU 
   SMNT*   STRU    MAIL*   ALLO    CWD     STAT    XRMD    SIZE 
   REIN*   MODE    MSND*   REST    XCWD    HELP    PWD     MDTM 
   QUIT    RETR    MSOM*   RNFR    LIST    NOOP    XPWD 
214 Direct comments to ftp-bugs@altair.

Remote Login: (rsh, rlogin)

$ rsh thor -l nil 
Password:
thor:[nil]# id
uid=0(root) gid=1(daemon) groups=1(daemon),0(wheel)
$ exit     # zureuck zur vorherigen shell

$ hostname
artemis
$ echo artemis > /home/bischof/.rhosts
$ rsh thor date
Wed Apr 16 09:44:09 MET DST 1997

Mit rsh bzw. rlogin kann man sich auf dem lokalen oder auf einem entfernten Rechner anmelden und gegebenenfalls ein Kommando ohne Angabe eines Paßworts absetzen. Die Dateien /$HOME/.rhosts bzw. /etc/hosts.equiv entscheiden welche Rechner als vertrauenswürdig anzusehen sind. Die Dateien werden nur zur Kontaktaufnahme konsultiert.

Großzügiges füllen einer der beiden Dateien ermöglicht ein leichtes einbrechen.

$ rm ~/.rhosts
$ echo artemis > ~/.rhosts && rsh artos "rm -f ~/.rhosts; who"
gremeyer console Apr 16 07:03
gremeyer ttyp1   Apr 16 07:03
gremeyer ttyp2   Apr 16 08:11
$ rsh portos
Password:

Man kann verhindern, daß sich der Benutzer mit der UID 0 via rsh, rlogin oder telnet anmelden kann. Siehe auch ttytab(5)

Daten übertragen: (ftp)

ftp wird dazu verwendet Daten zu übertragen. Ftp-Server akzeptieren als Benutzerkennung i.a.R. ftp oder anonymous.

Durch Nutzung von netrc(5) kann man initial-Werte vorgaben.

$ ftp ftp.rz.Uni-Osnabrueck.DE
Connected to ftp.rz.Uni-Osnabrueck.DE.
220 epimetheus FTP server (Version wu-2.4(8) Thu Jan 16 14:...
Name (ftp.rz.Uni-Osnabrueck.DE:bischof): anonymous
331 Guest login ok, send your complete e-mail address as password.
Password:

Die interessanten Daten finden sich i. a. R. im Katalog /pub.

ftp> pwd
257 "/pub/unix/gnu" is current directory.
ftp> bin
200 Type set to I.
ftp> get spell-1.0.tar.gz
200 PORT command successful.
150 Opening BINARY mode data connection for spell-1.0.tar.gz (44388 bytes).
226 Transfer complete.
local: spell-1.0.tar.gz remote: spell-1.0.tar.gz
44388 bytes received in 0.06 seconds (6.7e+02 Kbytes/s)

Man kann ftp auch im Batch-Betrieb verwenden.

 1      {       echo open ftp.rz
 2              echo user ftp bischof@
 3              echo ls
 4              echo bye
 5      } | ftp -ni    # 1 - Zeichen
 6      

Gruppe wechseln: (newgrp)

$ newgrp gruppe                # In neue Gruppe wechseln.
$ newgrp login-Gruppe

Wechselt in eine neue Benutzergruppe, falls erlaubt. Siehe auch /etc/group bzw. NIS (ypcat).

Dabei wird die Shell neu geladen. (In der Regel nur am Terminal möglich.) Funktioniert nicht bei Berkeley-Systemen, denn dort kann ein Benutzer in mehrere Gruppen eingetragen sein.

Passwort wechseln: (passwd)

$ passwd                        # Eigenes Paßwort ändern.
Changing password for bischof.  # (bei NIS-Verwaltung: yppasswd )
Old password:
New password:
Retype new password:

Trägt ein (neues) Paßwort ein. (In der Regel nur am Terminal möglich.)

Man kann sowohl Loginname wie auch die default-Shell modifizieren.

$ yppasswd -s
Changing NIS login shell for bischof on thor.
Old shell: /usr/local/gnu/bin/bash
New shell: ^C
$ yppasswd -f
Changing NIS finger information for bischof on thor.
Default values are printed inside of '[]'.
To accept the default, type <return>.
To have a blank entry, type the word 'none'.

Name [Hans-Peter Bischof]:  ^C

3.2.  Information

Information

``Ewiger'' Kalender: (cal)

$ cal                   # Kalender fuer aktuellen Monat ausgeben.
$ cal jahr              # ganzes Jahr
$ cal monat jahr        # bestimmter Monat

$ cal 9 1752
   September 1752
 S  M Tu  W Th  F  S
       1  2 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

Datumsausgabe in vielen Variationen: (date)

$ date 			# +format Ausgabe a la printf formatieren.
$ date 
Fri Apr  4 12:31:38 MET DST 1997
$ date '+%w/%y'
5/97
$ date '+%D'
04/18/97
$ date '+%A %B %Y'
Friday April 1997

freier Plattenplatz: (df)

$ df
Filesystem         1024-blocks  Used Available Capacity Mounted on
/dev/sd0a            1020893  411996   506807     45%   /
/dev/sd0b            1020894  756107   162697     82%   /export

$ df /dev/sd0a /dev/rsd0a
Filesystem         1024-blocks  Used Available Capacity Mounted on
/dev/sd0a            1020893  411996   506807     45%   /
/dev/rsd0a           1020893  411996   506807     45% 

$ df -i
Filesystem           Inodes   IUsed   IFree  %IUsed Mounted on
/private/vm/swapfile  255360   27143  228217    11%  /private/vm/swapfile.front
portos:/export/local      -1       0      -1     0%  /tmp_mnt/Local
/dev/sd0a             255360   27143  228217    11%  /

Platzverbrauch: (du)

$ du -s .
131071  .
$ du c.01
4       c.01
$ ls -l c.01
-rw-r--r--   1 bischof  inform       3828 Apr  3 18:23 c.01

Wer bin ich?: (id, who, logname)

$ id         # login-Information ausgeben.
$ id
uid=106(bischof) gid=11(inform) groups=11(inform),0(wheel)...
$ id -g
11
$ id -u
106


$ logname
bischof

$ who am i
artemis!bischof  ttyp1    Apr 17 15:56
$ who ss ss
artemis!bischof  ttyp1    Apr 17 15:56

Wie kommt man an die Information: (man)

$ man kommando	        # Manualseite ausgeben.
$ man nummer kommando	# aus bestimmtem Abschnitt
$ man 1 ed
$ man 2 read

Es ist darauf zu achen, daß die ``richtige'' Manual-Seite gefunden wird. Sprich $MANPATH und $PATH sollten übereinstimmend sein.

3.3.  Kommunikation

Elektronische Post: (mail)

$ mail 				# Eingegangene Post zeigen.
$ mail benutzer			# Brief schreiben.	
$ mail benutzer@rechner		# an Benutzer an fremdem Rechner
$ ... | mail ...		# Text(!) verschicken.

Zum Betrachten der Post gibt es viele Kommandos, insbesondere ? für Instruktionen. Beim Schreiben eines Briefes kann man mit ~? am Zeilenanfang Instruktionen sehen. Es gibt mehr oder weniger komfortable Abarten von mail.

Man kann eine mail auch fälschen. Dies ist dann wohl Betrug.

$ telnet artemis 25
Trying 131.173.161.32... Connected to artemis.
Escape character is '^]'.
220 artemis.informatik.uni-osnabrueck.de ESMTP
HELO artemis
250 artemis.informatik.uni-osnabrueck.de Hello
artemis [131.173.161.32], pleased to meet you
MAIL From: <h_k@dbt.de>
250 <h_k@dbt.de>... Sender ok
RCPT To: bischof
250 bischof... Recipient ok
Data
354 Enter mail, end with "." on a line by itself
Lieber HP, ich suche gerade einen
...
-hk
.
250 LAA09824 Message accepted for delivery
QUIT
221 artemis.informatik.uni-osnabrueck.de closing connection
Connection closed by foreign host.

$ mail
Mail version 8.1 6/6/93.  Type ? for help.
"/usr/spool/mail/bischof": 1 message 1 new
>N  1 h_k@dbt.de            Wed Apr 16 11:00  12/683  
& 
Message 1:
From h_k@dbt.de Wed Apr 16 11:00:53 1997
From: h_k@dbt.de
Date: Wed, 16 Apr 1997 11:00:03 +0200 (MET DST)

Hast Du nicht Lust mein neuer M ... zu werden?
-hk

Direktkommunikation am Terminal: (mesg, talk)

$ mesg	        # Fremdzugriffsrechte auf eigenes Terminal zeigen.
$ mesg y	# erlauben
$ mesg n	# verbieten
$ write user tty	# kommunizieren falls die Zugriffsrechte stimmen
$ talk bischof@artemis
$ news          # (Bei uns lokale) Neuigkeiten zeigen.

Es gibt eine ganze Subkultur news und entsprechend viele Programme zum Schreiben und Lesen von Artikeln. Leider ist der Informationsgehalt begrenzt.

Wer ist/war angemeldet: (who, last)

$ who
bischof  console  Apr  4 08:21
bischof  ttyp1    Apr  4 08:21
bischof  ttyp2    Apr  4 08:24
$ last 
bischof   ttyp5      Fri Apr  4 12:42   still logged in
bischof   ttyp4      Fri Apr  4 12:31   still logged in
bischof   ttyp6      Fri Apr  4 12:16   still logged in
bischof   ttyp5      Fri Apr  4 11:26 - 12:29  (01:03)
bischof   ttyp5      Fri Apr  4 09:37 - 09:37  (00:00)
bischof   console  artemis  ...
reboot    ~          Thu Apr  3 19:12 
...
$ who /usr/adm/wtmp
gremeyer ttyp3    Apr  1 07:36 (portos)
bischof  ttyp3    Apr  1 08:33
shutdown ~        Apr  3 10:09
shutdown ~        Apr  3 10:10
...
$ who am i
artemis!bischof  ttyp6    Apr  4 12:16
$ who i am
artemis!bischof  ttyp6    Apr  4 12:16
$ who bist Du
artemis!bischof  ttyp6    Apr  4 12:16

3.4.  Dateien

Bei der Anmeldung erhält man eine Benutzer- und Gruppennummer. Wenn ein neues Objekt im Dateisystem angelegt wird, erhält es diese als Besitzer- und Gruppennummer. Zugriff auf das Objekt wird durch die Rechte

--
Lesen
--
Schreiben
--
Ausführen

festgelegt. Wobei dies in die Kategorien Besitzer, Besitzer-Gruppe, Rest der Welt eingeteilt ist.

Die Ausführrechte bei Katalogen bedeutet, daß man einen Suchzugriff bekommt.

Zugriffsrechte ändern: (chmod, chown, chgrp)

$ chgrp gruppe pfad...       # Besitzer-Gruppe ändern.
$ chown besitzer pfad...     # Besitzer ändern.

$ chmod +x pfad...           # Ausführung erlauben.
$ chmod -w pfad...           # Schreiben verbieten.

Diese Änderungen darf nur der Besitzer (oder der Super-User) vornehmen. chmod akzeptiert die Rechte oktal (640 erlaubt Lesen für Besitzer und Gruppe, Schreiben für Besitzer) oder als Formel, wobei mehrere Operationen durch Komma getrennt werden:

u   für Besitzer,
g   für Gruppe,
o   für Rest

mit + - = kann man Rechte hinzufügen, wegnehmen, absolut einstellen:

Mit u+s oder 4000 definiert man, daß bei Ausführung eines binären Programms temporär die Besitzernummer der Datei als effektive Benutzernummer des Prozesses verwendet wird, (setuid(3)) die den Zugriff auf Dateien regelt. Analog führt g+s oder 2000 die Gruppennummer der Datei als effektive Gruppennummer ein. Damit kann zum Beispiel ein normaler Benutzer für genau kontrollierte Operationen die Rechte des Super-Users erhalten.

Wie könnte man sonst drucken?

Analog führt +t oder 1000 dassticky-Bit ein. Dies bedeutet, falls

Wie könnte man sonst vernünftig mit einer tmp arbeiten?

Die Verwendung des sticky-Bit kann zu einer effizienteren Nutzung der Ressourcen führen.

Src ## 9 ls -ld Sticky sticky
drwxr-xr-x   2 root     inform       1024 Apr 23 08:55 Sticky
-rwxr-xr-x   1 bischof  inform      24576 Apr 23 08:50 sticky

verdi Src ## 10 chmod +t Sticky
verdi Src ## 11 ls -ld Sticky
drwxr-xr-t   2 root     inform       1024 Apr 23 08:55 Sticky

verdi Src ## 12 chmod 1755 sticky
verdi Src ## 13 ls -l sticky
-rwxr-xr-t   1 bischof  inform      24576 Apr 23 08:50 sticky

Eine Datei, viele Namen: (ln)

$ ln alteDatei neuerName	# Einer Datei einen weiteren Namen geben.
$ ln alteDatei... katalog	# mehrere Dateien in einem Katalog

$ ls -l orig
-rw-r--r--   1 bischof  inform          0 Apr 16 11:04 orig
$ ln orig orig.ln 
$ ls -li  orig orig.ln
  14639 -rw-r--r--   2 bischof  inform          0 Apr 16 11:04 orig
  14639 -rw-r--r--   2 bischof  inform          0 Apr 16 11:04 orig.ln
$ rm  orig
$ ls -li  orig.ln
  14639 -rw-r--r--   1 bischof  inform          0 Apr 16 11:04 orig.ln

Diese mehrfache Benennung ist nur im gleichen Dateisystem möglich. Bei vielen Systemen kann man symbolische Links eintragen:

$ ln -s ersatzPfad ziel	# Verweis auf andere Datei eintragen.

Mehrfache Benennung von Katalogen ist nur mit symbolischen Links erlaubt. Dies kann zu Dateisystemen führen, die sehr verwirrend sind.

$ ln -s / root
$ ls -l root
lrwxrwxrwx   1 bischof  inform          1 Apr 16 11:12 root -> /
$ ls -L root | sed  1q
artemis_sd0a
$ cd root
$ pwd
/export/home/bischof/Vorlesung/Sp_97/Skript/root
$ /bin/pwd 
/

Mir der Option -f kann der super user einen harten Link zwischen Katalogen erzwingen. Davon ist abzuraten.

Fifo: (mknod)

Es gibt unter Unix folgende Dateitypen/Geräte

Blockgeräte       Magnetband, Platte
Charactergeräte   Platte, Terminal
Datei             Fifo, reguläre Dateien, Sockets, Links, symbolische Links
Katalog

$ mkfifo FIFO
$ ls -l FIFO
prw-r--r--   1 bischof  inform          0 Apr 18 14:28 FIFO
$ echo `date` > FIFO &
[1] 17064
$ ls -l FIFO
prw-r--r--   1 bischof  inform          0 Apr 18 14:28 FIFO
$ cat FIFO
Fri Apr 18 14:29:02 MET DST 1997
[1]+  Done                    echo `date` >FIFO
$ 

Dies kann nicht über Rechnerkanten hinweg funktionieren, weil die Synchronisatzion innerhalb des Kerns geregelt wird.

Datei-Listing: (ls)

$ ls -option pfad...	# Dateinamen und Informationen zeigen.

Es gibt sehr viele, kombinierbare Optionen, zum Beispiel:

-a    auch Dateinamen mit Punkt am Anfang
-d    Katalog zeigen, nicht seinen Inhalt
-i    mit inode-Zahl
-l    mit langer Information
-r    rückwärts sortiert
-R    rekursiv durch Unterkataloge
-s    mit Größe (in 512-Byte Blöcken)
-t    nach Schreibzeit sortiert
-ut   nach Zugriffszeit sortiert

Umbenennen: (mv)

Eine Datei wird im gleichen Dateisystem durch einen Aufrufe von mv umbenannt. Andernfalls muß sie kopiert und gelöscht werden; dabei gehen aber Links verloren.

$ mv alteDatei neuerName        # Einer Datei einen anderen Namen geben.
$ mv alteDatei... katalog       #  mehrere Dateien in einem Katalog

Löschen: (rm)

$ rm datei...           # Dateien löschen.
$ rm -f datei...        # ohne Rückfrage
$ rm -i datei...        # im Dialog
$ rm -r pfad...         # rekursiv

Wenn der letzte Link gelöscht wird, wird der Platz der Datei wieder verfügbar.

Zeitstempel aktualisieren: (touch)

$ touch pfad...            # Zeitstempel neu setzen.
$ touch mmddhhmmyy pfad... # explizit
$ touch -c pfad...         # falls nicht-existent, nicht erzeugen

touch liest und schreibt das erste Zeichen der Datei, sonst würde sich über NFS nichts ändern.

Es gibt Probleme, wenn das Dateisystem via NFS von NeXTStep auf SunOS 4.1.3 montiert wird.

SunOS mozart 4.1.3_U1 1 sun4m echo xx > to_old_to_rock
SunOS mozart 4.1.3_U1 1 sun4m ls -l to_old_to_rock
-rw-r--r--   1 bischof  inform          3 Apr 18 14:34 to_old_to_rock
SunOS mozart 4.1.3_U1 1 sun4m touch to_old_to_rock
SunOS mozart 4.1.3_U1 1 sun4m ls -l to_old_to_rock
-rw-r--r--   1 bischof  inform          3 Jan  1  1970 to_old_to_rock

3.5.  Kataloge

Dateisystem traversieren: (find)

$ find pfad... bedingung... -o bedingung...

find traversiert Katalogbäume und verknüpft alle Dateinamen mit den Bedingungen.

Bedingungen sind UND-verknüpft, können mit ! negiert, mit -o ODER-verknüpft und mit \( \) geklammert werden. Je nach Version von find gibt es sehr viele Bedingungen, zum Beispiel:

-print              aktuellen Pfad ausgeben; immer wahr
-atime -5           Zugriff seit weniger als 5 Tagen?
-exec ls -l {} \;   Kommando für aktuellen Pfad erfolgreich?
-name muster        paßt Dateiname?
-newer datei        später geändert als die Datei?
-size 0             leer?
-type d             ist es Katalog?
-user name          richtiger Besitzer?
$ cd /home/bischof/X
$ find a b -type d -print -exec ls -l {} \;
$ find a b -type d -print -exec ls -l {} \;
a
total 0
-rw-r--r--   1 bischof  inform          0 Apr  4 16:13 in_a
b
total 0
-rw-r--r--   1 bischof  inform          0 Apr  4 16:13 in_b

-name wendet ein Muster nur auf die aktuelle Pfadkomponente an. Wenn man ganze Pfade untersuchen will, muß man eine Kombination von du -a und grep verwenden.

$ touch an aber 
$ find . -name a*
find: paths must precede expression
Usage: find [path...] [expression]
$ find . -name 'a*' -print
./an
./aber

Kataloge erzeugen/löschen: (mkdir, rmdir)

$ mkdir katalog...      # Leere Kataloge erzeugen.
$ rmdir katalog...      # Leere Kataloge löschen.

Kataloge samt Inhalt kann man mit rm -rf sehr radikal entfernen.

3.6.  Kompaktieren

Kompaktieren: (compress)

$ compress datei...      # Dateien kompaktieren.
$ uncompress datei.Z     # Kompaktierte Datei wiederherstellen.

Es gibt verschiedene derartige Familien, zum Beispiel GNU Hier spielt die Furcht vor Software-Patenten eine große Rolle.

Bereits kompaktierte Dateien sollte man nicht nochmals kompaktieren. Manche Kommunikationsprogramme kompaktieren selbst, oder sie können nicht mit 8-Bit Information umgehen.

3.7.  Verschlüsseln:

Crypt: (crypt)

Es gibt die Möglichkeit Dateien verschlüsselt abzulegen.

$ echo 'Hoho' > ho
$ cat ho | crypt bg  > ho.crypt 
$ od -c ho.crypt
0000000 037 205   A 234   0
0000005
$ 
$ cat ho.crypt  | crypt bg
Hoho

3.8.  Kopieren

Kopieren von Dateien (cp)

$ cp alteDatei neueDatei        # Eine Datei kopieren.
$ cp alteDatei... katalog       # mehrere Dateien in einen Katalog

Kopieren von Bäumen (cpio, tar)

$ ls | cpio -oc > /tmp/cpio.out     # Dateien archivieren.
$ cpio -ict < /tmp/cpio.out         # I.-V. eines Archivs zeigen.
$ cpio -icdum < /tmp/cpio.out       # Archiv einspielen.
$ cd alt; \     # Dateibaum als Links kopieren.
> find . -depth -print | cpio -pdl neuerPfad

$ tar cf /tmp/x Soelim
$ tar cf - Soelim | ( cd /tmp && tar xf - )
$ ls -ld /tmp/S*
drwxr-xr-x   2 bischof  wheel        1024 Apr  4 15:19 /tmp/Soelim
$ tar cf - Soelim | ( cd /tmp;  tar xf - )
tar cf tar.out pfad...     # Dateien und Bäume archivieren.
$ tar tf tar.out           # Inhaltsverzeichnis eines Archivs zeigen.
$ tar xf tar.out muster...         # Archiv einspielen.
$ ( cd alt && tar cf -. ) | \     # Dateibaum kopieren.
> ( cd neu && tar xf - )

Archive mit absoluten Pfaden sind nicht sehr praktisch.

Datenstrom byteweise betrachten: (dd)

$ dd < ein > aus bs=30k       # Kopieren.

dd kopiert, wobei fixe Satzlängen und Anzahl von Sätzen kontrolliert werden können. Es gibt viele Optionen mit einer sehr veralteten Syntax, zum Beispiel:

bs=10k      in Stücken von 10 KB
skip=6      zuerst 6  10KB-Blöcke überspringen
seek=8      zuerst 8 10KB-Blöcke in Ausgabe überspringen
count=20    bis zu 20 10KB-Blöcke kopieren
conv=sync   Ausgabe ist Vielfaches von 10KB
dd ist das einzige Programm, das aus binärer Information nach Position Bytes extrahieren kann.

hello.c:

 1      void main(void){ printf("hello\n"); exit(0); }

$ make hello
cc   hello.c  -o hello
$ hello
hello
$ strings -o hello
   8060 The kernel support for the dynamic linker is not present to run this program.
   8140 __dyld_make_delayed_module_initializer_calls
   8185 hello
$ dd bs=1 if=hello of=x count=5 skip=8185
5+0 records in
5+0 records out
$ cat x
hello
$ dd bs=1 if=hello of=1 count=8185
8185+0 records in
8185+0 records out
$ echo HELO > 2
$ dd bs=1 if=hello of=3 skip=8190
23390+0 records in
23390+0 records out
$ cat 1 2 3 > x
$ chmod +x x
$ x
HELO

3.9.  Betrachten

Spionieren innerhalb einer Pipe: (tee)

$ ... | tee datei... |...    # Eingabe in alle Dateien und zur Ausgabe kopieren.
$ ... | tee -a datei... |... # anhängen

Cat: (cat)

$ cat datei...			# Dateien zu Terminal kopieren.
$ cat > datei			# Terminal in Datei
$ cat >> datei			# an Datei anhängen
$ cat datei... > ausgabe	# Dateien in Datei
$ cat << 'ende'			# Text zu Terminal
hier ist text
ende
$ cat eins - zwei | mail	Datei/Terminal in Pipe

cat bearbeitet (wie jedes sogenannte Filter-Programm) seine Standard-Eingabe oder Dateien, deren Namen als Argumente angegeben sind, und schreibt nur zur Standard-Ausgabe. Die eigentliche Leistung entsteht durch E/A-Umlenkung.

Art einer Datei: (file)

file errät(!) die Art des Dateiinhalts, siehe auch magic(5). text, executable, directory sind hierbei interessante Worte.

$ file pfad...		# Datei klassifizieren.
$ file .
.:      directory
$ file /dev/tty
/dev/tty:       character special (2/0)
$ file /dev/sd0a
/dev/sd0a:      block special (6/0)
$ file /dev/rsd0a
/dev/rsd0a:     character special (14/0)
$ file mk_gen
mk_gen: executable shell script
$ file Src/hello
Src/hello:      Mach-O executable (for architecture i386)

Betrachten von binären Daten: (od)

$ ... | od          # Eingabe 16-Bit oktal darstellen.
$ od datei         # eine(!) Datei
$ od datei +100.   # ab dezimaler Position
$ od -b datei      # 8-Bit oktal
$ od -c datei      # Zeichen sichtbar
$ od -d datei      # 16-Bit dezimal
$ od -x datei      # 16-Bit hexadezimal

Betrachten einer Platte:

rc ## 15 od -c /dev/root | more
0000000   S   U   N   0   4   2   4       c   y   l  ...
0000020       a   l   t       2       h   d       9  ...
0000040   8   0  \0  \0  \0  \0  \0  \0  \0  \0  \0  ...
0000060  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  ...
*
0000640  \0  \0  \0  \0 021   0  \t 304  \0  \0  \0  ...
\&...
0016240   B   o   o   t   b   l   o   c   k  \0   c  ...
0016260   u   m       %   x       !   =       %   x  ...
...

Enden und Wenden: (tail)

$ ... | tail       # Ende einer Texteingabe zeigen.
$ tail -24 datei   # letzte 24 Zeilen
$ tail +1 datei    # ab erster Zeile
$ tail -f logbuch  # Ende verfolgen

3.10.  Anordnen

Seitenweises paginieren: (pr)

$ pr option... datei... ...    # Text seitenweise anordnen.

pr kann auch mehrspaltig anordnen und Tabulatorzeichen entfernen oder erzeugen. Es gibt viele Optionen, zum Beispiel:

-zahl      mehrspaltig, vertikal gefüllt
-a         ...aber horizontal gefüllt
-e         Tabulatorzeichen expandieren
-f         formfeed statt vielen Leerzeilen
-h titel   Seitentitel
-i         Tabulatorzeichen einfügen (Vorsicht)
-l72       Seitenlänge
-m         Dateien mehrspaltig
-n         Zeilen numeriert
-t         kein Seitentitel und Abstand
-w120      Zeilenlänge

$ cat 3.pr_ex
1
2 3 4 9
6 7 9
1 
$ pr -l 1 -n -2 3.pr_ex 
    1   1                               2   2 3 4 9
    3   6 7 9                           4   1

$ pr -l 1 -h 3.pr_ex -n -2 3.pr_ex  | sed 2q
    1   1                               2   2 3 4 9
    3   6 7 9                           4   1
$ pr -h 3.pr_ex -n -2 3.pr_ex  | sed 3q


$ pr       -h 3.pr_ex -n -2 3.pr_ex  | sed 3q


Apr 23 09:09 1997  3.pr_ex Page 1



Sortieren: (sort)

$ sort option... datei...      # Textzeilen sortieren.

sort kann nach mehrfachen Kriterien in beliebiger Folge sortieren. Es gibt viele Optionen, zum Beispiel:

+zahl        beginnend nach Zwischenraum (ab 0)
-zahl        endend vor Zwischenraum (ab 1)
-d           nur Buchstaben, Ziffern, Zwischenraum
-f           große und kleine Buchstaben gleich
-n           numerisch (Gleitpunkt)
-o ausgabe   Ausgabedatei (kann Eingabe sein)
-r           rückwärts
-t:          Zeichen : bedeutet den Zwischenraum
-u           nur eine Kopie pro Zeile ausgeben

$ cat 3.sort
xaz:a:h
aaz:d:h
aac:g:j
xyz:a:g
abc:b:i
1:2:3
$ sort -t: +2 3.sort
1:2:3
xyz:a:g
aaz:d:h
xaz:a:h
abc:b:i
aac:g:j
$ sort -t: +2 +1 3.sort
1:2:3
xyz:a:g
xaz:a:h
aaz:d:h
abc:b:i
aac:g:j

$ sort +0.1 -0.2 3.sort
1:2:3
aac:g:j
aaz:d:h
xaz:a:h
abc:b:i
xyz:a:g

Uniq: (uniq)

$ ... | uniq               # Duplikate bei Ausgabe unterdrücken.
$ uniq datei              # Datei
$ uniq -c eingabe ausgabe # Anzahl notieren

Läßt man Anzahl (vor Zeile) ausgeben, kann man nach Häufigkeit sortieren.

$ cat 3.uniq
111
111
222
x11
x11
x22
x33

$ uniq -c 3.uniq
      2 111
      1 222
      2 x11
      1 x22
      1 x33

$ uniq 3.uniq
111
222
x11
x22
x33

$ uniq -d 3.uniq
111
x11

3.11.  Untersuchen

Vergleichen und Unterschiede: (cmp, comm)

$ cmp eins zwei         # Beliebige Information vergleichen.
$ ... | cmp - zwei

$ comm -123 eins zwei   # Sortierten Text in Spalten anordnen.
$ ... | comm -123 - zwei

comm ordnet sortierte Textzeilen in drei Spalten an: Zeilen nur in erster Datei, nur in zweiter und Zeilen in beiden. Die Option verhindert die Ausgabe der angegebenen Spalten.

Unterschiede in Text-Dateien: (diff)

$ diff eins zwei      # Unterschied von Textdateien beschreiben.
$ ... | diff - zwei

diff ist ein sehr effizienter Baustein, zum Beispiel für Versionskontrolle und zur Verteilung von Korrekturen für patch.

Man kann diff und ed dazu verwenden zwei Dateien in Übereinstimung zu bringen.

$ cat 1
111111111
$ cat 2
22222222
22222222
$ diff -e 1 2
1c
22222222
22222222
$ { diff -e 1 2 ; echo w; echo q } | ed  - 1
$ cat 1
22222222
22222222

$ cp 2 1    # geht schneller, ist aber nicht selektiv

I. a. R. ist es günstiger Patches, und nicht ganze Dateien zu verschicken.

NeXT:/usr/bin/opendiff startet eine grafische Oberfläche für ein Programm wie diff

Pattern finden in Dateien: ([ef]?grep)

$ grep 'muster' datei...       # Textzeilen nach Muster auswählen.
$ egrep 'emuster' datei...     # aufwendigere Muster,

Alternativen die schneller sind:

$ ... | fgrep 'text'            # exakter Text,

Bei egrep und fgrep können Alternativen zeilenweise oder in einer Datei stehen, die nach -f angegeben wird. -l gibt nur Liste von Dateinamen mit Treffern aus. -v gibt nur Zeilen aus, die nicht passen.

Worte, Zeile und Zeichen zählen: (wc)

$ wc -clw datei...        # Zeichen,

Die Optionen kontrollieren die Ausgabe; normalerweise alle drei Zahlen.

3.12.  Text ändern

Zersplitten: (csplit)

$ csplit datei '/muster/+3'...    # dritte Zeile nach Muster
$ csplit -k datei 100 {20}        # bis zu 21 mal 100 Zeilen

csplit zerlegt eine Textdatei; die Stücke heißen xx00, xx01, usw. muster oder eine Zeilennummer legt den Anfang der zweiten,... Datei fest. {20} wiederholt das Muster oder die Zeilennummer als Anzahl Zeilen.

Senkrecht zerschneiden: (cut)

$ cut -c1-5,8-10,13- datei      # nach Zeichenpositionen
$ ... | cut -f3- -d:             # : trennt Felder

cut gibt Ausschnitte von Textzeilen aus. Felder sind normalerweise durch Tabulatorzeichen getrennt.

Zusammenfügen: (join)

join hängt sortierte Textzeilen bei gleichem ersten Feld aneinander. Mit Optionen kann man beliebige Felder prüfen und in beliebiger Reihenfolge ausgeben.

$  cat 3.join
a%a:first  line
b%a:second line
c%a:third  line
$ 
$  cat 3.join.b
a%b:first  line
b%b:second line
d%b:third  line

$ join -t% 3.join 3.join.b
a%a:first  line%b:first  line
b%a:second line%b:second line

$ join -t% -o1.2 2.2 3.join 3.join.b
a:first  line%b:first  line
a:second line%b:second line

$ join -t% -o1.2 2.1 3.join 3.join.b
a:first  line%a
a:second line%b

Zusammenfügen: (paste)

$ paste datei...               # Dateien mit Tabulatorzeichen
$ ... | paste -d: datei -...    # Datei und Eingabe mit :
$ ... | paste - -               # Eingabe zweispaltig
$ paste -s -d'\t\n' datei    # sukzessive Zeilen, je
                               # zwei mit Tabulatorzeichen

paste hängt positionsgleiche Zeilen aus verschiedenen Dateien aneinander. Die Zeichenliste bei -d dient dabei zirkulär als Trenner, aber am Schluß ist immer ein Zeilentrenner.

$ paste -d'^' 3.p*
a%a:first  line^a%b:first  line^1
b%a:second line^b%b:second line^2 3 4 9
c%a:third  line^c%b:third  line^6 7 9
^^1 

$ paste -d'^' 3.paste 3.paste.b
a%a:first  line^a%b:first  line
b%a:second line^b%b:second line
c%a:third  line^c%b:third  line

$ paste -d'-----' 3.paste 3.paste.b
a%a:first  line-a%b:first  line
b%a:second line-b%b:second line
c%a:third  line-c%b:third  line

Zerlegen: (split)

$ split -100 datei         # je 100 Zeilen

split zerlegt eine Textdatei in Stücke xab, xab, usw. mit je gleich vielen Zeilen.

Zeichen ersetzen: (tr)

$ tr '[A-Z]' '[a-z]' <ein >aus  # groß in klein
$ tr -d '\15\32' <ein >aus    # löschen: return und control-Z
$ tr -cs '[A-Z][a-z]' '[\12*]' # ein Wort pro Zeile
$ tr -cs '[A-Z][a-z]' '\12'    # ein Wort pro Zeile [Berkeley]

tr ersetzt einzelne Zeichen: Zeichen in erstem Argument durch Zeichen in gleicher Position in zweitem Argument.

-c   komplementiert erstes Argument,
-d   löscht statt zu ersetzen,
-s   gibt vielfache Zeichen im zweiten Argument nur einmal aus.

3.13.  Service

Aktivitäten zu einem bestimmten Zeitpunkt starten: (at)

$ at 1:00       # Auftrag für Uhrzeit (und Datum) hinterlegen.
sort -o bigfile bigfile
^D

$ at -l         # Aufträge zeigen.
$ at -r auftrag...  # löschen
	
$ tty
/dev/ttyp0
$ at 11:00
at> date > /dev/ttyp0
at> <EOT>
job 2914 at Sun Apr  6 11:00:00 1997
$ at -l
      2914 a    Sun Apr  6 11:00:00 1997
$ Sun Apr  6 11:00:01 MET DST 1997

Erinnerungsservice: (calendar)

$ calendar
Calendar: Could not find calendar file in current directory
$ # create calendar
$ cat calendar
April   17      Am Donnerstag donnerts
April   18      Am Freitag ist frei und
April   19      am Samstag kommt das Sams
April   20      Am Sonntag scheint die Sonne ...
April   21      Am Montag scheint der Mond

$ calendar
April   18      Am Freitag ist frei und
April   19      am Samstag kommt das Sams
April   20      Am Sonntag scheint die Sonne ...
April   21      Am Montag scheint der Mond

calendar sucht Zeilen mit dem heutigen oder morgigen Datum in einer Datei $HOME/.calendar und gibt sie aus. Kennt Wochenende und manche Feiertage. Wird oft bei Nacht über alle Heimatkataloge als Systemservice angeboten.

3.14.  Prozesse

Prozesse beenden: (kill)

$ kill prozessnummer...      # SIGTERM Signal
$ kill -9 prozessnummer...   # SIGKILL Signal (sehr brutal)
$ kill 0                     # SIGTERM an Prozessgruppe

kill schickt ein Signal an Prozesse. Normalerweise verenden sie dann.

Prioritäten ändern: (nice)

$ nice kommando &  # Kommando mit weniger Priorität ausführen.
$ nohup kommando & # Kommando gegen Abbruch bei Abmelden schützen.

Beim Abmelden geht das Signal SIGHUP an alle eigenen Prozesse. Wir ein Prozeß mit nohup gestartet, bleibt der Prozeß aktiv und die Ausgabe findet sich in nohup.out.

Prozesszustände und Rechnerzustand: (ps, vmstat)

$ ps
 ps
  PID TT STAT  TIME COMMAND
 1200 p1 SW    0:00 -bash (bash)
14698 p1 S     0:00 rlogin verdi
14699 p1 S     0:00 rlogin verdi

ps zeigt Information über Prozesse. Damit erfährt man die Prozeßnummer für kill. Viele Optionen sind möglich.

vmstat, ps zeigt Information über das System. Damit erfährt man etwas über Auslastung des Systems, wie z.B. Swapping, ...

Ressourcebenutzung: (time)

$ time kommando       # Laufzeiten eines Kommandos zeigen.
$ time test_links
       19.2 real         7.5 user         4.1 sys  

Vor allem die Verweilzeit kann variieren.

3.15.  Rechnen

Pfadbetrachtung: (basename, dirname)

$ basename pfad            # Letzte Pfadkomponente ausgeben.
$ basename pfad endung     # ohne Endung
$ dirname pfad             # Pfad ohne letzte Komponente ausgeben.

$ basename /home/bischof/Vorlesung/Sp
Sp
$ dirname /home/bischof/Vorlesung/Sp
/home/bischof/Vorlesung

$ basename /home/bischof/Vorlesung/Sp/c01.ms .ms
c01

Rechnen: (expr)

$ expr \( 3 + 4 \) \* 2      # Arithmetik
14
$ expr Abc \< abc              # Vergleiche
1
$ expr aababccaa : aab          # Mustervergleich
3
$ expr /dir/name : '.*/\(.*\)' # Extrakt: basename
name
$ expr /dir/na/me : '\(.*\)/[^/]*$'
/dir/na

Diese Kommandos werden in der Regel im Zusammenhang mit Shell-Variablen oder in Kontrollstrukturen verwendet.

3.16.  Programmieren

Environment: (env)

$ env                 # Exportierte Variablen zeigen.
$ env - kommando      # ohne Export
$ env name=wert... kommando      # zusätzliche Definitionen

Exitcodes: ($?)

$ false	Bedingung,
immer falsch.
$ echo $?
255
$ true	immer wahr
$ echo $?
0
$ cat xx 
cat: xx: No such file or directory
$ echo $?
1
$ grep XXXXX c.01
$ echo $?
1
$ grep x c.01 > /dev/null
$ echo $?
0
$ grep "[ss"  c.01 > /dev/null
grep: RE error
$ echo $?
2

Bedingungen: (test)

$ cd /usr/bin
$ ls -li test [
  5609 -rwxr-xr-x  2 root         5200 Jan 21  1994 [
  5609 -rwxr-xr-x  2 root         5200 Jan 21  1994 test

$ test bedingung       # Bedingung prüfen.
$ [ bedingung ]
$ if [ -f c.01 ] 
> then
>    echo c.01 existiert
> else
>    echo haeh
> fi
c.01 existiert

test ist meist eingebaut in der Shell. Bedingungen können mit -a UND-verknüpft, mit -o ODER-verknüpft, mit \( \) geklammert und mit ! negiert werden. Es gibt viele Terme, zum Beispiel:

-d datei        existenter Katalog
-f datei        existente, normale Datei
-s datei        existente, nicht leere Datei
-r datei        lesbar
-w datei        schreibbar
-x datei        ausführbar
-t              Standard-Ausgabe ist Terminal
"$var"          Ersatztext nicht leer
-z "$var"       Ersatztext leer
"$var" = text   Strings gleich
"$var" -le 10   numerisch kleiner-gleich

4.  Dateisystem

4.1.  Datei

Eine Datei ist eine Informationsmenge, bei UNIX ohne innere Struktur, logisch also ein Vektor von Bytes (Zeichen). Sie erlaubt die Operationen Lesen, Schreiben, Positionieren in Einheiten von Bytes.

Vorteil:
einfache Programme zum Vergleichen, Kopieren, usw. (cp cmp, comm)
Nachteil:
Strukturlogik, zum Beispiel für Zeilen, dupliziert in den Anwendungen.

4.2.  Peripherie-Geräte

Typische Geräte erlauben beinahe das Vektor-Modell mit Lesen, Schreiben, Positionieren einzelner Bytes.

Ein Terminal kann nicht in der Informationsmenge positionieren und transferiert jeweils eine Zeile bzw. ein Zeichen.

Eine Platte kann durch Simulation mit Pufferung im Betriebssystem auf einzelne Bytes positionieren. Sie transferiert beliebige Vielfache ihrer Sektorlänge und durch Simulation mit Pufferung im Betriebssystem auch eine beliebige Anzahl von Bytes. Siehe auch sd.

Ein Magnetband kann nur bedingt, nämlich auf Dateimarken und Sätze, positionieren. Es kann oft nur Sätze mit einer geraden Anzahl von Bytes transferieren. Siehe auch st.

4.3.  Platten

Platten haben konzentrische Spuren, die aus Sektoren bestehen. Nur Sektoren sind les- und schreibbar. Die Anordnung der Sektoren erfolgt beim Formatieren. Sie muß nicht sequentiell sein (interleaving). Sie muß auch auf benachbarten Spuren nicht gleich sein.

Eine Plattenadresse besteht aus Spur, Kopf, und Sektor. Bei gleicher Spur spricht man von Zylindern 1 Umschaltung zwischen Köpfen ist schnell.

Positional latency ist die Zeit, bis der Kopf auf der richtigen Spur steht. Sie besteht aus einer Anfahr- und Stop-Zeit sowie aus der Fahrzeit, die proportional zur Anzahl Spuren ist.

Rotational latency ist die Zeit, bis der gewünschte Sektor am Kopf erscheint. Sie kann durch interleaving bei sequentiellem Zugriff verkürzt werden.

Mittlere Zugriffszeit ist die Summe aus Positionierung über die halbe Platte und Sektorzahl. Typisch sind 100 Millisekunden für Floppies und 50 Millisekunden für (langsame) PC -Platten. Gut sind etwa 6 - 10 Millisekunden im UNIX-PC -Bereich. Sie kann durch geschickte Anordnung der Information auf der Platte unterschritten werden.

Transferrate mißt die Übertragung von Bytes zwischen Rechner und Platte, nachdem richtig positioniert wurde. Sie kann durch Zylinderzufgriff drastisch verbessert werden.

Je nach Einsatz der Platte sind andere Kenndaten besonders wichtig 1 für Dateien braucht man kurze Zugriffszeiten, für die Verdrängung von Prozessen swapping oder eine Datenbank kann eine hohe Transferrate wichtig sein.

4.4.  Plattenverwaltung

Eine Platte wird nach Spur, Kopf und Sektor adressiert. Eine Datei ist ein reservierter Bereich der Platte.

Magnetband-Stil

Ist eine Datei ein zusammenhängender Abschnitt der Platte, definiert ein Dateiname eine Anfangsadresse auf der Platte und die Dateilänge (in Bytes und dadurch in Sektoren o.ä.). (Beispiel: DEC RT -11.)

Die Plattenverwaltung ist dadurch recht einfach, der Nutzen einer Datei ist aber gering. Programme können sehr schnell geladen werden, Daten können direkt adressiert werden.

Freistil

Besteht eine Datei aus frei angeordneten Blöcken, definiert ein Dateiname eine Gruppe von Adressen auf der Platte und die Gesamtlänge der Datei in Bytes. (Beispiele: CP/M mit Adressen im Katalogeintrag, DOS mit Adreßketten, UNIX mit einer Baumstruktur für Adressen).

Die Plattenverwaltung ist komplizierter, aber eine Datei kann (je nach Adreßmanagement) beliebig wachsen. Daten können nur über Adreßtabellen adressiert werden.

Blockgröße

möglichst klein, um Verschnitt am Dateiende zu vermeiden; möglichst groß, um Verlust durch Adressen zu vermeiden und die Transferrate der Platte gut zu nutzen.

Sektoren sind typischerweise 256 oder 512 Bytes, Blöcke sind typischerweise 512 oder 1024 Bytes. Berkeley's fast file system verwendet Blöcke mit 4K oder 8K und legt die abschließenden Stücke mehrerer Dateien in einen gemeinsam genutzten Block (fragment).

4.5.  Adreßverwaltung

Ein Dateiname muß die Adressen aller Blöcke der Datei zugänglich machen.

CP/M
verwendet einen Adreßvektor im Katalogeintrag und eventuell viele Katalogeinträge (extensions) pro Datei.

DOS
verwendet eine Anfangsadresse im Katalogeintrag; über den file allocation table (FAT) entsteht eine lineare Liste von Adressen für Blöcke.

UNIX
hat eine Dateibeschreibung (fs) (inode) mit einem kleinen Vektor von direkten Adressen. Bei großen Dateien zeigt eine der Adressen auf einen Block mit weiteren Adressen (einfach indirekt), eine zweite auf einen Block mit Adreßblock-Adressen (doppelt indirekt), und eine dritte ist dreifach indirekt.

4.6.  Lineares Dateisystem

Freie Liste und lineares Dateisystem beschreiben die gesamte Fläche der Platte. Dateinamen im linearen Dateisystem entsprechen eindeutigen Flächen der Platte. Alle Dateinamen bilden einen einzigen Adreßraum.

CP/M
Das Dateisystem ist linear.
DOS
Die Anfangsadressen aller Dateien (also die Anfänge aller Ketten im FAT bilden das lineare Dateisystem.
UNIX
Die inodes bilden das lineare Dateisystem. Am Anfang jeder Platte befindet sich ein Vektor fixer Länge, dessen Elemente die Dateiflächen beschreiben:

(stat)

	/* Inode structure as it appears on a disk block.  */
struct dinode
{
	ushort	di_mode;		/* mode and type of file */
	short	di_nlink;    	/* number of links to file */
	ushort	di_uid;      	/* owner's user id */
	ushort	di_gid;      	/* owner's group id */
	off_t	di_size;     	/* number of bytes in file */
	char  	di_addr[40];	/* disk block addresses */
	time_t	di_atime;   	/* time last accessed */
	time_t	di_mtime;   	/* time last modified */
	time_t	di_ctime;   	/* time inode changed */
	};
	/*
	    * the 40 address bytes:
	    *	39 used; 13 addresses
	    *	of 3 bytes each.
	    */

4.7.  Kataloge

Das lineare Dateisystem kann Kataloge enthalten, das heißt Dateien, die dann ihrerseits Dateinamen enthalten, die auf das lineare Dateisystem verweisen.

DOS

Katalogeinträge enthalten Information über die Datei. Sie zeigen eindeutig auf Anfänge im FAT.
UNIX

Katalogeinträge enthalten keine Information über die Datei. Sie enthalten nur Namen und inode-Nummern der gleichen Platte. Mehrfache Verweise auf die gleiche inode sind möglich. Jede inode kennt die Anzahl der Verweise auf sich.

Bei Katalogen ist dieAnzahl der Verweise immer wenigstens 2 plus die Anzahl der Unterkataloge, denn ein Katalog zeigt mit dem Eintrag . auf sich selbst und mit .. auf seinen Vorgänger im Baum.

4.8.  Freiraumverwaltung

Hat man alle Dateien, kennt man den Freiraum auf der Platte als Komplement der Dateien.

Bitvektor

Je ein Bit beschreibt positionell einen Plattenblock. Dies ist auch dynamisch berechenbar.

Lineare Liste

Freie Blöcke sind verkettet. Relativ leicht zu verwalten.

Baumstruktur

Freie Blöcke sind Blätter eines Baums, dessen erste Ebene linear verkettete Adreßblöcke bilden. Die Vergabe sollte eine Lokalisierung von Dateien auf Zylindern (oder -gruppen) erlauben... Die Vergabe sollte eine günstige Anordnung der Blöcke auf der gleichen Spur erlauben...

4.9.  Vnodes

UNIX verwendet heute eine zweite Schicht zwischen den Anforderungen von Benutzerprozessen und der Realisierung von Dateisystemen auf Platten. In dieser Schicht wird festgehalten, welche Server-Funktionen für die Manipulationen eines Dateisystems zuständig sind.

Inodes sind eine mögliche Implementierung eines Dateisystems. NFS-Service zu einem UNIX- oder gar DOS-System ist eine andere.

4.10.  Prozesse als Dateisystem

Das proc-Dateisystem beschreibt Informationen über Prozesse in Form eines Dateisystems, das sich dynamisch ändert. Damit lassen sich eine Reihe von Dienstprogrammen sehr elegant portabel implementieren. Auf LINUX kann man folgendes beobachten:

$ cd /proc; ls
1/       29/      37/      44/      510/     meminfo  stat
1013/    31/      41/      45/      kcore    modules  uptime
139/     33/      42/      46/      kmsg     net/     version
16/      35/      43/      509/     loadavg  self/
$ ls -l self
total 3
-r--r--r--   1 bischof     root            0 Apr 18 07:58 cmdline
lrwx------   1 bischof     root           64 Apr 18 07:58 cwd -> [0001]:1
-r--r--r--   1 bischof     root            0 Apr 18 07:58 environ
lrwx------   1 bischof     root           64 Apr 18 07:58 exe -> [0305]:18450
dr-x------   2 bischof     root            0 Apr 18 07:58 fd/
-r--r--r--   1 bischof     root            0 Apr 18 07:58 maps
-rw-------   1 bischof     root            0 Apr 18 07:58 mem
dr-x------   2 bischof     root            0 Apr 18 07:58 mmap/
lrwx------   1 bischof     root           64 Apr 18 07:58 root -> [0305]:2
-r--r--r--   1 bischof     root            0 Apr 18 07:58 stat
-r--r--r--   1 bischof     root            0 Apr 18 07:58 statm
$ ls -l self/fd
total 5
lrwx------   1 bischof     root           64 Apr 18 07:58 0 -> [0305]:24725
lrwx------   1 bischof     root           64 Apr 18 07:58 1 -> [0305]:24725
lrwx------   1 bischof     root           64 Apr 18 07:58 2 -> [0305]:24725
lrwx------   1 bischof     root           64 Apr 18 07:58 3 -> [0305]:2090
lrwx------   1 bischof     root           64 Apr 18 07:58 4 -> [0001]:66846727
$ od -c self/cmdline
0000000   o   d  \0   -   c      s   e   l   f   /   c   m   d   l   i
0000020   n   e
0000022

4.11.  Peripherie-Geräte

Geräte erfüllen fast die Betriebsbedingungen für Dateien. Deshalb kann man spezielle inodes anlegen, die bei Zugriff nicht mit Dateien im Dateisystem sondern mit Geräten arbeiten: Siehe auch sd4

$ ls -l /dev
   2975 -rwxr-xr-x   1 root     staff       12739 Jan 21  1994 MAKEDEV
   2973 crw-rw-rw-   1 root     staff     69,   0 Jun  9  1995 audio
   2976 crw-rw-rw-   1 root     staff     69,   1 Jun  9  1995 audioctl
   3428 crw-rw-rw-   1 root     staff     26,   0 May 10  1993 bwone0
   3433 crw-rw-rw-   1 root     staff     39,   0 May 10  1993 cgfour0
   2982 crw--w--w-   1 root     wheel      0,   0 Apr  7 09:21 console
   2993 crw-rw-rw-   1 root     staff     22,   0 May 10  1993 fb
   3023 crw-rw-rw-   2 root     staff     54,   2 Apr  1 15:48 rfd0
   2984 crw-r-----   1 root     kmem       3,   0 May 10  1993 mem
   2991 crw-rw-rw-   1 root     staff     13,   0 May 10  1993 mouse
   3365 crw-rw-rw-   1 root     staff     18,  12 Feb  8  1995 nrst8
   2986 crw-rw-rw-   1 root     staff      3,   2 Apr  7 10:56 null
   3511 crw-rw-rw-   1 root     staff     21,  27 May 10  1993 ptyqb
   3070 crw-r-----   2 root     operator  17,  22 Aug 24  1995 rbackup_4
   3070 crw-r-----   2 root     operator  17,  22 Aug 24  1995 rsd2g
   3442 crw-rw-rw-   1 root     staff     30,   8 May 10  1993 rmt8
   3049 crw-r-----   1 root     operator  17,   9 May 10  1993 rsd1b
   2983 crw-rw-rw-   1 root     staff      2,   0 Apr  6 11:49 tty
   2989 crw-rw-rw-   1 root     staff     12,   0 Jul 15  1993 ttya
...
$ mknod crt c 10 1	Zeichenorientiertes Gerät eintragen.
$ mknod fd b 17 3	blockorientiert

Siehe auch mknod

Abgesehen von speziellen Geräten ist der Zugriff im allgemeinen nicht nützlich oder erlaubt.

In UNIX sind Geräte keine Dateien und können auch nicht mit Dateioperationen in ihrem Verhalten manipuliert werden. Ist dies erforderlich, muß ioctl verwendet werden. Unter Plan 9 ist alles Datei, und alles kann durch Dateimanipulation erreicht werden. Ein kill pid wird zu echo kill > /proc/pid/cntl.

4.12.  Block- und Zeichen-Geräte

Bei der Ausgabe von ls erkennt man drei wesentliche Angaben pro Geräte-Inode: b und c unterscheidet block- und zeichenorientierte Geräte, und in jeder der beiden Gruppen gibt es einen ersten Index (major), der die Geräteklasse (Terminal, Platte, ...) wählt, sowie einen zweiten Index (minor), der in der Klasse zum Beispiel ein bestimmtes Gerät oder eine Betriebsart definiert.

Block- und Zeichen-Orientierung ergibt sich daraus, daß der Zugriff im Betriebssystem über zwei Tabellen erfolgt, die primär Funktionen enthalten:

bdevsw[]	.open	.close	.strategy
cdevsw[]	.open	.close	.read	.write	.ioctl

b oder c wählen die Tabelle, major die Zeile in der Tabelle, minor wird als Argument an die Treiberfunktion übergeben. Die Funktionen .open und .close können zum Beispiel exklusiven Zugriff auf ein Gerät erzwingen oder kontrollieren, ob eine Floppy eingelegt ist.

Bei Block-Geräten werden Puffer fixer Größe an .strategy übergeben. Diese Funktion sorgt dann für den Transfer zum Gerät in einer möglichst effizienten Reihenfolge. Siehe auch mknod(8) und mknod(2)

Bei Zeichengeräten transferieren .read und .write sequentiell ziemlich beliebig viele Zeichen. ioctl implementiert Gerätesteuerungen wie stty oder Formatierung einer Floppy.

4.13.  mount

Am Anfang kennt das Betriebssystem nur ein Dateisystem (die root).

inode-Nummern sind (als Vektorindizes) in einem Dateisystem eindeutig. Sie verweisen nicht auf andere Dateisysteme.

Man kann dem Betriebssystem mittels mount mitteilen, daß eine inode temporär im eigenen Betriebssystem als Wurzel eines anderen Dateisystems verstanden werden soll:

$ mount /dev/fd /mnt	Dateisystem auf Gerät mit Hierarchie verbinden.
$ mount	Tabelle zeigen
$ umount /dev/fd	Dateisystem wieder entfernen

Durch mount könnte das früher gezeigte Dateisystem zum Beispiel so erweitert werden:

Whärend des boot-Vorgangs werde in aller Regel die in /etc/fstab festgelegten Dateisystem montiert.

Entfernte Dateisystem werden, falls möglich, mittels automount montiert.

$ cd /var/yp/src
$ ls aut*
auto.direct      auto.direct.was  auto.master
auto.direct.old  auto.home        auto.vol
$ cat auto.home | sed 5q
#       auto.home
#       mounted at /home, everybody's home directory

bischof            portos:/export/home/bischof
hp             -rw,anon=0      thor:/export/home/hp
$ cat /etc/rc.local
....
#
# start up the automounter
#
LOCALSERVER1=thor; export LOCALSERVER1
LOCALSERVER2=thor; export LOCALSERVER2
if [ -f /usr/etc/automount ]; then
        automount  &&           echo -n ' automount '
        # automount -tl ${AUTOTIME:=300} &&             echo -n ' automount'
fi
                                echo '.'
...
$ cat auto.direct
#       auto.direct
#       can be hierarchical, mounted almost always
#
#       LOCALSERVER1, ...       serve /usr/local for architecture
#       ARCH                    architecture-specific

/usr/distrib    -ro     thor:/export/home/distrib

/usr/local      \
        /       -rw     $LOCALSERVER1:/export/local/$ARCH/local \
                        $LOCALSERVER2:/export/local/$ARCH/local \
        /X      -rw     iduna:/export/X11R6

/usr/man        -rw     thor:/export/man/$ARCH
/usr/openwin    -rw     thor:/export/openwin

4.14.  Typische Dateihierarchie

Es gibt viele Varianten. Wenn zum Beispiel die root nur für Lesezugriff montiert wird, werden eine Reihe der hier beschriebenen Dateien und Kataloge mit symbolischen Links in einen Bereich verlegt, für den es Schreibzugriff gibt (/var oder /private). Wenn ein System in einem Netz verwaltet wird, existiert manche Information nicht in Dateien sondern als Tabelle in einer Datenbank (NIS oder NetInfo).

	/	Wurzel
	  bin/	wichtigere Kommandos (sh)
	  boot	Hilfsprogramm zum Systemstart
	  dev/	Geräte
	    console	Systemkonsole
	    fd0	Diskettenlaufwerk
	    lp	Zeilendrucker
	    mem	Speicher insgesamt
	    null	ewige Jagdgründe
	    tty	aktuelles Terminal
	    tty1a	Terminal an serieller Schnittstelle
	    ttyp1	Terminal an Netzverbindung (Pseudo-Terminal)
	  etc/	Systemverwaltung
	    group	Gruppen: Name/Nummer
	    init	Programm für ersten Prozeß
	    mount	Dateisystem verknüpfen
	    passwd	Benutzer: Name/Nummer
	    umount	Dateisystem abmelden
	  home/	Heimatkataloge der Benutzer
	    username/	...der Benutzer username
	      bin/	ganz private Kommandos
	  lib/	Bibliotheken, Hilfsdateien
	    libc.a	C Bibliothek
	  lost+found/	zur Reparatur des Dateisystems
	  tmp/	temporäre Dateien
	  unix	System (für boot, ps)
	  usr/	Benutzer
	    adm/	Systemverwaltung: Verbrauch
	    bin/	restliche Kommandos
	    include/	Definitionsdateien für C
	    lib/	Bibliotheken, Textmakros, ...
	    local/	lokale Entwicklungen
	      bin/	lokale Kommandos
	      lib/	lokale Bibliotheken
	    man/	online Manual
	    spool/	Bereich für Kommunikationsprogramme
	      lp/	Druckaufträge
	      mail/	elektronische Post
	    tmp/	temporäre Dateien (sort)
	  tmp/	temporäre Dateien

4.15.  Pflege

Dateisysteme können inkonsistent werden. Mit fsck(8) kann ein Reperaturversuch unternommen werden. Dies ist im Ernstfall nicht trivial und nicht dem Moment um zu üben. Mit minx kann man gefahrlos üben.

4.16.  minx

minx [Schreiner System-Programmierung in UNIX Band 2, Teubner 1986] ist ein Beispiel für einen UNIX-artigen Dateimanager in Form eines Programms, das unter UNIX und sogar DOS läuft. In dem System kann mit Zugriff auf Dateien und Pflege von Dateisystemen auch ohne Super-User-Privilegien experimentiert werden.

Startet man minx so kann man kurze Beschreibungen der möglichen Kommandos anfordern:

$ minx
...
1 $ cat /man/1	zeigt die allgemeinen Kommandos
1 $ cat /man/4	zeigt die Geräte
1 $ cat /man/8	zeigt Kommandos zur Pflege

minx wurde auch dazu konstruiert, das Innenleben einer Implementierung des Dateisystems zu zeigen. Details findet man in der o.a. Literatur.

Beispiele

$ minx=/home/sp/Skript/Code/Minx/NeXT/minx	Kommando erreichen
$ $minx	aufrufen
minx v1.11
Copyright (c) 1985,
1994 Axel T.
Schreiner,
Osnabrueck,
Germany
1 $ ls	ls ausprobieren
.:
./dev

./man

1 $ ls -l /dev/A:	NeXT Floppy-Laufwerk
br-------- 1   0/0    1,
 0 /dev/A:
1 $ rm -r man	abräumen
1 $ mknod platte b 1 0	Gerät anlegen
1 $ mkfs platte 20	wenn Panel erscheint,
Floppy einlegen,
	damit Dateisystem angelegt werden kann
platte: device 1/0
        isize 1 (16 files),
fsize 17 (20 blocks total)
1 $ mount platte dev	einbinden
1 $ echo hallo >dev/file	Datei erzeugen
1 $ cat /dev/file	und anschauen
hallo
1 $ umount platte	abmelden
1 $ icheck /dev/A:	inodes prüfen
/dev/A:: device 1/0
        isize 1 (16 files),
fsize 17 (20 blocks total)
       15 free blocks
        2 used blocks
        3 used inodes
        1 file
        1 directory
1 $ dcheck platte	links prüfen
platte: device 1/0
             isize 1 (16 files),
fsize 17 (20 blocks total)
1 $ ^D	beenden
$ /usr/etc/disk -e /dev/rfd0b	wieder in UNIX: Floppy entfernen
disk name: Sony MPX-111N 2880
disk type: removable_rw_floppy

Das erste Dateisystem ist Teil des Simulators minx selbst, das heißt, es steht im Hauptspeicher und kann beliebig zerstört werden. Beim nächsten Aufruf von minx ist es neu vorhanden.

Dateisysteme auf Disketten sind natürlich nicht kompatibel mit NeXTSTEP.

Kommandos

NAME	DESCRIPTION	SYNOPSIS

cat	catenate and print	cat [arg...]
cd	change working directory	cd [dir]
chgrp	change group	chgrp id file...
chmod	change mode	chmod mode file...
chown	change owner	chown id file...
cp	copy	cp old new
		cp old...  dir
echo	echo arguments	echo [-n] [arg...]
exit	logout	exit
ln	make a link to a file	ln old [new]
		ln old...  dir
ls	list contents of directory	ls [-adilR] [arg...]
mkdir	make a directory	mkdir dir...
mv	move or rename a file	mv old new
		mv old...  dir
mount	mount file system	mount special name
pwd	print working directory name	pwd
rm	remove files	rm [-r] arg...
rmdir	remove directories	rmdir dir...
set	set trace flags	set [-+!~] [-0..z]
		    [buf inode file io name svc]
sh	(fake) shell	sh  [-+!~] [-0..z]
		    [buf inode file io name svc]
sync	update super block	sync
umount	dismount file system	umount special

^D	logout	^D or end of file

clri	clear inode	clri fs inum...
dcheck	check directory consistency	dcheck [-i inum...] [fs]
icheck	check storage consistency	icheck [-s] [-b bnum...] [fs]
ipatch	modify inode	ipatch fs
login	set user id	login [-e] id
mkfs	construct file system	mkfs fs blocks
mknod	build special file	mknod name b|c major minor
newgrp	set group id	newgrp [-e] id
who	show identification	who

4.17.  Geräte

In der typischen Konfiguration am NeXT wird minx mit einem speicher-residenten Dateisystem gestartet, das folgende Gerätedateien enthält:

	memory	/dev/root	block 0/?	root filesystem
	
	disk	/dev/A:	block 1/0	internal floppy drive
		/dev/B:	block 1/1	[illegal]
	
	null device	/dev/null	character 0/?	read: end of file
				write: black hole
	
	terminal	/dev/tty	character 1/?	read: stdin
		/dev/tty	character 1/!=2	write: stdout
		/dev/err	character 1/2	write: stderr

Eine Floppy darf unbedingt erst dann eingelegt werden, wenn ein entsprechendes Panel erscheint. Nach Ende von minx entfernt man die Floppy mit dem Kommando

$ /usr/etc/disk -e /dev/rfd0b

4.18.  Ablaufverfolgung

minx enthält eine Vielzahl von Ablaufverfolgungen, die mit set bei + gesetzt, bei ! gelöscht und bei ~ komplementiert werden. Sie können einzeln oder in Gruppen angegeben werden, wobei - für alle steht:

0   fpy.c      floppy disk block driver
1   mem.c      memory block driver
2   null.c     null character driver, c.c stubs
3   std.c      stdio terminal character driver
4   name.c     additionally show name scan
5   trace.c    show Uerror if not null
6   tfil_      additionally show inode and data buffer
7   fshow_     show open files
8   ishow_     show inode cache contents
9   bshow_     show buffer cache contents
A   balloc_    calloc next free block to cache buf
B   bcache_    block to cache
C   bclose_    end using device
D   bdone_     release block in cache
E   bfree_     add block to free list
F   blockio    perform i/o for buffer
G   bopen_     begin using device
H   bwrite_    mark block to be written
I   ialloc_    get free inode, set link count to 1 inode
J   icache_    inode to cache
K   idone_     release inode in cache
L   inodeio    perform i/o for inode
M   imount_    mount dev onto dp
N   itrunc_    cut inode to size zero
O   iumount_   dismount dev
P   iunlink_   reduce link count
Q   iwrite_    mark inode to be written
R   fclose_    release file file
S   fcreat_    file from next free inode
T   fd_        unused file descriptor
U   fopen_     file from existing inode
V   fp_        convert file descriptor to open file
W   ftrunc_    cut file to size zero
X   ifread_    read file io
Y   ifseek_    position file
Z   ifwrite_   write file
a   nchdir_    dirty work for chdir/chroot name
b   nlink_     write i,component[] into fp
c   nopen_     file pointer for existing name
d   nprot_     check protection
e   access     check access permission svc
f   chdir      change directory
g   chmod      change mode of a file
h   chown      change owner and group of file
i   chroot     change root directory
j   close      close file access
k   creat      create or truncate file
l   dup        duplicate file descriptor
m   dup2       ...  to particular file descriptor
n   exit       terminate process
o   fstat      get information about file descriptor
p   link       make additional link to file
q   lseek      position
r   mknod      create inode
s   mount      mount file system
t   open       open file access
u   read       read from file descriptor
v   stat       get information about file
w   sync       write all cached blocks
x   umount     unmount file system
y   unlink     remove link to file
z   write      write to file descriptor

5.  Muster

5.1.  Reguläre Ausdrücke

Der Begriff kommt aus der Automatentheorie: reguläre Sprachen kann man mit endlichen Automaten, also ohne Rekursion, analysieren. Nicht analysieren kann man zum Beispiel ein beliebig tiefes Klammergebirge.

Reguläre Ausdrücke sind Suchmuster, die reguläre Sprachen beschreiben und sehr effizient implementiert werden können.

Die Theorie findet man in Compilerbau von Aho, Sethi und Uhlmann, oder in Compiler Design in C von Holub.

Eine mögliche Implementierung samt Simulation ist re.c, gute public-domain-Quellen stammen von GNU.

Für praktischen Gebrauch gibt es reguläre Ausdrücke in Kommandos:

Shell
verwendet Muster für Dateinamen und in der Kontrollstruktur case. find, und cpio verwenden ebenfalls diese Muster.

Editoren
vi, sed und andere verwenden einfache reguläre Ausdrücke (ohne Alternativen und Klammern) zum Suchen und für Textersatz.

fgrep
fgrep sucht nur nach exakten Texten, die zeilenweise angegeben werden, das aber für viele Texte gleichzeitig sehr schnell.

grep
grep sucht nach den gleichen Ausdrücken wie die Editoren.

egrep
egrep verwendet erweiterte reguläre Ausdrücke -- mit Alternativen und Klammern -- zum Suchen.

awk
awk verwendet die gleichen Ausdrücke wie egrep vor allem als Auswahlkriterium zur Bearbeitung von Textzeilen.

perl
perl verwendet die gleichen Ausdrücke wie egrep vor allem als Auswahlkriterium zur Bearbeitung von Textzeilen.

lex
lex compiliert aus den egrep-Ausdrücken ein C Programm, das dann Eingaben erkennen und verarbeiten kann. lex arbeitet nicht zeilenorientiert.

5.2.  Übersicht

Zur Generierung von Dateinamen und bei Kontrollstruktur case. gibt es in der Shell folgende Operationen :

c                    normalerweise stellt ein Zeichen sich selbst dar
\c  'c...'  "c..."   Sonderbedeutung unterdrücken
?                    beliebiges, einzelnes Zeichen
*                    beliebig viele beliebige Zeichen
[...]                einzelnes Zeichen aus Klasse, auch Bereich a-z
[!...]               einzelnes Zeichen nicht aus Klasse (nicht X/Open)
r1r2                 r1 und dann r2
r1|r2                case r1 oder r2

Bei Dateinamen werden . am Anfang und / überall nur explizit erkannt.

Beispiele:

$ ls ?
*

$ ls -l *
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 *
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 c.01
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 c.02
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 c.09
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 ccb
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 ccr
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 fiver

$ ls '*'
*

$ ls -l c*[0-9]
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 c.01
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 c.02
-rw-r--r--   1 bischof  inform          0 Apr 10 10:03 c.09
$ ls -l 'c*[0-9]'
ls: c*[0-9]: No such file or directory

$ ls c.0[!29]
c.01

$ ls ??[!b]
ccr

Bei regulären Ausdrücken gibt es mit abnehmendem Vorrang folgende Operationen :

c                  normalerweise stellt ein Zeichen sich selbst dar
\c                 Sonderbedeutung unterdrücken
^                  Zeilenanfang
$                  Zeilenende
.                  beliebiges, einzelnes Zeichen
[...]              einzelnes Zeichen aus Klasse, auch Bereich a-z
[^...]             einzelnes Zeichen nicht aus Klasse
\(r\)      grep    markierter Ausdruck
\n         grep    was die n-te \(r\) erkannte
r*                 beliebig viele r
r+         egrep   ein oder mehr r
r?         egrep   ein oder kein r
r\{m,n\}           Anzahl, r von m bis n mal
r\{m,\}            Anzahl, r mindestens m mal
r\{m\}             Anzahl, r genau m mal
r1r2               r1 und dann r2
r1|r2      egrep   r1 oder r2
(r)        egrep   Klammerung für Vorrang

Vorsicht: mit Ausnahme von \n bei lex erkennt kein regulärer Ausdruck einen Zeilentrenner.

Beispiele Hier sind zum Vergleich die gleichen Beispiele als grep-Muster:

\.c$               C-Quellen
^cmd\..$           Dateien für cmd
^[A-Z]             Dateinamen mit großen Anfangsbuchstaben
^[^A-Z]            andere Dateinamen
^c\.[1-9]?[0-9]$   ein großes Buch (plus ein Kapitel)
[^./]              Dateinamen mit einem Zeichen, nicht . oder /
^\.                alle Dateinamen, die mit .  anfangen
^\.[^.]            alle mit .  aber nicht .  und ..
^\.(..|[^.])       besser (egrep)

Vorsicht: Reguläre Ausdrücke erkennen die längste Zeichenkette, und sie sind nur verankert, wenn ^ und $ benutzt werden. Manche egreps haben (8-Bit-)Fehler.

Wortspiele

/usr/dict/words (siehe auch spell ) enthält (oft) ein englisches Wörterbuch. Bei NeXT gibt es zusätzlich die Wort- und Stammliste /usr/dict/web2. Kernighan und Pike schlagen folgende Muster für Wortspiele vor:

$ egrep '^a?b?c?d?e?f?g?h?i?j?k?l?m?n?o?p?q?r?s?t?u?v?w?x?y?z?$' \
  /usr/dict/words | sed 3q
a
abc
abet

monotone liefert Worte mit Buchstaben in natürlicher Reihenfolge:

$egrep '^[^aeiou]*a[^aeiou]*e[^aeiou]*i[^aeiou]*o[^aeiou]*u[^aeiou]*$'\
  /usr/dict/words | sed 3q
facetious

liefert Worte mit exakt fünf aufsteigenden Vokalen:

Vorsicht: grep kennt die Option -f nicht.

Nützliche grep-Optionen:

-e re     egrep        Präfix für reguläre Ausdrücke
-f file   nicht grep   Ausdruck aus Datei
-l                     Dateiname ausgeben, falls Zeile gefunden
-n                     Zeilennummer ausgeben
-v                     (veto) nur wenn Muster nicht zutrifft
-x        fgrep        ganze Zeile muß passen
-y                     groß und klein gleich (X/Open: -i)

5.3.  Textersatz

Editoren wie vi, oder sed benützen einfache reguläre Ausdrücke, das heißt, sie erlauben Markierungen. Textersatz kann auf die Markierung Bezug nehmen:

$ echo 'a b' | sed 's/\(.*\) \(.*\)/\2 \1/'
b a

Damit kann man Spalten tauschen oder verdoppeln.

5.4.  Text-Extraktion

sed bearbeitet seine Standard-Eingabe und kann zur Text-Extraktion benützt werden:

$ date | sed 's/.*\(..:..:..\).*/It is now \1/'
It is now 17:31:42

expr kann Muster mit : erkennen; dabei produziert eine Markierung den Text als Standard-Ausgabe:

$ expr "`date`" : '.*\(..:..:..\)'
8:32:21

expr ist ein kleineres Programm als sed aber manchmal verwirrend:

$ echo It is now `expr "\`date\`" : '.*\(..:..:..\)'`
It is now 17:35:05

Man beachte, daß auch set set (in der Shell eingebaut) extrahieren kann:

$ set `date`
$ echo $1 "---" $2
Thu --- Apr
$ date
Thu Apr 10 12:04:15 MET DST 1997
$ 
$ set `date`
$ echo It is now $4
It is now 17:36:48

5.5.  ``Mißbrauch''

$ find / -name wildcard -print
$ du -a / | cut -f 2 | egrep pattern

wildcard bezieht sich auf jede Pfad-Komponente. pattern ist ein erweiterter regulärer Ausdruck, der sich auf den ganzen Pfad bezieht.

$ vi `find / -name test.c -print`

test.c editieren, egal wo es sich befindet. Es gibt Probleme, wenn ein Katalog test.c heißt.

Dieses Problem kann durch

$ vi `find / -type f -name test.c -print`

umgangen werden.

$ vi `fgrep -l variable *.[ch]`

Jede C-Quelle und Definitionsdatei editieren, die einen bestimmten Variablennamen enthält. Nicht empfohlen für Variblennamen wie i.

$ tr ' \12' '\12 ' | tr -d ' ' | grep -- -

Wörter mit Bindestrich zeigen, auch wenn sie über Zeilen getrennt sind.

$ grep '\([^ ]\{2,\}\) \1'

Doppelte Wörter zeigen. Zeigt aber auch gleiche Nach- und Vorsilben.

$ tr ' \12' '\12 ' | grep '\([^ ]\{2,\}\) \1'

Nach- und Vorsilbe am Ende und Anfang aufeinanderfolgender Zeilen. Hat Probleme bei Zeilen mit einzelnen Worten und bei der letzten Zeile.

#!/bin/sh

{ tr ' \12' '\12 '; echo; }                | 
sed '/\([^ ]\{2,\}\) \1/!s/[^ ]//g'     |
tr ' \12' '\12 '                           |
grep -n '[^ ]'

Analog, Ausgabe zeigt aber Position.

#!/bin/sh
	
while read x
do
       eval "case \$x
                  in *$1*) echo \$x
             esac"
done

grep mit Shell-Mustern. echo beschädigt aber die Ausgabe...

6.  Programmierung

6.1.  Was ist ein Kommando?

Ein Kommando besteht aus einer Folge von Worten.

Zuweisung
ist ein Wort, das aus einem Variablennamen, einem Gleichheitszeichen und dem neuen Wert besteht. Der neue Wert ist beliebiger Text, also auch leer.

Variablenname
beginnt mit Buchstabe (auch Unterstrich) und enthält Buchstaben und Ziffern.
Umlenkung
beginnt mit >, >>, <, oder <<. Davor kann noch eine Ziffer stehen, die den File-Deskriptor bezeichnet, auf den sich die Umlenkung bezieht. Der Rest des Worts oder das nächste Wort ist ein Dateiname, beziehungsweise für << das Endsymbol; dieses Wort entsteht nicht durch Musterersatz.

Zuweisungen am Beginn eines Kommandos gelten nur für die Ausführung des Kommandos allerdings mit leichten Unregelmäßigkeiten:

$ x=x	
$ x=5 3>z date				normales Kommando
Thu Apr 10 13:39:25 MET DST 1997
$ echo $x
x
$ x=4 >z echo hi			eingebautes Kommando
$ echo $x
4
$ x=3 >z				leeres Kommando, normale Zuweisung
$ echo $x
3

Umlenkungen können überall im Kommando stehen. Zuweisungen werden nach dem Kommandonamen nur als solche erkannt, wenn die Option -k gesetzt ist. Allerdings werden Variablen ersetzt bevor die Zuweisungen ausgeführt werden, Zuweisungen werden von rechts nach links ausgeführt und die Zuweisungen gelten bei nicht-eingebauten Kommandos nur für das Kommando:

$ set -k
$ x=9; x=10 \
ed \
x=11 y=12 $x; echo $x
9?
! echo $x
10
^D
9
$ x=9; x=10 echo x=11 $x; echo $x	eingebautes Kommando
9
10
$ a=9; echo $a; a=8 b=$a a=10 b=$a$a; echo $a $b
9
8 10

6.2.  Welches Kommando?

Das erste Wort im Kommando, das weder eine Zuweisung noch eine Umlenkung ist, legt fest, welches Kommando ausgeführt wird:

$ test		# eingebaut oder Funktion
$ date		# über PATH gesucht
$ /bin/echo	# absoluter Pfad
$ ./test	# relativ, hier im aktuellen Katalog

Enthält das erste Wort keinen / so wird unter den eingebauten Kommandos, dann den Funktionen und dann in jedem Katalog gesucht, der in PATH angegeben ist. Die Variable wird normalerweise bei der Anmeldung ausreichend definiert.

$ echo $PATH
::/home/bischof/bin/i386:/home/bischof/bin:/usr/bin/X11:...

PATH sollte den aktuellen Katalog als . oder als leeren Eintrag enthalten üblicherweise zuerst. Beim Super-User enthält PATH den aktuellen Katalog a priori aus Sicherheitsgründen nicht.

type zeigt, wo ein Kommando im Dateisystem zuerst gefunden wird. Das Kommando ist in der Shell eingebaut:

$ type awk echo
awk is /bin/awk
echo is a shell builtin

which zeigt, wo ein Kommando im Dateisystem zuerst gefunden wird, falls die csh verwendet wird.

whereis zeigt, Information zu einem Wort gefunden werden kann.

$ whereis man ls
man: /usr/ucb/man /usr/local/man /usr/man/man1/man.1 /usr/man/man7/man.7
ls: /bin/ls /usr/man/man1/ls.1

6.3.  Was ist ein Wort?

Zwischenraum
besteht aus Leerzeichen und Tabulatorzeichen. Zwischenraum trennt Worte. Die Folge \newline wird insgesamt entfernt sie ist kein Zwischenraum.
Worte
sind Zeichenfolgen, die durch Zwischenraum getrennt sind. Zitierter Zwischenraum (mit \ oder einfachen oder doppelten Anführungszeichen) zählt als Teil eines Worts.
\
zitiert das nächste Zeichen; \newline wird ignoriert.
'...'
zitiert alle Zeichen außer einem einfachen Anführungszeichen.
...
erlaubt Textersatz für Variablen und Kommandos. \ zitiert `, " und $.
Kommentare
beginnen mit # und reichen bis zum Zeilenende. # kann dabei auch (nicht zitiert) den Anfang eines Worts bilden.

Das eigentliche Problem besteht darin, daß Textersatz stattfindet.

6.4.  Textersatz für Kommandos

Ein Kommando in `...` wird durch seine Standard-Ausgabe ersetzt. Zeilentrenner am Schluß werden entfernt:

$ echo `hostname` hat `who | wc -l` Benutzer
next hat 2 Benutzer
$ echo linux hat "`rsh linux who | wc -l`" Benutzer
linux hat       1 Benutzer

Kommandos werden auch innerhalb von "..." ersetzt, wobei dann nur ein Argument als Resultat entsteht. Sonst wird die Ausgabe an den Zeichen in Worte zerlegt, die in IFS stehen, wobei zusätzlich gilt

... IFS Internal field separators, normally space, tab, and newline. IFS is ignored if sh is running as root or if the effective user id differs from the real user id. ...

$ IFS=:
$ echo `ypcat passwd | grep bischof | grep H`
bischof 9Y.UyH7/PzuOY 106 11 Hans-Peter Bischof /home/bischof /usr/local/gnu/bin/bash

Allgemein schützen '...' und "..." vor Nachzerlegung an IFS.

$ echo "`ypcat passwd | grep bischof | grep H`"
bischof:9Y.UyH7/PzuOY:106:11:Hans-Peter Bischof:/home/bischof:/usr/local/gnu/bin/bash

Ungeschützte Argumente, Variablenwerte und Kommando-Ausgabe werden nachzerlegt, Dateinamen aber nicht:

$ IFS=:
$ echo hello > a:b; ls            
a:b
$ x='c:d'; echo * $x e:f "$x" `echo '1:2'` "`echo '3:4'`"
a:b c d e f c:d 1 2 3:4

Innerhalb von `...` wird \ zweimal bearbeitet:

$ a='echo `date`'
$ $a	`date`
$ echo $a	
$ `echo $a`
$ `echo \$a`
$ `echo \\$a`
$ `echo \\\$a`
$ `echo \\\\$a`
$ a='eval echo `$a`'
$ $a

Das letzte Beispiel ist eine endlose Schleife...

Man kann `...` schachteln, wenn man die innere Gruppe mit \ schützt:

$ echo Tag: `set `date`; echo $2`
Tag: 21

6.5.  Textersatz für Variablen

Die Werte von Shell-Variablen werden durch $name abgerufen. Die Werte stammen aus Zuweisungen. Eine Folge von Zuweisungen wird von rechts bearbeitet. Wenn nicht durch "..." geschützt wird, wird der Wert dann noch an IFS zerlegt. Der Wert wird nur einmal ersetzt:

$ set -x; IFS=:; a=$b b='c:d    e'
IFS=:
b=c:d   e a=c:d e
$ echo $a "$b"
+ echo c d      e c:d   e 
c d     e c:d   e
$ a='$b'; echo $a
$b

Die Shell setzt einige spezielle Variablen:

$1 ...  $9   Argumente
$#           Anzahl der Argumente
$* und $@    alle Argumente
"$*"         alle Argumente als ein Wort
"$@"         alle Argumente als einzelne Wörter
$-           Optionen der aktuellen Shell
$?           Exit-Code des letzten Kommandos
$!           Prozeßnummer des letzten Hintergrund-Kommandos
$$           Prozeßnummer der Shell (falsch bei Sub-Shell)

Das System verwendet einige Variablen; eine Auswahl:

HOME    login                 Heimatkatalog (für cd)
IFS     space tab newline     trennende Zeichen für Worte
MAIL    leer                  (versucht, Post zu überwachen)
PATH    :/bin:/usr/bin        Suchpfade für Kommandos
PS1     '$ '                  Prompt
PS2     '> '                  Prompt bei Fortsetzung
SHELL   /bin/sh               Name der Shell
TERM    login                 Terminaltyp
TZ      MEZ-1MESZ,M4.5,M9.5   Interpretation des Datums

Mit Doppelanführungszeichen kann man Variablen einigermaßen als Wertelisten verwenden:

$ tee a.c b.c c.c </dev/null
$ o=  
$ for i in *.c; do o="$o `basename $i .c`.o"; done
$ echo $o
a.o b.o c.o

Dabei muß man allerdings sehr vorsichtig mit Zwischenraum umgehen:

$ ls
a    a b  b
$               # Doppelpunkt und Zeilentrenner
$ IFS=':	
> '
$ f=; for i in `ls`; do f="$f:$i"; done
$ set $f
$ echo $2
a b

6.6.  Textersatz für Argumente

Argumente sind $1 bis $9. Der Name des Shell-Skripts selbst ist $0; er kann Pfadanteile enthalten. Das folgende Skript gibt seine Eingabe n-spaltig aus, wobei n der Name des Skripts ist:

#!/bin/sh
pr -t -l1 -`basename $0`

Argumente stammen aus der Kommandozeile, das heißt, der Systemaufruf execv(const char * pfad, const char * argv []); ersetzt den Programmtext des aufrufenden Prozesses durch das Programm in der Datei pfad und übergibt den Vektor argv[] samt seinen Zeichenketten an das neue Hauptprogramm. Der Speicherbedarf des Vektors und der Zeichenketten, also die Länge der expandierten Kommandozeile, ist begrenzt kann aber relativ groß sein.

Alle Argumente (auch mehr als 9) sind durch $* oder $@ erreichbar. "$*" liefert sie als ein Wort, verkettet mit Leerzeichen. "$@" liefert sie als ein Argument pro Wort:

 1      #!/bin/sh
 2      
 3      for i in $*
 4      do
 5              echo '$*:' $i
 6      done
 7      echo "----------------"
 8      
 9      for i in "$*"
10      do
11              echo '"$*":' $i
12      done
13      echo "----------------"
14      
15      for i in $@
16      do
17              echo '$@:' $i
18      done
19      echo "----------------"
20      
21      for i in "$@"
22      do
23              echo '"$@":' $i
24      done
25      
26      exit 0

$ 6.args a b
$*: a
$*: b
----------------
"$*": a b
----------------
$@: a
$@: b
----------------
"$@": a
"$@": b
$ 6.args "A B"
$*: A
$*: B
----------------
"$*": A B
----------------
$@: A
$@: B
----------------
"$@": A B

Argumente können auch mit set gesetzt werden:

$ set `date`; echo $4 17:14:29

shift verschiebt alle Argumente nach links, eventuell um mehrere Positionen:

$ shift `expr $# - 1`; echo $1
1997

Eine Funktion verwendet die gleichen Argumente wie die Shell selbst, das heißt, ein Funktionsaufruf hat praktisch set als Nebeneffekt:

$ f()
> { shift $1; }
$ f 3 a b c d e; echo $*
c d e

Alle Argumente (auch mehr als 9) sind durch $* oder $@ erreichbar. "$*" liefert sie als ein Wort, verkettet mit Leerzeichen. "$@" liefert sie als ein Argument pro Wort:

$ echo heute > a; echo morgen > b; echo jetzt > 'a b'; ls
a    a b  b
$ cat *
heute
jetzt
morgen
$ set *; cat "$@"
heute
jetzt
morgen
$ cat $*
heute
heute
morgen
morgen
$ cat "$*"
cat: a a b b: No such file or directory

Vorsicht: leere Anführungszeichen erzeugen leere Argumente; nicht alle Kommandos reagieren vernünftig auf leere Argumente:

$ e() { ed "$@"; }
$ e a
6
q
$ e          # leert Argumentliste
?
q
$ ls "$@"
a    a b  b
$ rm -i "$@"	leeres Argument
rm: : is a directory
$ rm -i ${1:+"$@"}	kein Argument
Try `rm --help' for more information.

6.7.  Bedingter Textersatz für Variablen

Zum Abruf von Variablenwerten gibt es folgende zusätzliche Syntax:

${name}           {...} begrenzen name gegen nachfolgenden Text
${name?message}   Wert oder Fehlermeldung mit message (auch leer)
${name=default}   Wert oder default; letztere wird dann Wert
${name-default}   Wert oder default; Wert bleibt aber leer
${name+ersatz}    ersatz, falls es Wert gibt; sonst nichts
Der Test bezieht sich mit Doppelpunkt darauf, ob name existiert und keinen leeren Wert besitzt. Ohne Doppelpunkt wird nur Existenz geprüft.

Eine Zuweisung an Argumente, also etwa ${1=default}, ist nicht möglich. Die anderen Formen sind auch für Argumente erlaubt. Der Ersatz muß jeweils ein einziges Wort sein.

6.8.  Dateinamen

Nach Ersatz von Variablen und Kommandos werden aus den nicht zitierten Wörtern, also auch aus dem Ersatztext von Variablen, Dateinamen generiert. Aus einem Wort entsteht jeweils eine alphabetisch sortierte Namensliste die Listen werden nicht zusammengefügt. Das Resultat der Generierung wird weder nachersetzt noch an IFS zerlegt.

$ echo hi > a=b
$ a=*
$ echo "$a"
*
$ echo $a
a=b

Allgemein folgt, daß man Variablen und Kommandos wohl grundsätzlich mit "..." schützen sollte:

$ read x; echo $x
*
a=b

Da Muster in Variablen aufbewahrt werden, muß man Dateilisten sorgfältig konstruieren:

$ tee a b c </dev/null
$ files=*; echo "$files" $files
*
a a=b b c
$ files=`echo *`; echo "$files"
a a=b b c
$ `echo ???`
a=b: execute permission denied
$ eval `echo ???`; echo $a
b

6.9.  Was ist ein Kommando?

einfaches Kommando

besteht aus einer expandierten Folge von Worten, abgeschlossen durch Zeilentrenner oder andere Zeichen.

Zuweisungen am Anfang (nach set -k überall) und Umlenkungen werden ausgelassen; das erste übrige Wort ist das Kommando, der Rest sind Argumente. Die Umlenkungen werden angewendet, die Zuweisungen werden normalerweise lokal zum Kommando exportiert.

Kommando

ist ein einfaches Kommando oder eine Kontrollstruktur.
Pipeline |

ist eine Folge von Kommandos, unterteilt mit |. Das rechteste Kommando ist Abkömmling der Shell und entscheidet den Exit-Code.

logische Liste && ||

ist eine Folge von Pipelines, unterteilt mit && oder ||. Die Folge wird von links her abgearbeitet:
a && b Kommando b nur, wenn Kommando a den Exit-Code 0 liefert.
a || b Kommando b nur, wenn Kommando a nicht den Exit-Code 0 liefert.

Liste newline ; &

ist eine Folge von logischen Listen, abgetrennt mit beliebig vielen Zeilentrenner oder Semikolons, damit Kommandos nacheinander abgearbeitet werden. Eine mit & beendete logische Liste wird asynchron abgearbeitet, das heißt, die Shell wartet nicht auf das Prozeßende.
$ test -f file && rm file &	# insgesamt im Hintergrund

6.10.  Kontrollstrukturen

Ein Kommando ist entweder eine Liste, also auch eine Pipeline oder ein einfaches Kommando, oder es ist eine Kontrollstruktur:

for variable	# durchläuft $1 ...
do	liste
done
    
for variable in wort...
do	liste
done
    
while liste	# Exit-Code Null?
do	liste
done
   
until liste	# Exit-Code nicht Null?
do	liste
done
   
if liste	# Exit-Code Null?
then	liste
elif liste	# viele elif ...können entfallen
then	liste
else	liste	# ein else ...kann entfallen
fi
   
case wort
in muster | muster )	# wie Dateimuster,
erkennen auch /
	liste
;; muster )	# viele Fälle
	liste
esac
   
{ liste ;}	# Zusammenfassung,
gleiche Shell
( liste )	# Sub-Shell (also fork)
    
name() { liste ;}	# definiert Funktion

Der Exit-Code stammt vom letzten Kommando if, while und until liefern Null, wenn kein Kommando ausgeführt wird.

E/A-Umlenkung ist für die ganze Kontrollstruktur möglich, denn sie gilt wieder als Kommando. Das führt dann aber zu Sub-Shells.

Vorsicht: Die ganzen steuernden Worte sind nicht reserviert insbesondere sind nur die runden Klammern reserviert, die geschweiften nicht!

6.11.  Eingebaute Kommandos

Eine Reihe von Kommandos sind in der Shell eingebaut, Kommandos wie cd oder set -k wirken auf Informationen in der Shell selbst, bei anderen wie echo oder test geschieht das aus Effizienzgründen. Nach XPG3 sind eingebaut:

:
Kommentar; wird aber ausgeführt und liefert 0.

. datei
datei wird auf PATH gesucht und in der gleichen Shell ausgeführt, damit kann man per Skript Variablen in der eigenen Shell setzen.

break [anzahl]
for-, while- und until-Schleifen verlassen.

cd [pfad]
Arbeitskatalog auf HOME oder pfad setzen.

continue [anzahl]
for-, while- und until-Schleifen fortsetzen.

echo [arg...]
Argumente ausgeben (Vorsicht bei \).

eval arg...
Argumente einlesen, parsieren und ausführen damit findet vor allem nochmals Textersatz und Zerlegung statt.
$ eval 'date; who'
Thu Apr 10 15:12:43 MET DST 1997
bischof  ttyp1    Apr 10 09:00 (:0.0)

exec arg...
Argumente werden als Kommando an Stelle dieser Shell ausgeführt. Wenn nur Umlenkungen angegeben sind, wirken sie sich auf diese Shell aus.
$ exec >&2 2>/dev/null

exit [code]
Shell verlassen. Exit-Code ist code oder stammt vom letzten Kommando.

export [name...]
Variablen für Export markieren oder exportierte zeigen.

hash name...
Zugriffsweg zu name notieren.

hash [-r|name...]
Zugriffswege zeigen oder löschen.

pwd
Arbeitskatalog zeigen.

read name...
Eine Zeile einlesen, an IFS zerlegen und die Wörter an die Variablen zuweisen. Restliche Wörter gehen (zerlegt) an die letzte Variable. (Ohne Argumente endet offenbar die Shell.)

readonly [name...]
Schreibzugriff auf Variablen sperren oder gesperrte zeigen.

return [code]
Funktion verlassen. Exit-Code ist code oder stammt vom letzten Kommando.

set [optionen arg...]
Variablenwerte zeigen, Argumente an $1, ... zuweisen oder Optionen setzen .

shift [anzahl]
Argumente ``nach links schieben''. Das erste oder die anzahl ersten Argumente gehen verloren, $*, $@ und $# werden korrigiert. Fehler, falls es nichts zu schieben gibt.

test [arg...]
Bedingungen prüfen .

times
Zeitverbrauch der Subprozesse zeigen .

trap [signal...]
Signalvereinbarungen zeigen oder löschen.

trap '' signal...
Signale ignorieren.

trap cmd signal...
cmd für Signale vereinbaren. 0 als signal bedeutet ``bei Verlassen der Shell''. Vor Start der Shell ignorierte Signale kann man nicht beeinflussen.

type name...
Ausgeben, was name als Kommando bedeutet.

ulimit [-f blöcke]
Maximale Dateigröße zeigen oder limitieren.

umask [ooo]
Maske für Zugriffsschutz neuer Dateien zeigen oder (oktal) setzen. 1-Bits in der Maske sind 0 im Zugriffsschutz.

unset name...
Variable oder Funktion löschen.

wait [pid]
Ende eines (bestimmten) erzeugten Prozesses abwarten.

6.12.  Optionen

Fast alle Optionen können mit set eingeschaltet (-x) und ausgeschaltet (+x) werden. Alle Optionen können als Kommandoargumente beim Aufruf von sh verwendet werden. Nach XPG3 gibt es folgende Optionen:

±a
alle neu erzeugten oder geänderten Variablen werden exportiert.

-c string
Kommandos in string werden ausgeführt (für system()).

±e
exit, wenn ein Kommando mit Fehler endet.

±f
(file) keine Expansion von Mustern für Dateinamen.

-i
interaktiv (ebenso, wenn Terminal zur Eingabe und Ausgabe dient): Prompt, SIGTERM wird ignoriert, SIGINT wird abgefangen. SIGQUIT wird immer ignoriert.

±k
(keyword) Zuweisungen auch nach dem Kommandonamen erkennen.

±n
Kommandos lesen aber nicht ausführen.

-r
(restricted) eingeschränkte Funktionalität.

-s
Standard-Eingabe bearbeiten, auch wenn Argumente angegeben sind. Ausgabe zu File-Deskriptor 2.

±t
ein Kommando lesen und ausführen.

±u
undefinierte Variablen reklamieren.

±v
(verbose) Eingabezeilen ausgeben.

±x
Kommandos unmittelbar vor Ausführung ausgeben.

zum Beispiel bei set

-a, -k und -u realisieren einen anderen Stil zum Umgang mit Variablen. -n, -v und -x dienen vor allem zur Fehlersuche. Mit -c und -i zusammen mit -t kann man Shell-Service in anderen Applikationen realisieren.

-e hat etwas verblüffende Eigenschaften:

$ set -e; false; echo $?
1
$ test; echo $?
1
$ false

Offenbar wirkt -e erst auf die nächste Zeile und nicht auf Fehler in eingebauten Kommandos.

6.13.  Fragen

Ein Skript soll als optionales Argument ein Zahl (NF) bekommen, und danach Text-Zeilen einlesen, und von der Zeile das NF. Feld ausgeben. Der Aufruf-syntax könnte wie folgt aussehen:

nf [i]

Man kann davon ausgehen, daß i eine Zahl ist. Das Problem soll ohne cut gelöst werden.

Siehe 6.nf_[12]

Erkären Sie folgenden Sachverhalt:

artemis Src 61 6.nf_2 1
Ein    
Ein
-x
+ eval echo $1 
+ echo Ein 
Ein
+ read LINE 

Siehe 6.nf_3

Die Argumentbehandlung geht noch etwas kürzer. Siehe 6.nf_4

7.  Beispiele I

Viele Probleme haben eine relativ einfache erste Lösung. Von dort bis zu einem robusten Skript für den täglichen Gebrauch kann es aber ein weiter Weg sein. Deshalb erscheinen hier in der Regel mehrere Lösungen zum gleichen Problem. Dieses Kapitel beschäftigt sich mit relativ kleinen Skripten.

Shell-Virus

McIlroy gab folgendes Rezept für einen Shell-Virus an [unix/mail 1/1990]:

 1      #!/bin/sh
 2              
 3      for i in "$@"                                   #virus#
 4      do      case "`sed 1q $i`"
 5              in "#!/bin/sh") grep '#virus#' $i > /dev/null ||
 6                              sed -n '/#virus#/,$p' $0 >> $i
 7              esac
 8      done 2> /dev/null
 9      
10      exit 0

Dies hat folgende Konsequenz:

$ cat v1
#!/bin/sh
        echo '#virus#'
exit 0
$ 7.virus v1
$ cat v1
#!/bin/sh
        echo '#virus#'
exit 0

$ cat v2
#!/bin/sh

$ 7.virus v2
$ cat v2
for i in "$@"                                   #virus#
do      case "`sed 1q $i`"
        in "#!/bin/sh") grep '#virus#' $i > /dev/null ||
                        sed -n '/#virus#/,$p' $0 >> $i
        esac
done 2> /dev/null

exit 0

7.1.  Boot-Diskette

Für den Fall, daß ein System ernsthaft kaputtgeht, sollte man sich zum Beispiel ein minimales System auf Diskette zurechtlegen. UNIX-Kerne sind heute jedoch so groß, daß man nur noch sehr wenige Programme in einem solchen Mini-System zur Verfügung stellen kann. Die folgenden Funktionen emulieren deshalb verschiedene nützliche Programme nur mit Hilfe der Shell und dd Da man sie im Ernstfall eintippen muß, sind sie möglichst kurz und nicht unbedingt robust.

Cat

 1              cat() { # [file ...]    simuliert cat mit dd
 2                      if [ $# = 0 ] 
 3                      then    dd 2>&-
 4                      else    for i
 5                              do      dd 2>&- < $i
 6                              done
 7                      fi
 8              }
 9      

Cut

 1      cut() { # [field-number]    extrahiert ein [erstes] Feld
 2               nf=${1:-1}
 3               while read line
 4               do      set -- $line
 5                       eval echo \$$nf
 6               done
 7      }
 8      

Five -- fünf Argumente pro Zeile

 1      five() { # arg...       fuenf Argumente pro Zeile
 2              until [ $# -lt 5 ]
 3              do      echo "$1 $2 $3 $4 $5"
 4                      shift; shift; shift; shift; shift
 5              done
 6              [ $# -gt 0 ] && echo "$*"
 7      }

ls -R

 1      lsR() { # [path...]             rekursiv, eingerueckt
 2              [ $# = 0 ] && set -- *
 3              for i
 4              do      if [ -f "$i" ]
 5                      then    echo "$indent$i"
 6                      elif [ -d "$i" ]
 7                      then (  echo "$indent$i"/
 8                              cd "$i"
 9                              indent="$indent  "; lsR
10                      ) fi
11              done
12      }

ls -L

 1      lsL() { # [path...]             viel Information
 2              [ $# = 0 ] && set -- *
 3              for i
 4              do      if   [ -f $i ]; then t=-
 5                      elif [ -d $i ]; then t=d
 6                      elif [ -c $i ]; then t=c
 7                      elif [ -b $i ]; then t=b
 8                      elif [ -p $i ]; then t=p
 9                      fi
10                      r=-; [ -r $i ] && r=r
11                      w=-; [ -w $i ] && w=w
12                      if [ -x $i ]
13                      then    x=x; [ -u $i ] && x=s
14                      else    x=-; [ -u $i ] && x=S
15                      fi
16                      echo $t$r$w$x $i
17              done
18      }

ls -- rekursiv, volle Pfade

 1      lsP() { # [path...]             rekursiv, volle Pfade
 2              [ $# = 0 ] && set -- *
 3              for i
 4              do      if [ -f "$i" ]
 5                      then    echo "$indent$i"
 6                      elif [ -d "$i" ]
 7                      then (  cd "$i"
 8                              indent="$indent$i/"; lsP
 9                              echo "$indent"
10                      ) fi
11              done
12      }

Gruppen von fuenf Zeilen ausgeben

 1      pg() { #        Gruppen von fuenf Zeilen ausgeben
 2      (       pglen=xxxxx IFS=
 3              while read line
 4              do      echo "$line"; count=x
 5                      until [ $count = $pglen ]
 6                      do      read line || break 2
 7                              echo "$line"; count=x$count
 8                      done
 9                      read line </dev/tty
10              done
11      )
12      }

7.2.  man

Typischerweise sind die Quellen der Manualseiten öffentlich und müssen mit nroff formatiert werden. Diese Version von man geht davon aus, daß die Seiten bereits formatiert sind:

 1      #!/bin/sh
 2      PATH=/usr/ucb:/bin:/usr/bin
 3      
 4      section=? exit=0
 5      
 6      files=
 7      while [ $# -gt 0 ]
 8      do    case $1
 9          in [1-8])    section=$1
10          ;; *)        found=
11                  for dir in /usr/local/gnu/man       \
12                             /usr/local/man /usr/man
13                  do    f=`echo $dir/cat$section/$1.*`
14                      case $f
15                      in *\*)
16                      ;; *)    found="$found $f"
17                      esac
18                  done
19                  if [ -z "$found" ]
20                  then   
21                      echo >&2 "`basename $0`: $1 not found"
22                      exit=1
23                  else    files="$files $found"
24                  fi
25          esac
26          shift
27      done
28      
29      if [ "$files" ]
30      then    cat $files
31      else    echo >&2 "usage: `basename $0` [section] page ..."
32          exit=1
33      fi
34      exit $exit

Die Formatierung übernimmt normalerweise das Berkeley-Kommando catman das ungefähr folgendermaßen funktioniert:

 1      #!/bin/sh
 2      PATH=/usr/ucb:/bin:/usr/bin
 3      
 4      for dir in /usr/local/gnu/man /usr/local/man /usr/man
 5      do    ( cd $dir
 6            for i in 1 2 3 4 5 6 7 8 n l
 7            do    if [ -d man$i ]
 8                  then    [ -d cat$i ] || mkdir cat$i
 9                  j=`ls -t cat$i | sed 1q`
10                  ( cd man$i
11                    case $j
12                    in '') j=`ls`
13                    ;; *)  j=`find . -newer ../cat$i/$j -print`
14                    esac
15                    for j in $j
16                    do    nroff -man -u -h $j |
17                      uniq |
18                      sed 's/^    //' > ../cat$i/$j
19                    done
20                  )
21              fi
22            done
23          )
24      done

7.3.  cal

Kernighan und Pike führen vor, daß man einem Kommando wie cal eine elegantere Benutzeroberfläche geben kann:

 1      #!/bin/sh       
 2      PATH=/usr/ucb:/bin:/usr/bin     # sonst rekursiv...
 3      
 4      case $#
 5      in 0)
 6        set `date`; m=$2; eval y=\$$# # cal   heute
 7      ;; 1)
 8        m=$1; set `date`; eval y=\$$# # cal x Monat x im Jahr
 9      ;; *)
10        m=$1; y=$2    # cal x y       Monat x im Jahr y
11      esac
12      
13      case $m
14      in [1-9] | 1[0-2])              # Monatszahl
15      ;; [0-9]*)      y=$m; m=        # Jahreszahl
16      ;; [jJ]an*)     m=1             # Monat als Name
17      ;; [fF]eb*)     m=2
18      ;; [mM]a[er]*)  m=3
19      ;; [aA]pr*)     m=4
20      ;; [mM]a[iy]*)  m=5
21      ;; [jJ]un*)     m=6
22      ;; [jJ]ul*)     m=7
23      ;; [aA]ug*)     m=8
24      ;; [sS]ep*)     m=9
25      ;; [oO][ck]t*)  m=10
26      ;; [nN]ov*)     m=11
27      ;; [dD]e[cz]*)  m=12
28      ;; *)   echo >&2 "usage: `basename $0` [month [year]]"
29                      exit 1
30      esac
31      
32      exec cal $m $y

Den Fallverteiler kann man eleganter aufschreiben:

 1      #!/bin/sh       
 2      PATH=/usr/ucb:/bin:/usr/bin     # sonst rekursiv...
 3      
 4      case $#
 5      in 0)   set `date`; m=$2; eval y=\$$#   # cal
 6      ;; 1)   m=$1; set `date`; eval y=\$$#   # cal x
 7      ;; *)   m=$1; y=$2                      # cal x y
 8      esac
 9      
10      while :
11      do      case $m
12              in [1-9] | 1[0-2])              # Monatszahl
13              ;; [0-9]*)      y=$m; m=        # Jahreszahl
14              
15              ;; *)   for i in '1 an' '2 eb' '3 ar' \
16                        '3 ae' '4 pr' '5 ay' '5 ai' \
17                        '6 un' '7 ul' '8 ug' '9 ep' \
18                        '10 ct' '10 kt' '11 ov'     \
19                        '12 ec' '12 ez'
20                      do      set $i
21                              case $m
22                              in ?$2*) m=$1   # Monatsname
23                                      break 2
24                              esac
25                      done
26                      echo >&2 "usage: `basename $0` \c"
27                      echo >&2 " [month [year]]"
28                      exit 1
29              esac
30              break
31      done
32      exec cal $m $y

7.4.  type which

type zeigt zwar was die aktuelle Shell über einen Kommandonamen denkt, aber ein Shell-Skript wird normalerweise PATH selbst setzen. Kernighan und Pike führen ein Kommando which vor, das die Pfad-Suche simuliert:

 1      #!/bin/sh
 2      
 3      : ${1:?'no command name'}
 4      
 5      for path in `echo $PATH | sed 's/^:/.:/
 6                                      s/:$/:./
 7                                      s/::/:.:/g
 8                                      s/:/ /g'`
 9      do      if [ -x $path/$1 ]
10              then    echo $path/$1
11                      exit 0
12              fi
13      done
14      exit 1

Das Skript sollte mehrere Argumente erlauben und alle Fundstellen ausgeben:

 1      #!/bin/sh
 2      
 3      paths=`echo $PATH |
 4      sed -e 's/^:/.:/'       \
 5          -e 's/:$/:./'       \
 6          -e 's/::/:.:/g'     \
 7          -e 's/:/ /g'`
 8      exit=1
 9      
10      for name
11      do      for path in $paths
12              do      [ -x $path/$name ] && exit=0 echo $path/$name
13              done
14      done
15      exit $exit

Ein Skript sollte grundsätzlich PATH setzen, um absichtliche oder unabsichtliche trojanische Pferde zu vermeiden. Außerdem sollte ein Argument mit / nicht gesucht werden. Ein Katalog ist kein nützliches Resultat. Letztlich besteht die Möglichkeit, daß test -x (noch) nicht verfügbar ist:

 1      #!/bin/sh
 2      
 3      paths=`echo $PATH |
 4      PATH=/usr/ucb:/bin:/usr/bin \
 5      sed -e 's/^:/.:/' -e 's/:$/:./' \
 6          -e 's/::/:.:/g' -e 's/:/ /g'`
 7      PATH=/usr/ucb:/bin:/usr/bin
 8      exit=1
 9      
10      if [ -x /tmp ] 2> /dev/null     # not every test has -x
11      then    has_x=yes
12      else    has_x=
13      fi
14      
15      executable() { # path
16              if [ "$has_x" ]
17              then    [ ! -d "$1" -a -x "$1" ]
18              else    [ `expr "\`ls -l \"$1\" 2> /dev/null \
19                         \`" : '-..x'` = 4 ]
20              fi
21      }
22      
23      for name
24      do      case $name
25              in */*)
26                executable "$name" && exit=0 echo "$name"
27              ;; *)
28                for path in $paths
29                do
30                  executable "$path/$name" &&
31                  exit=0 echo "$path/$name"
32                done
33              esac
34      done
35      exit $exit

Um am Schluß keinen Zeilentrenner auszugeben, benötigen manche Versionen von echo die Option -n, andere schließen den Text mit \c ab.

7.5.  echo

Die folgende Funktion beruht auf einer Idee von Skalla und ist portabel:

 1      #!/bin/sh
 2      
 3      prompt() { # arg ...
 4              c=`echo '\c'`
 5              n=; : ${c:+${n:=-n}}
 6              c=; : ${n:-${c:=\\c}}
 7              echo $n "$@"$c
 8      }
 9      
10      [ prompt = `PATH=/usr/ucb:/bin:/usr/bin basename "$0"` ] && prompt "$@"

Man kann prompt auf verschiedene Arten aufrufen:

$ set a b c
$ . prompt; type prompt
prompt is a function
prompt(){
c=`echo '` 
n= 
: ${c:+${n:=-n}} 
c= 
: ${n:-${c:=} 
echo $n "$@"$c 
}
$ prompt a b c
a b c$ unset prompt; prompt a b c
a b c$ type prompt
prompt is hashed (prompt)

7.6.  newer

newer setzt einen exit-Code, der angibt, ob eine bestimmte Datei neuer als eine andere ist. Die Aufgabe geht auf Kernighan und Pike zurück, aber das Problem ist verblüffend schwierig zu lösen.

ls -t gibt seine Argumente zeitlich sortiert aus. Folglich könnte eine Lösung so aussehen:

 1      #!/bin/sh
 2      PATH=/usr/ucb:/bin:/usr/bin
 3      set -f
 4      
 5      if [ $# != 2 ]
 6      then    echo >&2 "usage: `basename $0` newfile oldfile"
 7              exit 1
 8      fi
 9      
10      ls -dt "$@" |
11      { read new && [ X"$new" = X"$1" ] && exit 0
12        exit 1
13      }

Leider funktioniert diese Lösung nicht, wenn eine der Argument-Dateien nicht existiert:

$ newer0 . a; echo $?; newer0 a .; echo $?
a not found
0
a not found
1

Man muß offenbar zuerst den exit-Code von ls auswerten:

 1      #!/bin/sh
 2      # PATH=/usr/ucb:/bin:/usr/bin
 3      set -f
 4      
 5      if [ $# != 2 ]
 6      then    echo >&2 "usage: `basename $0` newfile oldfile"
 7              exit 1
 8      fi
 9      
10      if ls -dt "$@" > /dev/null
11      then    ls -dt "$@" |
12              { read new && [ X"$new" = X"$1" ] && exit 0
13                  exit 1
14              }
15      else    exit 1
16      fi

Ohne eigenen PATH wird GNU-ls verwendet und die Lösung funktioniert. Setzt man PATH, stellt sich heraus, daß NeXT-ls den exit-Code 0 auch dann liefert, wenn ein Argument nicht existiert.

$ PATH=/bin sh -c './newer2 a .; echo $?; ./newer2 . a; echo $?'
a not found
a not found
1
a not found
a not found
0

Vielleicht sollte man versuchen, find -newer zu verwenden:

 1      #!/bin/sh
 2      PATH=/usr/ucb:/bin:/usr/bin
 3      set -f
 4      
 5      if [ $# != 2 ]
 6      then    echo >&2 "usage: `basename $0` newfile oldfile"
 7              exit 1
 8      fi
 9      
10      test X"`find "$1" -newer "$2" -print | sed 1q`" = X"$1"

Alle bisherigen Lösungen funktionieren außerdem nicht besonders gut, wenn ein Dateiname mit einem Minuszeichen anfängt oder gar einen Zeilentrenner enthält.

 1      #!/bin/sh
 2      PATH=/usr/ucb:/bin:/usr/bin
 3      set -f
 4      
 5      if [ $# != 2 ]
 6      then    echo >&2 "usage: `basename $0` newfile oldfile"
 7              exit 1
 8      fi
 9      
10      test X"`ls -dt -- "$@"; echo .`" = X"$1
11      $2
12      ."

Der Aufruf von echo verhindet zwar, daß abschließende Zeilentrenner aus der Ausgabe von ls entfernt werden können, aber wenn Dateinamen nur aus Zeilentrennern bestehen, gibt es noch immer Probleme. Außerdem ist das Resultat auch dann wahr, wenn beide Dateien exakt gleich alt sind...

7.7.  pick

pick bietet Argumente zur Auswahl an und gibt die ausgewählten Argumente als Standard-Ausgabe aus:

 1      #!/bin/sh
 2      . 7.prompt
 3      
 4      for arg
 5      do      prompt "$arg"; read x
 6              case $x
 7              in [yYjJ]*)     echo "$arg"
 8              esac
 9      done

[Kernighan & Pike] schreibt die Idee Tom Duff zu. pick erlaubt interaktive Auswahl bei allen Kommandos:

$ pr `pick *.c` | lpr

So funktioniert die Lösung auch in einer Pipeline.

 1      #!/bin/sh
 2      . 7.prompt
 3      
 4      for arg
 5      do      prompt >&2 "$arg" '? '; read x <&2
 6              case $x
 7              in [yYjJ]*)     echo "$arg"
 8              esac
 9      done

So wählt man garantiert mit dem Terminal aus:

 1      #!/bin/sh
 2      . 7.prompt
 3      
 4      for arg
 5      do      prompt >/dev/tty "$arg" '? '; read x </dev/tty
 6              case $x
 7              in [yYjJ]*)     echo "$arg"
 8              esac
 9      done

So ist der Zugriff auf das Terminal billiger:

 1      #!/bin/sh
 2      . 7.prompt
 3      
 4      exec 3>/dev/tty 4</dev/tty
 5      
 6      for arg
 7      do      prompt >&3 "$arg" '? '; read x <&4
 8              case $x
 9              in [yYjJ]*)     echo "$arg"
10              esac
11      done

Wenn - das einzige Argument ist, wird die Standard-Eingabe angeboten:

 1      #!/bin/sh
 2      . 7.prompt
 3      
 4      case $#
 5      in 0)   exit 0
 6      ;; 1)   case "$1"
 7              in -)   while read arg
 8                      do      prompt >&2 "$arg" '? '; read x <&2
 9                              case $x
10                              in [yYjJ]*)     echo "$arg"
11                              esac
12                      done
13                      exit 0
14              esac
15      esac
16      
17      for arg
18      do      prompt >&2 "$arg" '? '; read x <&2
19              case $x
20              in [yYjJ]*)     echo "$arg"
21              esac
22      done
23      exit 0
24      

So beendet control-D die Auswahl:

 1      #!/bin/sh
 2      . 7.prompt
 3      
 4      case $#
 5      in 0)   exit 0
 6      ;; 1)   case "$1"
 7              in -)   while read arg
 8                      do
 9                        if prompt >&2 "$arg" '? '; read x <&2
10                        then
11                              case $x
12                                 in [yYjJ]*)  echo "$arg"
13                                 esac
14                        else
15                               echo
16                               exit 0
17                        fi
18                      done
19                      exit 0
20              esac
21      esac
22      
23      for arg
24      do      prompt >&2 "$arg" '? '; read x <&2
25              case $x
26              in [yYjJ]*)     echo "$arg"
27              esac
28      done
29      exit 0

Mit einer Funktion ist das besser strukturiert:

 1      #!/bin/sh
 2      
 3      c=`echo '\c'`; n=; : ${c:+${n:=-n}}; c=; : ${n:-${c:=\\c}}
 4      
 5      pick() { # arg ...
 6              if echo >&2 $n "$1 ? "$c; read x <&2
 7              then    case $x
 8                      in [yYjJ]*)     echo "$1"
 9                      esac
10              else    echo
11                      exit 0
12              fi
13      }
14      
15      case $#
16      in 0)   exit 0
17      ;; 1)   case "$1"
18              in -)   while read arg
19                      do      pick "$arg"
20                      done
21                      exit 0
22              esac
23      esac
24      
25      for arg
26      do      pick "$arg"
27      done
28      exit 0

bundle verpackt Textdateien so als Shell-Skript, daß sie am Ziel durch Ausführen des Shell-Skripts wiederhergestellt werden können:

7.8.  bundle

 1      #!/bin/sh
 2      PATH=/usr/ucb:/bin:/usr/bin
 3      
 4      for i
 5      do      echo "echo $i >&2"
 6              echo "cat >$i << 'Ende von $i'"
 7              cat $i
 8              echo "Ende von $i"
 9      done

Kernighan und Pike schreiben bundle Alan Hewett und James Gosling zu. Ehrgeizige Implementierungen erreichen fast die Fähigkeiten von tar.

Hier ist eine mittelprächtige Lösung ohne Funktionen, die Kataloge rekursiv bearbeitet, Zugriffsschutz berücksichtigt und mit wc eine Art Prüfsumme übermittelt:

 1      #!/bin/sh
 2      PATH=/usr/ucb:/bin:/usr/bin
 3      bundle=`pwd`/bundle             # for recursive call
 4      exit=1
 5      
 6      if [ -x /tmp ] 2> /dev/null     # not every test has -x
 7      then    has_x=yes
 8      else    has_x=
 9      fi
10      
11      case $1
12      in -r)  rflag=y; shift
13      ;; *)   rflag=n
14              echo '# to unbundle set umask and sh this file'
15              tmp=/tmp/bundle$$
16              echo 'exit=1 tmp=/tmp/bundle$$'
17              trap "trap 0; rm -f $tmp;
18                      exit \$exit" 0 1 2 3 15
19              echo 'trap "trap 0; rm -f $tmp;
20                      exit \$exit" 0 1 2 3 15'
21              echo 'exec > $tmp'; exec 3> $tmp
22      esac
23      
24      for i 
25      do      if [ -d $i ]
26              then    echo "[ -d $i ] || mkdir $i"
27                      $bundle -r $i/*
28              elif [ -r $i ]
29              then    echo "sed 's/^  //' > $i << 'End of $i'"
30                      sed 's/^/       /' $i
31                      echo "End of $i"
32                      echo "wc $i"; wc $i >&3
33                      if [ "$has_x" ]
34                      then    [ -x $i ]
35                      else    [ `expr "\`ls -l $i\`" : '...x'` = 4 ]
36                      fi && echo "chmod +x $i"
37              else    continue
38              fi
39              [ -w $i ] || echo "chmod -w $i"
40              echo "echo >&2 $i"; echo >&2 $i
41      done
42      
43      case $rflag
44      in n)   echo "diff -b - \$tmp << 'End of wc' >&2"
45              exec 3>&-
46              cat $tmp
47              echo 'End of wc'
48              exit=0
49              echo 'exit=$?'
50      esac

Die Lösung ist nicht sehr elegant, da sie sich selbst rekursiv aufruft. I bundle muß entweder auf PATH zu finden sein, oder man muß den Pfad bei der Installation explizit setzen. Außerdem wird relativ viel Code quasi verdoppelt. Dies kann man mit Funktionen vermeiden.

 1      #!/bin/sh
 2      
 3      usage() {
 4              echo >&2 "usage: `basename $0` [-y] \c"
 5              echo >&2 " file... dir... > bundle"
 6              exit 1
 7      }
 8      
 9      if [ -x /tmp ] 2> /dev/null     # not every test has -x
10      then    has_x=yes
11      else    has_x=
12      fi
13      
14      case "$1"
15      in '')  usage
16      ;; -y)  yflag=-y; shift
17              [ $# = 0 ] && usage
18      ;; *)   yflag=
19      esac
20      
21      for i
22      do      case "$i"
23              in *'
24      '* | *' '* | *' '* )    echo >&2 "$i: illegal filename"
25                              exit 1
26              esac
27      done
28      argv="$*"       
29              
30      dup() { # arg ...
31              echo "$*"
32              eval "$@"
33      }
34      
35      echo '# to unbundle set umask and sh this file'
36      echo '# specify a parameter to suppress queries'
37      
38      dup PATH=/usr/ucb:/bin:/usr/bin
39      dup exit=1
40      dup tmp='/tmp/bundle$$'
41      echo 'yflag=$1'
42      
43      dup 'trap "trap 0; rm -f $tmp; exit \$exit" 0 1 2 3 15'
44      
45      dup 'ask() {
46              if [ "$yflag" ]
47              then    echo >&2 $*
48              else    if [ "`echo -n`" ]
49                      then    echo >&2 $* "? \\c"
50                      else    echo >&2 -n $* "? "
51                      fi
52                      if read x
53                      then    case "$x"
54                              in [yYjJ]*)     return 0
55                              esac
56                      else    echo >&2
57                      fi
58                      return 1
59              fi
60              return 0
61      }'
62      
63      dup 'exec 3> $tmp'
64      
65      bundle() { # file...
66      (       for i
67          do    if [ -d $i ]
68              then    ask bundle $i || continue
69                  echo "ask unbundle $i && {"
70                  echo "[ -d $i ] || mkdir $i"
71                  bundle $i/*
72              elif [ -r $i ] && [ `tr -d '[ -~]\11\12' < $i |
73                   wc -c` = 0 ]
74              then    ask bundle $i || continue
75                  echo "ask unbundle $i && {"
76                  echo "sed 's/^    //' > $i << 'End of $i'"
77                  sed 's/^/    /' $i
78                  echo "End of $i"
79                  dup "wc >&3 $i"
80                  if [ "$has_x" ]
81                  then    [ -x $i ]
82                  else    [ `expr "\`ls -l $i\`" : '...x'` = 4 ]
83                  fi && echo "chmod +x $i"
84              else    continue
85              fi
86              [ -w $i ] || echo "chmod -w $i"
87              echo '}'
88          done
89      ) }
90      bundle $argv
91      
92      dup 'exec 3>&-'
93      echo 'diff >&2 -b - $tmp <<' "'End of wc'"
94      cat $tmp
95      echo 'End of wc'
96      dup exit=0

8.  Beispiele II

Dieses Kapitel beschäftigt sich mit Skripten, die in einem größeren Zusammenhang stehen.

8.1.  Archivierung mit sccs

Archivierung von Daten und Schutz vor gleichzeitig schreibendem Zugriff.

 1      #!/bin/sh
 2      #
 3      # Eine Datei wird unter der Kontrolle von sccs verwaltet.
 4      # Die Dateien finden sich unter
 5      #       ~umail/SCCS/`cat ~umail/SCCS/sub_dir`.
 6      # ~umail/SCCS/sub_dir hat die als Inhalt eine Zeile
 7      # der Form: 3.93.
 8      #
 9      # Kommandos:
10      # u_in:  Bringt eine Datei unter Kontrolle von u_* (sccs)
11      # u_edit Man erhaelt eine Datei zum edieren
12      # u_get  Man erhaelt eine Datei zum angucken
13      # u_info Man erhaelt Information ueber eine edierte Datei
14      # u_ls   ls ueber alle mit u_* verwalteten Dateien
15      #
16      # Mon May  3 15:55:01 MET DST 1993,                     hp
17      
18              . /home/umail/bin/shell_lib.a
19              USAGE="`basename $0`: `basename $0` file"
20      
21      FILE=                           # einzupackende Datei
22      TARGET_DIR=$SCCS/`cat $SCCS_SUBDIR`
23      SAVE_DIR=$SCCS/`cat $SCCS_SUBDIR`/Save
24      
25      case "`arch`"
26              in sun4)
27              ;; *   ) fatal "rlogin to a sun4-architecture. Thanks"
28      esac 
29      
30      case "`basename $0`"
31      in "u_info")
32      ;; "u_ls")
33              
34      ;;      *)      case $#
35                              in 1)   FILE="$1"
36                              ;; *)   fatal "$USAGE"
37                      esac
38      esac
39      
40      
41      sccs_test_dir "$SCCS"                   # exist SCCS dir
42      sccs_test_file "$SCCS_SUBDIR"           # current number
43      sccs_test_dir "$TARGET_DIR"             # exist SCCS dir
44      #sccs_test_dir "$SAVE_DIR"              # exist SAVE_DIR
45      
46      case "`basename $0`"
47      in "u_in")
48              if [ !  -f $FILE ]              # back with delta
49              then
50                      fatal "$FILE didn't exist."
51              fi
52              if [ ! -r $FILE ]               # back with delta
53              then
54                      fatal "$FILE is read protected"
55              fi
56      
57              if [ -f $TARGET_DIR/s.$FILE ]   # back with delta
58              then
59                      WORK=delta
60              else
61                      WORK=create
62                      echo "$FILE is new."
63              fi
64              cp $FILE $SAVE_DIR/$FILE.`date '+%y.%m.%d.%H.%M.%S'`
65      ;; "u_get")
66                      WORK="get -k"
67      ;; "u_edit")
68                      WORK=edit
69      ;; "u_info")
70                      WORK=info
71                      FILE=""
72      ;; "u_ls")
73                      ( cd "$TARGET_DIR" && ls |
74                        sed 's/^[sp]\.//' | sort -u )
75                      exit 0
76      esac
77      
78      {
79      sccs -p "$TARGET_DIR" $WORK $FILE   << EOF
80      EOF
81      case "$WORK"                    # clean up
82              in      "create"|"delta") if [ $? -eq 0 ]
83                                      then
84                                              rm -f ,$FILE $FILE
85                                      else
86                                              echo "Something failed." 
87                                      fi
88      esac
89      } 2>&1  | fgrep -v "No id keywords"  
90      

shell_lib.a:

 1      #!/bin/sh
 2      # 
 3      # Shell_lib.a
 4      #
 5      #       Variable:
 6      #
 7              PATH=:/usr/etc:/usr/local/bin:/etc:$PATH
 8              PATH=/usr/bin:/home/umail/bin:$PATH
 9              HOME=/home/umail
10              export HOME
11              SCCS=$HOME/SCCS
12              SCCS_SUBDIR=$HOME/SCCS/sub_dir
13      
14      #
15      #       Functions:
16      #
17      fatal() # message
18      {       echo >&2 `basename $0`: "$@"
19              exit 1
20      }
21      
22      test_and_create_dir() #  dir
23      {       if /usr/bin/test ! -d $1 
24              then
25                      echo "Warning: $1 not exists."
26                      mkdir $1
27                      chmod 770 $1
28                      chmod g+s $1
29              fi
30      }
31      
32      sccs_test_dir() #  dir
33      {       if /usr/bin/test ! -d $1 
34              then
35                      echo "Warning: $1 not exists."
36                      mkdir $1
37                      chmod 770 $1
38                      chmod g+s $1
39              fi
40              if /usr/bin/test ! -w $1       # write protected?
41              then
42                      fatal "$1 is write protexted. Call for help"
43              fi
44      
45      }
46      
47      sccs_test_file() #  dir
48      {       if /usr/bin/test ! -f $1       # current version
49              then
50                      fatal "$1 didn't exist. Call for help"
51              fi
52              if /usr/bin/test ! -r $1         # read protected
53              then
54                      fatal "$1 is read protected. Call for help"
55              fi
56      }

8.2.  Archivierung mit der sh

Man kann auch mit der sh(1), archivieren. Es gibt allerdings Probleme, wenn die Dateien binäre Daten enthalten.

 1      :       Bourne Shell
 2      #       bundle -- 1.2 Jun 22 06:17:04 1987
 3      #       Copyright (c) 1987 Axel T. Schreiner
 4      
 5      #       bundle file... directory...
 6      
 7              tmp=/tmp/bundle$$
 8      
 9      case $1
10      in -r)  rflag=y; shift
11      ;; *)   rflag=n
12              echo ': to unbundle set umask and sh this file'
13              echo 'trap "rm -f /tmp/bundle$$" 1 2 3 15'
14              echo 'exec > /tmp/bundle$$'; exec 3> $tmp
15      esac
16      
17      for i 
18      do      echo "echo >&2 $i"; echo >&2 $i
19              if [ -d $i ]
20              then    echo "[ -d $i ] || mkdir $i"
21                      bundle -r $i/*
22              else    echo "sed 's/^  //' > $i << 'End of $i'"
23                      sed 's/^/       /' $i
24                      echo "End of $i"
25                      echo "wc $i"; wc $i >&3
26                      [ -x $i ] && echo "chmod +x $i"
27              fi
28              [ -w $i ] || echo "chmod -w $i"
29      done
30      
31      case $rflag
32      in n)   echo "diff -b - /tmp/bundle\$\$ << 'End of wc' >&2"
33              exec 3>&-
34              cat $tmp; rm $tmp
35              echo 'End of wc'
36              echo 'rm -f /tmp/bundle$$'
37              echo 'exit 0'
38      esac

8.3.  Exklusiven Zugriff zum Editieren

Nur einer darf eine Datei edieren. (vim ist Voraussetzung).

 1      #!/bin/sh
 2      
 3      USAGE="vim: vim file"
 4      
 5      case $#
 6              in 1)   if [ -f "$1".swp ]
 7                      then
 8                              echo > /dev/tty "$1 is under construction."
 9                      else
10                              vim "$1"
11                      fi
12              ;; *)   echo > /dev/tty $USAGE
13                              exit 1
14      esac
15      exit 0

8.4.  ftp dann und das

ftp zu einer bestimmten Zeit.

 1      #!/bin/sh
 2      #       (c)                     afb     SAI Uni Ulm
 3      #       adapted                 gsk     FB6 Uni Osnabrueck
 4      #       adapted                 hpb     FB6 Uni Osnabrueck
 5      #       
 6      # Zweck: Aufruf von ftp, wobei die Kommandos, die ftp remote
 7      #        ausfuehren soll, auf der Kommandozeile mitgegeben
 8      #        werden koennen bzw. von stdin kommen.
 9      #       
10      # Optionen:-s seconds:  Bevor der ftp aufruf erfolgt,
11      #                       seconds Sekunden warten.
12      #       -f path         path wird von ls und get verwendet
13      #       -ls:            ls-Listing von path
14      #       -get:           get von path
15      #       -cat:           die Kommandos werden von stdin erwartet.
16      #       host:           ftp-Host
17      
18      PATH="/usr/local/gnu/bin:/usr/ucb:/usr/bin:/bin:/etc:/usr/etc"; export PATH
19      MYHOST=`hostname`
20      :       ${MYHOST:=jupiter}
21      echo $MYHOST | grep '\.' > /dev/null  || \
22                      MYHOST=`arp $MYHOST | sed 's/.*(\(.*\)).*/\1/'`
23      # originally "nsquery" was used to
24      # determine the full name of the
25      # requesting host, but here (OS)
26      # "nsquery" is not installed.
27      
28      
29      :       ${ANONYM:=anonymous}
30      :       ${PASSWD:=${LOGNAME:-`whoami`}@}        #${MYHOST}}
31      
32      Sleeping=0                      # sleep 0 seconds before ftp ...
33      Cmd=ls                  # default cmd
34      Cmd_line=ls             # default cmd_line
35      Ftphost=dione           # default ftp-host
36      Path=/                  # default path
37      File=                   # default filename
38      Flag_counter=0          # > 1 --> ls, get, cat exclusive !!
39      
40      #
41      # local funtions
42      #
43      fatal()
44      {
45              echo "fatal: `basename $0` [-s sec] [-ls|-get] [-f path] [-cat] host"
46              if [ $# -gt 0 ]
47              then
48                      echo "fatal:  $1"
49              fi
50              echo "        defaults: cmd     = ls"
51              echo "        defaults: seconds = 0"
52              echo "        defaults: host    = $Ftphost"
53              exit 1
54      }
55      
56      
57      # some abbreviations (for nameserver
58      # sometimes does not work correctly)
59      while [ $# -gt 0 ]
60      do
61      
62              case "$1"
63              in "")  
64              ;; -v*)         fatal           # --> usage & exit
65                              
66              ;; -ls)         Cmd=ls;         # listing only
67                              Flag_counter=`expr $Flag_counter \+ 1`
68                      
69              ;; -get)        Cmd=get;        # get file --> mode == I
70                              Flag_counter=`expr $Flag_counter \+ 1`
71                      
72              ;; -cat)        Cmd=cat;        # cat -- read from stdin
73                              Flag_counter=`expr $Flag_counter \+ 1`
74                      
75              ;; -f)          shift;
76                              Path=`dirname $1` # split file &
77                              File=`basename $1` # path
78      
79              ;; -s)          shift; Sleeping=$1
80                              if [ $Sleeping -lt 0 ]
81                              then
82                                      fatal "seconds > 0 !!!"
83                              fi
84                      
85              ;; -*)          fatal "unknown option: $1"
86              ;; *)           if [ $# -eq 1 ]
87                              then
88                                      Ftphost="$1"
89                              else
90                                      fatal "only one host."
91                              fi
92              esac
93      
94              shift
95      done
96      
97                      # only one cmd will be accepted
98      if [ $Flag_counter -gt 1 ]
99      then
10              fatal "ls, get and cat, ooohhhhhhh!!!"
10      fi
10      
10      case $Cmd
10              in cat) Cmd_line1="cat"
10                      Cmd_line2=
10              ;; *)   Cmd_line1="echo cd $Path"
10                      Cmd_line2="echo $Cmd $File"
10      esac
10      
11      
11      which ping
11      ping $Ftphost 1 1       > /dev/null
11      
11      case $?
11              in 0)   break;          # host is reachable
11              ;; *)   fatal "host is not reachable"
11      esac
11              
11      sleep $Sleeping
12      
12      {       echo open $Ftphost
12              echo user $ANONYM "\"$PASSWD\""
12              echo binary                             # ok?
12              eval $Cmd_line1
12              eval $Cmd_line2
12              echo bye
12      } | ftp -niv
12      
12      exit $?

8.5.  Buchstabierkontrolle.

 1      #!/bin/sh
 2      # (c) hpb, Sat Aug 31 12:33:42 MET DST 1991
 3      #
 4      
 5      usage()
 6      {
 7      echo "`basename $0`: `basename $0` [-D dict] [-d delete] [-a add_on] file"
 8      echo "---------------------------------------------------------"
 9      echo " Vergleicht alle Woerter der Eingabedatei (ASCII oder troff-Text)"
10      echo " mit einem Woerterbuch."
11      echo " Alle Woerter, die im Woerterbuch nicht"
12      echo " enthalten sind, werden auf stdout ausgegeben."
13      echo ""
14      echo " Einbuchstabige, zweibuchstabige, dreibuchstabigen Worte"
15      echo " und Worte die mit einem Punkt beginnen"
16      echo " werden entfernt."
17      echo " Alle Worte die mit einem Punkt beginnen werden entfernt."
18      echo ""
19      echo " Existiert eine Datei 'add_on' wird diese zum"
20      echo " Woerterbuch hinzugenommen."
21      echo ""
22      echo " Alle Worte die in der Datei 'delete' enthalten sind,"
23      echo " werden nicht ausgegeben."
24      echo "---------------------------------------------------------"
25      }
26      
27      TMP=/tmp/`basename $0`.$$
28      TMP_WDB=/tmp/wdb.$$
29      WDB=$HOME/bin/dict
30      ADD_ON=add_on
31      DELETE=delete
32      INPUT_FILES=" "
33      PROG=`basename $0`
34      
35      while :
36      do      case $1
37              in -D)          shift;          WDB=$1
38              ;; -d)          shift;          DELETE=$1
39              ;; -a)          shift;          ADD_ON=$1
40              ;; -*)          usage;          exit 1
41              ;; *)           break
42              esac
43              shift
44      done
45      
46      if [ ! -f $WDB ]
47      then
48              echo "$PROG: Woerterbuch ($WDB) existert nicht"
49              exit 1
50      fi
51      
52      if [ ! -f $ADD_ON ]
53      then
54              echo "$PROG: Warning: Lokales Woerterbuch ($ADD_ON)  existert nicht" >&2
55              ADD_ON=
56      fi
57      
58      if [ ! -f $DELETE ]
59      then
60              echo "$PROG: Warning: spezielles  Woerterbuch ($DELETE)  existert nicht" >&2
61              DELETE=
62      fi
63      
64      
65      case $#                 # Mindestens eine Datei muss vorhanden sein
66      in   0) usage;
67              exit 2
68      esac
69      
70      while :                 # alle Eingabedateien müssen existieren.
71      do
72              case $1
73              in   '') break
74              esac
75                      
76              if [ ! -f "$1" ]
77              then
78                      echo "$PROG: $1 existiert nicht!"
79                      echo "$PROG: bye bye."
80                      exit 1
81              else
82                      INPUT_FILES="$INPUT_FILES $1"
83                      shift
84              fi
85      done
86      
87      cat $INPUT_FILES |  sed  's/\\f(CW//g
88                       s/\\f[123456789P]//g
89                       s/\\(hy//g
90                       s/\\(<<//g
91                       s/\\(>>//g
92                       s/\\\*\[//g
93                       s/\\\*$//g
94                       s/\\s[+-][0-9]//g
95                       s/\\s0//g'                                     \
96              |  /usr/bin/tr  '[()[]{},;:~"!@$%^&<>/ ] ' '\012'       \
97              |  grep -v '^\.'        |  grep -v '^.$'                \
98              |  /usr/bin/tr  '\.' '\012'      |  grep -v '^$'        \
99              |  grep -v '^..$'       |  grep -v '^...$'              \
10              |  grep -v '^.$'                                        \
10              |  sort -u                                         > $TMP
10      
10      cat $DELETE $ADD_ON $WDB | sort -u > $TMP_WDB
10      
10      comm $TMP $TMP_WDB  | grep -v ' '
10      
10      rm -f $TMP $TMP_WDB
10      exit 0

8.6.  Protokollieren.

 1      #!/bin/sh
 2      #
 3      #       Mit diesem Skript soll protokolliert werden koennen,
 4      #       aus welchem Grund ein Rechner stehen blieb.
 5      #
 6      #       Aufruf ---> vi
 7      #                       editier den Inhalt
 8      #                       :wq
 9      #                   die Datei wird via mail verteilt.
10      #
11      #                       hp,             Mon Feb 14 08:54:43 MET 1994
12      #       ADMIN=problems ---> ADMIN=sys   Thu Jul 21 16:06:48 MET DST 1994 - hp
13      
14      
15              USAGE="`basename $0` hostname"
16              :       ${SHLIB:=/usr/local/lib/shf}
17              . ${SHLIB}/setPATH
18              TMP_FILE=/tmp/`basename $0`.$$
19              REASON=
20              HOST=
21              ADMIN=bischof
22      
23      #
24      #       Functions:
25      #
26      fatal() # message
27      {       echo >&2 `basename $0`: "$@"
28              exit 1
29      }
30      
31      create_file() #  file
32      {       touch "$1" 2> /dev/null
33              [ $? -eq 0 ] || fatal "Can't create $1 for writing."
34      }
35      
36      trap 'trap ""  0 1 2 3 13 14 15;
37            rm -f "$TMP_FILE";
38            exit' 0 1 2 3 13 14 15
39      
40      
41      case $#
42              in 1)   REASON="Absturz von $1"
43                      HOST="$1"
44                      break
45              ;; *)   fatal $USAGE            
46      esac
47      
48      case `whoami`
49              in "root")      fatal "Don't use the root login for this work."
50      esac
51      
52      create_file $TMP_FILE
53      
54      cat << EOF > $TMP_FILE
55      Protokoll ueber einen Rechnerabsturz
56      =====================================
57      
58      Rechner         : $HOST
59      Date            : `date`
60      Knowledge Person: `who am i`
61      
62      =====================================
63      
64      Grund            : XX
65      /usr/adm/messages:
66      Vorgehensweise   :
67      
68      
69      EOF
70      
71      vi  +/XX $TMP_FILE
72      
73      /usr/ucb/mail -s "$REASON" $ADMIN   <  $TMP_FILE
74      

8.7.  Poor man's backup

Können Sie den Zeitpunkt schon erkennen, an dem Ihre über's Netz verstreuten Dateisysteme mit dem bisherigen Konzept nicht mehr zu sichern sind? Uns ging es ebenso. Weil kommerzielle Systeme zu teuer waren, entwickelte ich mit existenten Werkzeugen, wie awk, rsh, rdump und der Bourne-Shell, einen simplen Mechanismus, mit dem wir unseren Backup realisieren.

Ausgangssituation

In unserem heterogenen UNIX-Netz, bestehend aus NeXTen, Sun3s, Sun4s und einer Celerity, sind ca. 70 Dateisysteme mit etwa 7 GB an Datenvolumen zu sichern. Davon sind ca. 4 GB Benutzerdaten, die täglich, und ca. 3 GB Systemdaten, die einmal wöchentlich gesichert werden sollten. Als Backup-Medium steht ein Exabyte mit 5 GB fassenden Bändern zur Verfügung.

Da sich in unserem Netz die Struktur der Dateisysteme semesterweise ändert, muß der Mechanismus leicht an jede wahrscheinliche Konfiguration anpaßbar sein. In Zukunft wird sich in unseren Rechner-Zoo noch eine weitere Gattung einschleichen, was bedeutet, daß der Mechanismus keine speziellen Eigenschaften des bestehenden Netzes nutzen kann. Damit der Betrieb tagsüber ungestört ist, muß der Backup nachts und demzufolge ohne Bedienung als Batch-Job ablaufen.

Weil wir Rechnerbetrieb als Hobby betreiben, stand für die Entwicklung sehr wenig Zeit zur Verfügung. Um den Entwicklungsaufwand in Grenzen zu halten, wurden wann immer möglich, existente Programme eingesetzt. Die reine Entwicklungszeit betrug ca. 8 Stunden.

Datenbasis

Das Backup-Skript wird vom Benutzer backup auf dem Rechner, an dem das Exabyte physikalisch angeschlossen ist, gestartet. Durch den Inhalt der Datei dump_table wird gesteuert, welche Dateisysteme von welchen Rechnern an welchem Tag gesichert werden sollen, siehe Abbildung 1.

$ head dump_table
[1]     thor:/dev/root
[1]     thor:/dev/usr 
[02-6]  thor:/dev/export
[02-6]  thor:/dev/home 
[1]     thor:/dev/l_export 
[1]     iduna:/dev/usr
[02-6]  iduna:/dev/export
[1]     frigga:/dev/root 
[1]     frigga:/dev/usr /usr
[02-6]  frigga:/dev/export
[1]     garm:/dev/root 


Die erste Spalte von dump_table gibt an, an welchen Tagen das in der zweiten Spalte stehende physikalische Dateisystem gesichert werden soll. Als Indikatoren für den Wochentag wurden Ziffern gewählt, um die Datei übersichtlich zu halten und Bereichsangaben leichter mit awk bearbeiten zu können. Für date +%w beginnt die Woche mit dem Tag 0 am Sonntag.

Diese Datei muß unter allen Umständen mit dem Zustand des Netzes übereinstimmen, was die Verbindung zwischen Montagepunkt des Dateisystems und dem entsprechenden Rechner betrifft. Die Daten, die dieser Datei zugrunde liegen, sind auf dem Netz, wenn auch verstreut, vorhanden. c_dump_t sammelt die einzelnen /etc/fstabs der Rechner unseres Netzes auf und generiert dump_table, siehe Abbildung 2.

#!/bin/sh
#       c_dump_t - generiert eine Rohversion der Tabelle,
#       nach der backup sichert.
#
. shell_lib.a                            # Globale Variablen & fatal & test_...
usage="usage: `basename $0`"               # Aufruf
DUMP_TABLE=${DUMP_TABLE}.new               # pro Zeile ein Eintrag

case "$#"
       in 0)              break;
       ;; *)              fatal "$usage"
esac

       # Falls $0 zweimal zur gleichen Zeit laeuft, gibt's Aerger.
/etc/link "$HOME/bin/`basename $0`" "$LOCK"    ||
fatal "`basename $0` in use; try again"

trap 'rm -f "$LOCK"; exit' 0 1 2 3 13 14 15       # Aufraeumaktionen

test_and_create_dir  $FSTABS             # Falls nicht vorhanden generieren
hosts=`gethostname "inf"`                # alle Rechner der Netzgruppe 'inf'

for i in $hosts                          # alle fstabs aufsammeln
do
       rcp  $i:/etc/fstab $FSTABS/$i     # wegen grep
       case $?
              in 1)       fatal "$0: \"rcp $i:/etc/fstab $FSTABS/$i\" failed"
              ;; 0)       chmod 666 $FSTABS/$i       # wegen Ueberschreiben
       esac
done

cd  $FSTABS
                         # [\tBlank]
grep "^\/dev\/" * | sed 's/[        ].*//' >> $TOP/$DUMP_TABLE
exit 0


Dadurch ist eine Inkonsistenz zwischen dem Zustand des Netzes und dump_table nahezu ausgeschlossen.

Um Funktionen und Werte, die in mehreren Skripten verwendet werden nur an einer Stelle pflegen zu müssen, werden diese in eine Shell-Bibliothek ausgelagert. Diese Bibliothek wird in den entsprechenden Skripten durch das .-Kommando ausgewertet, siehe Abbildung 3.

#!/bin/sh
# 
# shell_lib.a
#
TOP=/home/backup                        # Katalog
FSTABS=$TOP/Fstabs                      # Die Fstabs aller Rechner
LOCK=$TOP/.lock                         # entweder backup oder c_dump_t
DUMP_TABLE=$TOP/dump_table              # pro Zeile ein Eintrag
TAPE=/dev/nrst1                         # das Exabyte
HEADER=header
#
fatal() # message
{      echo >&2 `basename $0`: "$@"
       exit 1
}

test_and_create_dir() #  dir
{      if [ ! -d $1 ]
       then
              echo "Warning: $1 does not exist."
              mkdir $1
       fi
}


dump_table ist die einzige Datei, die vom Systemadministrator gepflegt werden muß. Die Arbeit beschränkt sich allerdings auf die Festlegung, an welchem Tag ein Dateisystem gesichert werden soll. Die einfache Struktur dieser Datei verringert die Gefahr eines fehlerhaften Eintrags.

backup

Das backup-Skript läuft auf dem Exabyte-Server mit den Berechtigungen eines normalen Nutzers. Würde man das Skript als root starten, müßte der Exabyte-Server für jeden Rechner, dessen lokale Platte gesichert werden soll, als vertrauenswürdig erscheinen. Dies erschien mir als unnötig und zu risikoreich.

Der Algorithmus des Skripts ist trivial: Für jede Zeile von dump_table wird auf dem entsprechenden Rechner ein rdump-Kommando der Form
/etc/rdump ${LEVEL}ufs $EXA_SERVER:/dev/nrst1 mit root-Berechtigung abgesetzt. Ist dump_table abgearbeitet, wird verifiziert, ob der Backup für alle Dateisysteme erfolgreich war, falls nicht, wird der Systemadministrator via mail informiert.

execS

Wie sorgt man nun dafür, daß rdump auf den unterschiedlichen Architekturen unter root-Berechtigungen abläuft? Mit dem dazwischen geschaltenen Shell-Skript execS, siehe Abbildung 4.

#!/bin/sh
#
# execS fuehrt $@ unter euid = uid = 0
# auf Sun[34], NeXT oder Celerity aus.
#
# Beware of:
#       -rwsr-xr-x  1 root       106496 Jan 18 11:57 *equal
#       ....

        PATH=:/usr/lbin:/usr/bin:$HOME/bin:/home/backup/bin:$PATH

case `arch`
        in "sun4")     echo " $@" | $HOME/bin/Sun4equal;     break
        ;; "sun3")     echo " $@" | $HOME/bin/Sun3equal;     break
        ;; "NeXT")     echo " $@" | $HOME/bin/Nextequal;     break
        ;; "celerity") echo " $@" | $HOME/bin/Celerityequal; break
        ;; *)          exit 1       # Generiere *equal aus .../Src/equal.c
esac


Abhängig von der Architektur aktiviert execS ein kleines C-Programm *equal, das die effektive und wirkliche Benutzernummer auf 0 setzt, mit execl, die Shell aufruft, die dann von Standardeingabe das entsprechende Kommando liest und abarbeitet, siehe Abbildung 5.

/*
 * setzt die euid = uid = 0.
 */
#include <stdio.h>
#include <sys/types.h>
#include <pwd.h>
#include <strings.h>

#define BACKUP "backup"

main()
{
uid_t uid;
uid_t euid;
struct passwd * pp;

        if ( ! (pp = getpwuid(getuid()) ) )     {
                fprintf(stderr," getpwuid failed\n");
                exit(1);
        }
        if ( strcmp(pp->pw_name, BACKUP ) )     
                exit(1);
        if ( setreuid(0,0) == -1 )      {
                fprintf(stderr," setregid failed\n");
                exit(1);
        }
        execl("/bin/sh", "/bin/sh", (char *)0);
}


Um das Sicherheitsloch in Grenzen zu halten, überprüft *equal wer der Aufrufende ist. Bei jedem anderen als backup terminiert das Kommando vor Ausführung des kritischen Aufrufs.

Es reicht nicht, nur die effektive uid auf 0 zu setzen, da Suns rdump überprüft, ob uid = euid ist. Deshalb genügt es nicht, eine Kopie von /bin/sh mit den entsprechenden Rechten auszustatten.

Für die kurze Zeit des Kommunikationsaufbaus wird auf dem Exabyte-Server arrangiert, daß der Rechner dessen Dateisystem als nächstes gesichert werden soll, als vertrauenswürdig gilt.

Der wesentliche Teil von backup ist in Abbildung 6 abgebildet.

ex_dump_l  $DUMP_TABLE           |  \
sed 's/[        ].*//'           |  \
sed 's/:/ /'                     |  \
while read Host Rdev
do
       rsh -n $Host $HOME/bin/execS $DumpCmd ${LEVEL}ufs    \
                                    $Dumphost:$TAPE $LENGTH $Rdev
done 


ex_dump_l extrahiert die an diesem Tag zu sichernden Zeilen.

Wir sichern prinzipiell alle Daten der Dateisysteme. Inkrementelle Sicherungen bringen uns keinen Vorteil, da auf alle Fälle ein Band benötigt wird, das bisher genügend Platz für einen kompletten Backup bieten. Da die Sicherung von 4 GB Daten ungefähr 6 Stunden dauert, reicht die Nacht zum Sichern der Daten aus. Die Restaurierung verlorengegangener Daten ist für uns aus vollständigen Sicherungen einfacher, als wenn wir inkrementell sichern würden. Das Skript wird an allen Arbeitstagen von Hand gestartet. In Abbildung 7 ist ein typischer Ablauf abgebildet.

loki backup 72 backup
Tape number = 20
Sleep for ?? seconds: 15000
I hope 15000 is ok.
loki backup 72 


restore

Von jedem beschriebenen Band existiert eine Datei, in welcher der Verlauf des Backups protokolliert ist. Auftretende Fehler können so analysiert und falls möglich behoben werden.

In einer zweiten Datei stehen mt-Anweisungen, mit denen man bis zu der Stelle, an der das gesicherte Dateisystem zu finden ist, vorspulen kann, siehe Abbildung 8.

On Volume: 20
========================
mt -f /dev/nrst1 fsf 0
DUMP echo thor:/dev/export -------------   backup on sun4 was ok

mt -f /dev/nrst1 fsf 1
DUMP echo thor:/dev/home -------------   backup on sun4 was ok

mt -f /dev/nrst1 fsf 2
DUMP echo dosuni:/dev/export -------------   backup on sun4 was ok

...


Diese Anweisungen erleichtern ein Restaurieren erheblich, denn auf einem Band können bis zu 40 Dateisysteme gesichert sein. Die eigentliche Restaurierung muß via restore in die Wege geleitet werden. Da dies bei uns selten vorkommt, erschien mir eine weitere Unterstützung unnötig.

Zusammenfassung

In aller Regel funktioniert unser Backup problemlos. Ab und zu blockiert unser Exabyte den SCSI-Bus des Exabyte-Servers, was dann dazu führt, daß manuell eingegriffen werden muß. Dies ist allerdings ein Problem das weniger auf den Backup-Mechanismus, als vielmehr auf das Exabyte zurückzuführen ist.

Was uns noch fehlt, ist ein Programm, das die Bandverwaltung übernimmt, damit Verwechslungen von Bändern ausgeschlossen werden können.

Der Mechanismus hat sich bei uns bewährt. Der Aufwand an menschlicher Arbeitskraft, die bisher nötig war, um den Backup zu ziehen, wurde wesentlich reduziert. Die Entwicklungszeit hat sich um ein Vielfaches ausbezahlt. Die fehlende Graphikoberflächen, die man sich denken kann, wurde bisher noch nicht vermißt.

8.8.  Sicherheit versus Nutzbarkeit

Hat man eine Anzahl von Unix-Rechnern vernetzt, dann kann man die vorhandenen Ressourcen optimal ausnutzen, falls man auf jedem beliebigen Rechner ein Kommando mittels rsh ohne Eintippen des Paßworts aktivieren kann. Ist Ihr System so konfiguriert? Falls ja, haben Sie jedem Einbrecher, der sich für Ihr Netz interessiert, den goldenen Schlüssel zum Öffnen in die Hand gegeben. Warum dies so ist und was man dagegegen unternehmen kann, wird in diesem Artikel dargelegt.

Das Vertrauen

Ist man auf einem Rechner, nennen wir ihn mozart, angemeldet, und möchte auf thor ein Kommando wie z.B. date absetzen, wird in punkto Sicherheit der in Abbildung 1 dargestellte Algorihmus angewendet.

[picture]

Abbildung 1: Kontaktaufnahme

Der fremde Rechner überprüft, ob das Tupel (loginname, Rechner) berechtigt ist, Kommandos auf ihm zu aktivieren. Er beantwortet die Frage, indem er eine lokale Datenbasis zu Rate zieht. Falls ja, wird das Kommando ausgeführt; falls nein, wird die Ausführung des Kommandos verweigert.

Wann ist ein fremder Rechner in Verbindung mit einer loginname vertrauenswürdig? Das Vertrauensverhältnis kann auf zwei verschiedenen Wegen definiert werden.

Das Problem

Nehmen wir an, im Netz gibt es wenigstens die beiden Rechner balrog und gandalf und alle Nutzer gelten auf allen Rechner als vertrauenswürdig. Ein Bösewicht, der Zugang zum Netz hat, bringt seinen Laptop mit, steckt diesen auf das Netz und gibt ihm die gleiche IP-Adresse wie balrog. Dann meldet er sich als superuser oder ein anderer interesanter Nutzer auf balrog an. Da dies sein Rechner ist, dürfte ihm das entsprechende Paßwort wohl bekannt sein. Mit rsh gandalf erlangt er superuser-Rechte auf einem Rechner des Netzes, ohne daß er das entsprechende Paßwort kennt. Vorbei ist es mit der Sicherheit der Daten.

Weil dieser Aspekt nicht sicher zu bekommen ist, werden von fast allen Administratoren die systemweiten rhost-Dateien gelöscht. Die Arbeit auf dem Netz wird unbequemer, vieles dauert länger und wird demzufolge teurer.

Die Lösung

Angenommen der Heimatkatalog eines Nutzers und seine user id auf allen Rechnern des Netzes sind gleich, und $HOME/.rhosts ist leer. Betrachten wir nochmals Abbildung 1: Nur zum Zeitpunkt der Kontaktaufnahme der beteiligten Rechner wird das Vertrauensverhältnis geprüft -- also legen wir die Datei $HOME/.rhosts kurz vor der Kontaktaufnahme an und löschen sie zum fühest möglichen Zeitpunkt wieder. Siehe Abbildung 2.

#!/bin/sh

RHOSTS=$HOME/.rhosts

if [ --f $RHOSTS ]
then
        echo "$RHOSTS exists."
fi

echo `hostname` > $RHOSTS
rsh $1 "rm --f $RHOSTS 2> /dev/null; $2"
exit 0

Abbildung 2: Vetrauen ist gut, kein Vertrauen ist besser.

Nur für die Zeitdauer zwischen dem Füllen von $HOME/.rhosts und dem Löschen ist die Benutzerkennung unsicher. Es ist klar, daß das Skript noch zusätzliche Arbeit in puncto Environment und Pfaden leisten muß.

8.9.  Ist Ihr Paßwort sicher?

Unberechtigterweise in ein gut gepflegtes Unix System einzudringen ist, ohne Kenntnis eines entsprechenden fremden Paßworts, sehr schwierig. Aus diesem Grunde konzentrieren sich die meisten Attacken der †bösen Buben† darauf, fremde Paßwörter in ihren Besitz zu bringen.

Die Gegenspieler der †bösen Buben† sind einerseits der Systemadministrator, der durch komplexe Maßnahmen versucht, die Entfaltungsmöglichkeit dieser †bösen Buben† zu erschweren, und andererseits der Nutzer, indem er †vernünftige† Paßwörter wählt und diese in regelmäßigen Abständen ändert.

Ausgangssituation

Angriffe von außen, d.h. der Versuch in ein fremdes Netz einzudringen, setzen sehr großes Wissen voraus und sind aufgrund von Sicherheitsmaßnahmen der Administration in aller Regel zum Scheitern verurteilt.

Ist man allerdings in einem Netz und versucht ein fremdes Paßwort in Erfahrung zu bringen, sind die Karten gar nicht so schlecht. Für den Artikel gehen wir vom zweiten Fall aus.

Naiv

Prinzipiell bieten sich folgende Wege an: Der erste, etwas langwierige Weg besteht darin, für einen bestimmten Nutzer alle Paßwörter auszuprobieren, die er haben könnte, indem man sie eintippt. Dies sieht dann in etwa so aus:

mozart% login
login: bischof
Password:
Login incorrect
login: bischof
Password:
Login incorrect
login: 
...

Berechnen wir die Anzahl der Möglichkeiten, die auszuprobieren sind. Wenn wir annehmen, daß ein Paßwort zwischen vier und acht Zeichen lang ist und aus 95 verschiedenen Zeichen (Leerzeichen bis †~† bestehen kann, errechnet sich die Anzahl der verschiedenen Möglichkeiten durch

[equation]

Nehmen wir an, daß man zum Eintippen eines Paßworts im Durchschnitt 5 Sekunden benötigt, so hat man nach ca. 1077800436 Jahren alle Paßwörter ausprobiert. Betrachtet man im Vergleich dazu das Alter der Erde, welches ca. 5 Milliarden Jahre beträgt, dann sieht man deutlich, daß dieser Weg dem †bösen Buben† etwas Geduld abverlangt.

Besser

Der zweite Weg besteht darin, den Rechner die Paßwörter generieren und durch einen login-Versuch testen zu lassen. Das Erfinden geht sicher unheimlich schnell, aber da login bei einem falschen Paßwort kurz einnickt, bevor er einen erneuten login-Versuch akzeptiert, ist dieser Weg bestimmt bequemer als der erste Vorschlag, aber nicht wesentlich schneller als derselbe.

Mittelprächtig

Folgende Überlegung könnte die Sache etwas beschleunigen: Wie funktioniert eigentlich login? login liest das eingetippte Paßwort ein, verschlüsselt es und vergleicht das verschlüsselte Paßwort dann mit dem Paßworteintrag des Nutzers. Sind die beiden ungleich, wird der Anmeldeversuch abgewehrt, ansonsten findet die Anmeldung statt.

Soweit die Überlegungen, nun gehts in die Praxis. Die Verschlüsselung erfolgt mittels char *crypt(key, salt). crypt(3) nimmt ein Wort (key) und einen Initialwert (salt) und liefert das verschlüsselte Wort zurück. Ist das verschlüsselte Wort und der Paßworteintrag gleich, hat man das Paßwort gefunden. Das einzige Problem besteht nur in der richtigen Wahl von salt. salt besteht aus den ersten zwei Buchstaben des Paßworts. Abbildung 1 zeigt ein Programm,

/*
 * Ein Wort in Klartext wird mit crypt verschluesselt,
 * und das Resultat mit dem Passworteintrag verglichen.
 *
 * Passworteintrag: bischof:ei1/r7g1wRyAI:106:11:Hans\(emPeter ...
 * Passwort:        dmva1m10
 *                      
 */

#include <stdio.h>


void main(void)     {
char * pwd       = "dmva1m10";
char * pwd_entry = "ei1/r7g1wRyAI";
char   salt[]    = { pwd_entry[0], pwd_entry[1], '\0' };

        printf("crypt:     %s\n", crypt(pwd, salt) );
        printf("pwd_entry: %s\n", pwd_entry );
        exit(0);
}


Abbildung 1: Gewußt wie

Ein Ablauf des Programms liefert:

mozart Src 178 crypt_1
crypt:     ei1/r7g1wRyAI
pwd_entry: ei1/r7g1wRyAI

Somit haben wir alle Ingredienzen, um den zweiten Weg entscheidend zu verkürzen. Ein Verschlüsselungs- und Testvorgang benötigt auf einer Sparc 10 ca. 0.012 Sekunden. Nach ca. 12933605 Jahren hat unser Programm alle Möglichkeiten ausprobiert und ein Paßwort entschlüsselt. Auch Transputersysteme oder andere schnellere Rechner können das Problem wohl nicht in einer adäquaten Zeit lösen.

Gut

Ein Punkt wurde bisher nicht berücksichtigt: die Psychologie. Die meisten Nutzer nehmen als Paßwort nicht eine beliebige Buchstabenkombination, sondern ein Wort, das sie sich leicht merken können, z.B. eine Autonummer (OSJ392), den Namen einer Person aus einem Buch (gandalf), den Namen ihres Lieblingsinstruments (trompete), den Anfangsbuchstaben der Worte eines berühmten Spruchs (aus Goethes Faust, Hexenszene: Du mußt verstehen, aus eins mach zehn ... dmvaemz) usw. Deshalb ist es doch bestimmt sehr erfolgversprechend, das Programm aus Abbildung 1 mit Wörtern zu füttern, die aus einer Datenbank generiert werden, welche eine große Menge an Namen, Werken usw. enthält. Die Erstellung einer derartigen Datenbank ist in der heutigen Zeit dank world wide web und Co. kein Problem mehr.

Die meisten Nutzer machen es uns nicht ganz so leicht -- sie garnieren ihr Paßwort noch zusätzlich mit Sonderzeichen, manche schreiben Buchstaben groß oder verwenden an Stelle von Zahlwörtern bzw. Zahlwörtern sehr ähnlich klingenden Wörtern Ziffern (toofastforyou2fast4you). Also werden wir unser Programm derart modifizieren, daß auf die potentiellen Paßwörter vor dem Verschlüsselungsvorgang Regeln angewendet werden, die das Wort etwas verändern.

Die Anzahl der potentiellen Paßwörter läßt sich somit auf ein akzteptables Maß reduzieren. Wenn man mit dieser Technik auch nicht alle Paßwörter entschlüsseln kann, so bleibt doch die Hoffnung, daß man vielleicht 10% der Paßwörter decodieren kann.

Ich habe auf sechs verschiedenen Netzen versucht, Paßwörter in Erfahrung zu bringen. Von ca. 1641 Paßwörtern konnte ich mit dem skizzierten Algorithmus 223 Paßwörter innerhalb eines Nachmittags auf einer Sparc 10 entschlüsseln. Verwendet habe ich hierzu ein frei verfügbares Programm namens Crack. Das Programm realisiert den oben beschriebenen Algorithmus. Es ist also nicht einmal nötig selber zu denken oder zu entwickeln.

Fazit

Es stellt sich die Frage, warum ging dies so leicht?

Es bleibt noch anzumerken, daß ich natürlich kein Paßwort dazu verwendet habe, um widerrechtlich in ein fremdes System einzudringen. In Anbetracht der Problematik, die dieses Thema birgt, erschien mir es wichtig, darauf hinzuweisen und die Problematik durch Fakten zu untermauern.

8.10.  Von troff ins Web

Einleitung

Es gibt Werkzeuge mit denen troff -Dokumente für's Web aufbereitet werden können. Natürlich sollen die generierten Web-Seiten untereinander und mit der Welt verbunden sein, sowie Graphik, Tabellen und Mathematiksatz etc. enthalten können. Der zusätzliche Aufwand sollte minimal sein: zwei zusätzliche Makros sollten genügen. Wie dies geht, erfahren Sie in diesem Artikel.

Einleitung

Einige wenige Unverbesserliche, zu denen auch ich und einige meiner besten Freunde gehören, setzen ihre Texte immer noch mit den Mitgliedern der troff-Familie. Auch wir möchten unsere Texte, zumindest was die Optik anbelangt, in guter -- um nicht zu sagen in bester -- Qualität im Web publizieren. Die Anforderungen an die zu verwendenden Techniken sind hoch gelegt: Der Ausdruck und die Web-Seite sollen aus denselben Quellen erzeugt werden; die Web-Seiten sollen Querverweise und Graphik enthalten; jede Seite soll mit dem gleichen Rahmen versehen sein, damit die Navigation vereinheitlicht wird; eine Aufsplittung des Dokuments in kleine Einzelseiten soll an inhaltlich definierten Punkten automatisch erfolgen; ein Inhaltsverzeichnis und ähnlich nützliche Dinge sollten automatisch erzeugt werden; und ehrlich gesagt, möchte ich mich nicht so sehr mit HTML [1] beschäftigen.

Resultat

Bevor ich zum ``wie'' komme, möchte ich Ihnen das Resultat zeigen. Eine Publikation im Web stelle ich mir wie folgt vor:

--   das Layout muß, soweit als möglich, einheitlich sein
--   die einzelnen Seiten sollten relativ kurz
     sein und das Dokument sollte
     nach inhaltlichen Gesichtspunkten
     aufgeteilt sein. So sollte sich z.B. jedes Kapitel
     und Unterkapitel auf einer eigenen Seite befinden.

Wird der vorliegende Artikel anstatt nach PostScript so nach HTML übersetzt, wie der Artikel beschreibt, erhält man als erste Web-Seite die in Abbildung 1 dargestellte Seite. Ein Klick auf bischof aktiviert ein mail-Tool; die Web-Seite unseres Fachbereichs, bzw. unserer Universität, erhält man durch ein Klicken auf die entsprechende Zeile.

Mit den vier Pfeilen links oben bzw. unten kann man sich durchs Dokument navigieren. Das I-Symbol, rechts von den Navigations-Pfeilen, führt zum Inhaltsverzeichnis. Mit einem Klick auf das Brief-Icon läßt man dem Autor eine Nachricht zukommen. Klickt man auf den Pfeil, welcher nach rechts zeigt, kommt man zur inhaltlich nächsten Seite. In unserem Fall ist das Abbildung 2. Das Inhaltsverzeichnis ist in Abbildung 3 dargestellt; es keine inhaltlich nächste oder vorherige Seite. Deshalb ist kein Navigations-Pfeil aktiv.

Der troff-Input für die erste Seite ist in Abbildung 4 dargestellt.

.NH 1
Von troff ins Web
.LP
.ce 9
.Hr -url mailto:bischof@informatik.uni-osnabrueck.de          \
    "Hans-Peter Bischof"
.Hr -url http://www.mathematik.Uni-Osnabrueck.DE/home_ge.html \
    "Fachbereich Mathematik-Informatik"
.Hr -url http://www.informatik.uni-osnabrueck.de              \
    "Universit\*at Osnabr\*uck" .
.ce 0
.LP
.SH
Abstract
.LP
Einige wenige Unverbesserliche,

Abbildung 4: troff/unroff Input

Mit diesen Zeilen haben Sie alle zusätzlich Makros gesehen, die neben ein paar Programmen benötigt werden, um aus troff-Dokumenten Web-Seiten zu machen. Einfach, oder?

Nutzung

Lassen Sie mich, bevor wir in den nächsten Abschnitten zu den technischen Details kommen, erst die Randbedingungen, Ausgangssituation und Vorgehensweise beschreiben.

Ich schreibe meine Texte unter Unix und erzeuge PostSkript-Dateien mit den Werkzeugen der groff -Familie (groff(1), geqn(1), gtbl(1), gpic(1)) und lokal entwickelten Präprozessoren wie nsd [2]. In aller Regel wird bei der Übersetzung ein selbst entwickeltes Makropaket zusätzlich verwendet; Programmteile werden extrahiert und entsprechend aufbereitet -- Zeilennummern werden vorangestellt, \ durch \e ersetzt, etc -- und eingebunden. Gesteuert wird der Übersetzungsprozeß durch make(1). Mögliche Ziele sind hierbei:

1, 2, ...   die einzelnen Kapitel
all         das komplette Manuskript
spell       Buchstabierkontrolle
backup      Anlegen einer Sicherungskopie
clean       Löschen von Zwischenresultaten
nuke        Löschen von allen generierten Dateien
...
Es ist offensichtlich, daß die Pflege des makefiles aufwendig werden kann: Neue Projekte werden begonnen, Quellen innerhalb von existenten Projekten kommen hinzu, weitere Kapitel werden geschrieben, neue Quellen werden augelegt, etc. Weil die Nachführung bzw. der Neuaufbau von makefiles aon Hand nicht angenehm, sondern zeitraubend und langweilig ist, entwickelte ich ein makefile-Generierungsprogramm mk_gen, welches ein aktuelles makefile generiert.

Um dieses Skript möglichst ohne großen Aufwand erstellen und problemlos immer wieder verwenden zu können, war es hilfreich, eine gewissen Einheitlichkeit der Dokumente zu verlangen.

1.
Die Kapitel müssen fortlaufend benannt werden (c.01, c.02 ...)
2.
Alle verwendeten Quellen bzw. Quellfragmente müssen in die Quelle via ".so Src_t/path" eingebunden werden. Die zugehörigen Quellen müssen sich im Katalog "Src/path" befinden.

mk_gen funktioniert so gut, daß ich für die Produktion von PostSkript-Seiten in der Zwischenzeit keine makefiles mehr ``von Hand'' schreibe, sondern ausschließlich generieren lasse. Aus diesem Grunde sollte mk_gen so adaptiert werden, daß aus den troff-Quellen optional Web-Seiten generiert werden können.

Die Produktion von HTML-Seiten, Erstellen von Inhaltsverzeichnissen, Verbinden der Seiten, etc. zerfallen nach genauerer Analyse in zwei getrennte Problemkreise:

1.
Erzeugen der einzelnen Web-Seiten
2.
Verpacken der Web-Seiten in einheitliche Rahmen und Verbinden der Seiten

Das erste Problem kann man mit Werkzeugen lösen, welche sich im Netz finden. Grundsätzlich sind zwei verschiedene Wege gegangen worden:

1a.
Mit einem perl(1) oder awk(1)-Skript wird ein Konverter von einem Makropaket Richtung HTML implementiert. Normalerweise können eqn-, tbl-, pic-Quellen in Gif-Dateien umgewandelt und dann mit eingebunden werden. troff2html von Jim Briggs setzt ein mit dem me-Makropaket geschriebenes Dokument in HTML um.
Falls ein Makropaket, wie zum Beispiel man, eine sehr präzise Struktur produziert, wie dies beim man-Makropaket der Fall ist, besteht auch die Möglichkeit das mit nroff formatierte Dokument in eine Web-Seite umzuwandeln. man2html von Earl Wood setzt vorformatierte Manualseiten nach HTML um.
1b.
Im Kontrast dazu implementierte Oliver Laumann einen troff-Parser, unroff, welcher HTML anstelle einer Druckerbeschreibungssprache erzeugt. Dies ermöglicht unroff die Abarbeitung von benutzerdefinierten Makropaketen.

troff2html

troff2html ist ein perl-Skript, das ein troff-Dokument, falls es mit dem me-Makropaket geschrieben ist, nach HTML konvertieren kann. Folgende Funktionalität ist existent:

--   das me-Makropaket kann verwendet werden
--   die Ausgabe der Präprozessoren kann durch nroff
     weiter übersetzt bzw. via GIF-Bild
     inline eingebunden werden
--   alle ISO-8859-1 Sonderzeichen werden übersetzt
--   Inhaltsverzeichnis kann erzeugt werden
--   Navigationsleiste kann erzeugt werden
--   ein Aufsplitten des Dokuments ist konfigurierbar
--   eigene Makros können nur mit erheblichem Aufwand eingebaut werden

Das Resultat von troff2html ist nur dann befriedigend, wenn man keine in sich bzw. in die ``Welt'' verbundene Seiten generieren möchte. Die sehr komplizierte Adaption eigener Makros, Setzen von Registerwerten etc., schränkt den möglichen Einsatzbereich doch sehr ein. Ich fand troff2html für meine Anforderungen als nicht akzeptabel gefunden.

man2html

man2html ist ein perl-Skript, das Unix-Manual-Seiten nach HTML übersetzt und die Seiten untereinander auch verbindet. man2html akzeptiert als Eingabe mit nroff formatierte Manual-Seiten und generiert daraus eine HTML-Seite. Ein typischer Aufruf ist:

$ man echo | man2html > echo.html

Da man2html die Struktur der formatierten Seiten kennt, ist eine Umsetzung des Texts in HTML nicht weiter schwierig. Der generierte Text für den obigen Aufruf ist in Abbildung 5 dargestellt.

<HTML>
<BODY>
<PRE>
       echo - display a line of text


</PRE>
<H2>SYNOPSIS</H2><PRE>
       <STRONG>echo</STRONG> [<STRONG>-ne</STRONG>] [<EM>string</EM> ...]
       <STRONG>echo</STRONG> {--help,--version}

Abbildung 5: man echo | man2html | sed 10q

Schwieriger stellt sich die Sache dar, wenn die Seiten untereinander verbinden werden sollen. In diesem Fall kann sich man2html nur darauf verlassen, daß sich für Verweise der Art cmd(I), die an beliebiger Stelle im vorformatierten Text auftauchen können, eine Seite im Dateisystem an der Stelle manI/cmd.1 befindet bzw. diese Seite bei Bedarf erzeugt werden kann. Die Perl-Spezifikation des Musters lautet:

/([\+_\.\w-]+)\((\d)(\w?)\)/).

Für die Manual-Pakete einzelner Hersteller kann der Teil des perl-Skripts, der die Crossreferenzen einfügt, problemlos modifiziert werden.

Für man2html gibt es auch eine CGI-Schnittstelle. Diese kann allerdings bei unsachgemäßer Installation und böshafter Nutzung zu sehr viel Ärger führen. Die Anfrage nach ls; rm -rf / wird letztlich das Kommando man ls; rm -rf / ausgeführt.

Als Produzenten für ``rohe Seiten'' zum Beispiel als Begleitmaterial einer Vorlesung, kann man2html problemlos eingesetzt werden.

unroff

Unroff bearbeitet ein Dokument, welches mit troff gesetzt werden kann, und übersetzt dies in ein anderes Format, z.B. SGML oder HTML. Die konkrete Ausprägung der Übersetzung wird durch benutzerdefinierte Regeln und Funktionen spezifiziert. Die Programmierbarkeit wird durch die Nutzung von elk, einer Scheme-basierten Sprache erreicht. Je nach Back-End kann grundsätzlich jede beliebiges Ausgabeformat erreicht werden.

Besonders interessant ist, daß unroff einen vollständigen troff-Interpreter enthält. Dies hat zur Konsequenz, daß auch Dokumente, die ein eigenes Makropaket verwenden, mit unroff umgewandelt werden können.

Die Dokumente können untereinander durch Verweise verbunden sein, ebenso können externe Verweise verwendet werden.

Wie?

Die Produktion der Web-Seiten ist grob in drei Schritte unterteilt.

1.
Mit unroff werden die einzelnen Seiten und das Inhaltsverzeichnis generiert.
2.
Ein Shell-Skript, mk_links, generiert die Rahmen und verbindet die Seiten.
3.
Ein Shell-Skript, mk_toc_frame, generiert den Rahmen um das Inhaltsverzeichnis welches von unroff erzeugt wurde.

Unroff

Werden die Dokumente mit unroff übersetzt, können zusätzlich zwei Makros benutzt werden, um die Hypertext-Verweise zu generieren.

1.
Mit .Hr -url mailto:bischof "hpb" wird ein Anker hinterlegt, der das entsprechende mail-Tool aktiviert, wobei der Empfänger bischof ist. Als Text findet sich in der Web-Seite das letzte Argument des Makros. Wird das Dokument mit troff übersetzt wird dieses Argument kursiv gesetzt. Einen Hypertext-Verweis kann man mit .Hr -url http://www.informatik.uni-osnabrueck.de "Universit\*at Osnabr\*uck" an legen.
2.
Einen lokales Ziel erzeugt man mit .Ha sp Shell-Programmierung. Auf das lokale Ziel verweist man durch .Hr -symbolic sp "erstes Kapitel". In der Web-Seite findet sich der Text ``erstes Kapitel'', der dann zum lokalen Ziel sp zeigt.

Der Aufruf von unroff lautet zum Beispiel:

$ unroff -ms \
  document=all \
  split=2  \
  hyper.scm \
  lib/header c.01

c.01 wird mit dem ms-Makropaket gesetzt, wobei die Angabe von split=2 dafür sorgt, daß aus jedem Kapitel und aus jedem Unterkapitel erster Stufe eine eigene Seite wird, deren Dateiname mit all- beginnt. In hyper.scm sind die Hypertext-Anweisungen implementiert. lib/header enthält ein lokales Makropaket.

Wird c.01 mit troff gesetzt, lautet der Aufruf:

$ groff -tep -ms \
  lib/header c.01

Unroff kann trotz aller Mühen nicht alles in eine Web-Seite verwandeln, was man mit troff und seinen Präprozessoren setzen kann. Tabellen setzt unroff tbl und nroff und setzt sie dann als Text ein. Komplizierte typographische Objekte wie Mathematik-Satz, Liniengraphik etc. werden aus dem Text extrahiert, mit eqn und troff gesetzt, und letztlich in ein gif-Format umgewandelt. Ein Beispiel zeigt Abbildungldung 6.

Will man in Tabellen komplizierte typographische Objekte einbinden stößt man an die Grenzen der Architektur von unroff.

Weiteren Präprozessoren wie grap oder nsp kann können dagegen problemlos verwendet werden. So gesehen fügt sich unroff in die troff-Architektur optimal ein.

Das gesammte Verfahren ist zweistufig, weil unroff keinen Überblick über die produzierten Web-Seiten hat.

mk_links

Ein Setzen des Dokuments mit split=2 und document=all führt zu Dateinamen der Gestalt

all-1
        all-1.1
        all-1....
all-2
        all-2.1
        all-2....

Der Pfeil in den Navigationsleisten nach rechts bzw. links soll immer zur nächsten bzw. zur vorhergehenden Seite verweisen, falls diese existiert; falls nicht soll ein Link auf einen Verweis eingebaut werden, der anzeigt, daß hier die Welt zu Ende ist und der Pfeil ist durchgestrichen. Der Pfeil nach oben bzw. unten soll immer zur nächsten bzw. zur vorhergehenden Hauptkapitel verweisen, falls dieses existiert. Das nachfolgende Teil eines Shellskripts verbindet die Seiten korrekt.

 ...
304     
305     doLink()     # prefix j i
306     {
307         LEFT= `find_prev $1 $2 $3`   # naechste Seite
308         RIGHT=`find_next $1 $2 $3`   # vorhergehende Seite
309         UP=   `find_up   $1 $2`      # vorhergehendes Kapitel
310         DOWN= `find_down $1 $2`      # naechstes Kapitel
311         create_frame $4 $LEFT $RIGHT $UP $DOWN $PREFIX.html
312     }
313     j=1
314     i=0
315     while :                          # ueber alle Kapitel
316     do
317         if [ $i -eq 0 ]
318         then                         # Hauptkapitel
319             FILE=$PREFIX-$j.html
320         else                         # Unterkapitel
321             FILE=$PREFIX-$j.$i.html
322         fi
323     
324         if [ -f $FILE ]              # am Ende der Welt?
325         then
326             doLink $PREFIX $j $i $FILE
327         else
328             j=`expr $j \+ 1`
329             if [ ! -f $PREFIX-$j.html ]
330             then
331                 break
332             fi
333             i=-1
334         fi
335     
336         i=`expr $i \+ 1`
337     
338     done
339     
 ...

Manual

Unter Verwendung der vorgestellten Technik kann man mit unroff auch die Manualseiten in Web-Seiten umwandeln. Es wird ein zum Manual-Baum paralleler Baum aufgebaut, der anstelle von troff-Input HTML-Seiten enhält. Zusätzlich müssen nur zwei weitere Shellskripte eingesetzt werden, um die Navigation zu erleichtern. Eines setzt die Querverweise in HTML-Verweise um, das andere generiert ein Inhaltsverzeichnis für jeden Katalog, in dem sich HTML-Seiten befinden. Siehe Abbildung 7 und 8. Für jedes Problem wurde ein Skript entwickelt, wobei angemerkt werden muß, daß die Querverweise nur dann eingebaut werden konnten, wenn die Verweise in den Manualseiten systematisch verwendet wurden.

So werden in den Manuals zu Sun OS 4.1.3 bzw. Solaris 2.0 alle Verweise mit dem Makro BR gesetzt. Das Skript kann auf jede Systematik angepaßt werden. Werden die Manualseiten unsystematisch geschrieben, ist es nicht möglich, alle Querverweise in die HTML-Seiten aufzunehmen.

Literatur

[1]
Russ Jones & Adrian Nye, "HTML und das World Wide Web: selbst Publizieren imWWW", O'Reilly, 1995.
[2]
Axel T. Schreiner, "Ein DIN 66261 Baukasten --``/usr/bin/nsd''" unix/mail 4/89, Carl Hanser Verlag, 1989.

Http-Adressen:

elk

http://www-rn.informatik.uni-bremen.de/software/elk
man2html

http://medusa.acs.uci.edu/indiv/ehood/man2html/doc/man2html.html
troff2html

http://www.sis.port.ac.uk/~briggsjs/troff2html.html
unroff

http://www-rn.informatik.uni-bremen.de/software/unroff
Quellen

Die Quellen sind über die HTTP -Adresse im Impressum zu beziehen.

8.11.  ``imake'' contra ``I make''

Todd Brunhoff (Tektronix) entwickelte im Zusammenhang mit dem Projekt Athena (MIT) eine Technik, mit der installationsunabhängig Makefiles beschrieben werden können. In einer installationsunabhängigen Imakefile-Datei wird der Zusammenhang Ziel-Quelle inklusive der zugehörigen Aktionen unter Verwendung von vordefinierten C-Makros und Konstanten definiert. In den installationsabhängigen template-Dateien werden diese C-Makros und Konstanten (CFLAGS, CC, ...) definiert. imake erzeugt dann, ausgehend von einer Imakefile-Datei, auf jeder Installation ein korrektes Makefile. In diesem Artikel wird imake aus X11 Release 5 beschrieben.

Das Problem

Generiert und installiert man Software unter Unix, dann hat man neben dem üblichen Geplänkel mit den Fehlern der Utilities das Problem, daß die Schnittmenge der Gemeinsamkeiten der verschiedenen Ziel-Plattformen gegen die leere Menge strebt. Es gibt umso weniger Gemeinsamkeiten, je mehr Fremdsoftware auf den Rechnern installiert ist. Exemplarisch seien einige Probleme aufgeführt:

Ein Ansatz

Jeder, der Software portiert, muß seine Makefiles an die jeweilige Installation anpassen. Es wäre doch wesentlich arbeitssparender, wenn die in den Makefiles benötigte Information aus zentral gehaltenen, für die jeweilige Installation zugeschnittenen ``Datenbanken'' stammen würde. Im Zusammenhang mit der Entwicklung von X machten sich die Entwickler am MIT u.a. auch über die Portierungsmechanismen Gedanken. Folgende Prinzipien sollten bei der Entwicklung eines Werkzeugs berücksichtigt werden:

Als Lösung wird imake angeboten -- eine Koproduktion aus Konfigurationsdateien, C-Makros, make und dem C-Präprozessor cpp.

Ein Beispiel

Abbildung 1 zeigt ein Imakefile für xcalc (von John H. Bradley, University of Pennsylvania),

$ cat -n Imakefile
 1   XCOMM $XConsortium: Imakefile,v 1.8 91/08/15 12:19:56 rws Exp $
 2   #if defined(MacIIArchitecture) || defined(MotoR4Architecture)
 3         IEEE_DEFS = -DIEEE
 4   #endif
 5           DEFINES = $(IEEE_DEFS) $(SIGNAL_DEFINES)
 6           DEPLIBS = XawClientDepLibs
 7   LOCAL_LIBRARIES = XawClientLibs
 8     SYS_LIBRARIES = -lm
 9              SRCS = actions.c math.c xcalc.c
10              OBJS = actions.o math.o xcalc.o
11   ComplexProgramTarget(xcalc)
12   InstallAppDefaults(XCalc)

Abbildung 1: ``Imakefile'' für ``xcalc''

Abbildung 2 zeigt einen Teil des entstehenden Makefiles.

$ imake
$ cat Makefile
 ...
 1   #  $XConsortium: Imakefile,v 1.8 91/08/15 12:19:56 rws Exp $
 2
 3           DEFINES = $(IEEE_DEFS) $(SIGNAL_DEFINES)
 4           DEPLIBS = $(DEPXAWLIB) $(DEPXMULIB) $(DEPXTOOLLIB) $(DEPXLIB)
 5   LOCAL_LIBRARIES = $(XAWLIB) $(XMULIB) $(XTOOLLIB) $(XLIB)
 6     SYS_LIBRARIES = -lm
 7              SRCS = actions.c math.c xcalc.c
 8              OBJS = actions.o math.o xcalc.o
 9
10   PROGRAM = xcalc
11
12   xcalc: $(OBJS) $(DEPLIBS)
13   	$(RM) $@
14   	$(CC) -o $@ $(OBJS) $(LDOPTIONS) $(LOCAL_LIBRARIES)\
15                               $(LDLIBS) $(EXTRA_LOAD_FLAGS)
16   install:: xcalc
17   	 @if [ -d $(DESTDIR)$(BINDIR) ]; then set +x; \
18   	else (set -x; $(MKDIRHIER) $(DESTDIR)$(BINDIR)); fi
19   	$(INSTALL) -c $(INSTPGMFLAGS)   xcalc $(DESTDIR)$(BINDIR)
20
21   install.man:: xcalc.man
22   	 @if [ -d $(DESTDIR)$(MANDIR) ]; then set +x; \
23   	else (set -x; $(MKDIRHIER) $(DESTDIR)$(MANDIR)); fi
24   	$(INSTALL) -c $(INSTMANFLAGS) xcalc.man        \
25                  $(DESTDIR)$(MANDIR)/xcalc.$(MANSUFFIX)
26   install:: XCalc.ad
27   	 @if [ -d $(DESTDIR)$(XALPLOADDIR) ]; then set +x; \
28   	else (set -x; $(MKDIRHIER) $(DESTDIR)$(XALPLOADDIR)); fi
29   	$(INSTALL) -c $(INSTALPFLAGS) XCalc.ad $(DESTDIR)$(XAPPLOADDIR)/XCalc
 ....

Abbildung 2: Teil des aus Abbildung 1 resultierenden ``Makefiles''

Betrachtet man die beiden Dateien etwas genauer, wird klar, wie die Makros ComplexProgramTarget (siehe auch Abbildung 6) und InstallAppDefaults aussehen müssen.

Durch den C-Makro ComplexProgramTarget (Abb. 1, Zeile 11) werden die Regeln generiert, mit denen man xcalc generieren (Abb. 2, Zeilen 12-15) und installieren (Abb. 2, Zeilen 16-19) und die Manualseiten (Abb. 2, Zeilen 21-25) installieren kann. In diesem C-Makro wird u.a. die Information der zuvor definierten Konstanten verwendet. Der C-Makro InstallAppDefaults generiert die Regeln, mit denen die zugehörige Ressourcedatei (Abb. 2, Zeilen 26-29) installiert werden kann.

Das Prinzip

imake ist ein kleines, einfaches, und dabei doch sehr nützliches Programm. Unter Kontrolle von imake liest der C-Präprozessor die öffentlichen Konfigurations- (*.cf), Template- (*.tmpl) und Regeldateien (*.rules). In diesen Dateien werden C- und make-Makros definiert. Im Imakefile des Anwenders kann man diese ersetzen bzw. ergänzen, und man läßt sich die nötigen make-Regeln durch Aufruf von C-Makros, z.B.
ComplexProgramTarget generieren.

Das Wissen über die konkreten Verhältnisse, die auf einer bestimmten Plattform herschen, ist in zentral verwalteten Dateien (Imake.tmpl, machine.cf , Lib.rules und site.def ) abgelegt. Je Projekt -- wie etwa X11 oder Motif -- wird in der Regel eine Template- und eine Regel-Datei gepflegt. Lokal entwickelte Dateien verwenden meistens existente Regel- oder Template-Dateien. Diese enthalten sehr viel undokumentierte Information. Das aus Sicht des Entwicklers entstehende Dilemma ist offensichtlich. In aller Regel läßt es sich nur durch das aufwendige Studium der entsprechende Konfigurationsdateien (ca. 3000 Zeilen) lösen.

Abbildung 3 zeigt den Aufbau von Imake.tmpl.

[picture]

Abbildung 3: Aufbau von ``Imake.tmpl''

Imake.tmpl bildet den Rahmen, in den die weiteren Dateien via include eingebunden sind. In Imake.tmpl werden für fast alle benötigten Kommandos, Pfade und Flaggen symbolische Namen definiert. Von dieser generischen Plattform abweichende Werte müssen in site.def bzw. machine.cf (z.B. sun.cf) definiert werden. Die Auswahl der korrekten maschinenspezifischen Plattform geschieht durch cpp in Verbindung mit imake. In site.def werden alle aufgrund der lokalen Installation vom default-Wert abweichenden Variablen, z.B. ProjectRoot und UsrLibDir , definiert. In machine.cf werden die das Betriebssystem beschreibenden Variablen definiert. Die

#ifndef   x
#	define x
#endif

Technik garantiert, daß die Variablen nicht in den nachfolgenden Dateien überdefiniert werden. In aller Regel müssen site.def und machine.cf vom lokalen Systemadministrator ediert werden.

In Project.tmpl werden alle für das Projekt spezifischen Variablen festgelegt. Imake.rules enthält alle Definitionen der verwendbaren Makros. Imakefile beschreibt die Quellen und Ziele sowie deren Zusammenhang. Alles, was von cpp nicht bewertet werden kann, wird durchkopiert. Somit können an dieser Stelle auch ganz ``normale'' Makefile -Anweisungen stehen.

Generierung

Die Funktionsweise von imake basiert im wesentlichen darauf, daß der C-Präprozessor ein maschinenabhängiges Symbol definiert hat, um Konditionalisierung der Art:

#ifdef sun
     Sun-spezifische
     Definition
#endif

zu ermöglichen. Beim Entwurf von imake konnte selbstverständlich nicht davon ausgegangen werden, daß jeder C-Präprozessor ein eindeutiges Symbol definiert. Aus diesem Grund wurde die Möglichkeit geschaffen, durch Änderung einer imake include -Datei (imakedep.h) den internen Aufruf des C-Präprozessors so zu gestalten, daß an ihn eine Option der Form -Duniq_symbol übergeben wird.

Im Vektor cpp_argv aus imakedep.h steht, welches Kommando (cpp oder cc) als Präprozessor verwendet und mit welchen Optionen das Kommando aufgerufen wird. Der Vektor wird mit der bekannten #ifdef ...-Technik statisch initialisiert. Sollten in diesem Bereich Probleme auftauchen, müssen die Quellen verändert werden. In diesem Fall sind die Punkte Step 1 und Step 5 in imakedep.h zu beachten.

Ein Beispiel

An einem Beispiel wollen wir imake auf dem Weg durch das Labyrinth der Dateien auf einem Sun-Arbeitsplatzrechner verfolgen. Falls die Konfigurationsdateien nicht im default-Katalog liegen, wird imake der Pfad, der zu den Konfigurationsdateien führt, über die Environmentvariable IMAKEINCLUDE oder als Option auf der Kommandozeile angegeben. Abbildung 1 zeigt die Eingabe für imake. Im default-Fall sucht imake im aktuellen Katalog nach der Datei Imakefile und generiert eine Datei namens Makefile.

Imake.tmpl ist die erste Datei, die von imake gelesen wird. Als eine der ersten Zeilen findet sich dort: #define XCOMM # (siehe auch Abb. 1, Zeile 1). Im weiteren wird somit das Wort XCOMM als Kommentarbeginn betrachtet. Mit diesem Mechanismus wurde die Möglichkeit geschaffen, Kommentar besonders hervorzuheben. Falls lokale C-Präprozessoren an dieser Stelle scheitern sollten, hilft ein \ vor dem #. Mit der Phrase aus Abbildung 4 wird fixiert, in welcher Datei die plattformspezifische Information zu finden ist.

#ifdef sun
#      define MacroIncludeFile <sun.cf>
#      define MacroFile sun.cf
#      undef sun
#      define SunArchitecture
#endif /* sun */

Abbildung 4: Bestimmung der maschinenspezifischen Beschreibung

Die nächste wesentliche Zeile in Imake.tmpl lautet:
#include MacroIncludeFile.

In sun.cf werden u.a. die Betriebssystemversion (OSMajorVersion und OSMinorVersion ) und verschiedene Flaggen gesetzt, welche die Generierung (XsunMonoServer) bzw. Installation (SetTtyGroup) beeinflussen. Die Regeln, nach denen Büchereien auf einer Sun-Architektur gebaut und installiert werden, werden in sunLib.rules definiert. Diese Datei wird via include in sun.cf eingebunden. Die nächsten wesentlichen Zeilen in Imake.tmpl definieren zwei C-Makros, mit denen zwei (Concat) bzw. drei (Concat3) Strings zusammengefügt werden können. Weil diese in den weiteren Dateien benutzt werden, findet die Definition an dieser, etwas unlogischen Stelle statt.

Nach dieser Definition wird die für den lokalen Systemadministrator wichtigste Datei mit
#include <site.def>
gelesen. In dieser Datei wird all das definiert, was die lokale Installation von der default -Installation unterscheidet. Bei uns finden sich dort die Definitionen aus Abbildung 5.

#ifdef sun
#      define MacroIncludeFile <sun.cf>
#      define MacroFile sun.cf
#      undef sun
#      define SunArchitecture
#endif /* sun */

Abbildung 4: Bestimmung der maschinenspezifischen Beschreibung

#define UsrLibDir         /usr/llib
#define IncRoot           /usr/linclude
#define ManDirectoryRoot  /usr/man/sun_manx
#define BinDir            /usr/lbin/X11
#define ManSuffix         1

Abbildung 5: Lokale Veränderungen in ``site.def''

Die nächsten 500 Zeilen in Imake.tmpl gießen die real vorhandene Plattform in eine imaginäre. Alle Kommandos wie

(#define CppCmd /lib/cpp),

Optionen, Pfadkomponenten, Pfade usw., die im weiteren benutzt werden, sind jetzt hinter symbolischen Namen verborgen.

Project.tmpl enthält alles X-Spezifische. Mit Konstanten wird gesteuert, ob ein Fontserver (BuildFontServer) gebaut wird, mit welcher Kennung die Manualseiten (LibManSuffix und ManSuffix) abgelegt werden und derlei Dinge mehr. sunLib.tmpl enthält alle projektspezifischen Informationen, die Büchereien betreffen. Die Versionsnummern der shared library (#define SharedXlibRev 4.10) werden dort ebenso fixiert wie die Pfade, die zu den X-Bibliotheken führen. Diese beiden Dateien werden in aller Regel nicht modifiziert. Soll ein neues Projekt mit imake verwaltet werden, muß meistens eine neue Projekt-Datei entwickelt werden. Die Kunst besteht dann darin, alle projektspezifischen Informationen in korrekter Abhängigkeit anzuordnen.

In Imake.rules werden ca. 120 C-Makros definiert, die zum Generieren und Installieren des Projekts, in diesem Fall X11 Release 5, verwendet werden. Abbildung 6 zeigt die Definition von ComplexProgramTarget.

#define ComplexProgramTarget(program)                    @@\
        PROGRAM = program                                @@\
                                                         @@\
AllTarget(program)                                       @@\
                                                         @@\
program: $(OBJS) $(DEPLIBS)                              @@\
        RemoveTargetProgram($@)                          @@\
        $(CC) -o $@ $(OBJS) $(LDOPTIONS)                   \
        $(LOCAL_LIBRARIES) $(LDLIBS) $(EXTRA_LOAD_FLAGS) @@\
                                                         @@\
SaberProgramTarget(program,$(SRCS),$(OBJS),                \
                  $(LOCAL_LIBRARIES),NullParameter)      @@\
                                                         @@\
InstallProgram(program,$(BINDIR))                        @@\
InstallManPage(program,$(MANDIR))

Abbildung 6: Definition des C-Makros ``ComplexProgramTarget''

Dieser Makro verwendet die zuvor definierten Makros und Konstanten.

Ein Trick

Imake sammelt alle benötigten Dateien auf, die defines werden eingesetzt, die Makros bewertet und fertig ist das Makefile. So weit, so gut. Aber an sich sollten für imake zum Generieren und Installieren der Software dieselben Konfigurationsdateien wie beim Entwickeln mit der später installierten Software verwendet werden. Der Unterschied im Makefile , der zwischen Generierung der Software innerhalb des Quellbaums und der Benutzung der installierten Software liegt, ist in aller Regel nur in den Pfaden zu sehen, die zu den ausführbaren Programmen, den Bibliotheken und include -Dateien führen. Ein Beispiel: der Pfad, der zu einer Bibliothek führt, kann folgendes Aussehen haben: bei Generierung eines Objekts im Quellbaum ../../lib/libX11.a und bei der Generierung einer Applikation nach der Installation /usr/llib/X11/libX11.a. Trotzdem soll im Makefile in beiden Fällen jeweils derselbe symbolische Name benutzt werden können. Also müssen die entsprechenden Zuweisungen konditionalisiert werden, abhängig von der jeweiligen Verwendung. Wie der Pfad dann zusammengebaut werden muß, zeigt Abbildung 7.

/*
 * _UseCat - combination of _Use and Concat.
 *           exists to avoid problems with some preprocessors
 */
#ifndef _UseCat
    #if __STDC__ && !defined(UnixCpp)
        #ifdef UseInstalled
            #define _UseCat(a,b,c) a##c
        #else
            #define _UseCat(a,b,c) b##c
        #endif
    #else
        #ifdef UseInstalled
            #define _UseCat(a,b,c) a/**/c
        #else
            #define _UseCat(a,b,c) b/**/c
        #endif
    #endif
#endif

    USRLIBDIR = /usr/llib/X11
   TOOLKITSRC = ../../lib
         XLIB = _UseCat($(USRLIBDIR),$(TOOLKITSRC),/libX11.a)

Abbildung 7: Konditionalisierte Zuweisung

makedepend

Ein weiteres nützliches Werkzeug ist makedepend. Dieses, auch von Todd Brunhoff entwickelte Kommando, untersucht beliebige Dateien auf include-Anweisungen, generiert die daraus folgenden make-Regeln und fügt diese am Schluß zu dem Makefile hinzu. Es werden alle Konditionalisierungsmechanismen gemäß den Direktiven des C-Präprozessors behandelt. Abbildung 8 zeigt eine für ein Makefile typische Regel.

depend: $(SRC)
	makedepend -- $(CFLAGS) -- $(SRCS)

Abbildung 8: typische Regel eines Makefiles

Abbildung 9 die Ausgabe von % makedepend imake.c.

depend: $(SRC)
	makedepend -- $(CFLAGS) -- $(SRCS)

Abbildung 8: typische Regel eines Makefiles

# #  -------------------------------------------------------------------------
# #  dependencies generated by makedepend

# DO NOT DELETE

imake.o: /usr/include/stdio.h /usr/include/stddef.h /usr/include/stdarg.h
imake.o: /usr/include/ctype.h .././X11/Xosdefs.h /usr/include/sys/types.h
imake.o: /usr/include/fcntl.h /usr/include/signal.h /usr/include/sys/signal.h
imake.o: /usr/include/sys/stat.h /usr/include/sys/wait.h
imake.o: /usr/include/stdlib.h /usr/include/errno.h /usr/include/sys/errno.h
imake.o: imakemdep.h
# DO NOT DELETE THIS LINE -- make depend depends on it.

Abbildung 9: Resultat von ``makedepend imake.c''

Fazit

Daß imake ein sehr gut funktionierendes Werkzeug ist, zeigt sich, wenn man X und Motif auf verschiedenen, nicht vorgesehenen Plattformen generiert und installiert. Man ist erstaunt, wie leicht es geht. Auch die Erweiterung der Konfigurationsdateien, um eigene Projekte in das System mit einbinden zu können, ist nicht schwierig. Ab und an fragt man sich allerdings, ob nicht mit Kanonen auf Spatzen geschossen wird, denn die für die Sun-Architektur benötigten Konfigurationsdateien umfassen zusammen ca. 3000 Zeilen. Um eine gezielte Änderung erreichen zu können, muß man gelegentlich schon etwas genauer hinsehen. Trotzdem, der Einsatz von imake und makedepend hat sich bei uns vielfach ausbezahlt. In diesem Fall gilt bestimmt nicht: ``Was nichts kostet, taugt nichts''.

Literatur

J. Fulton, ``Configuration Management in the X Window System'', MIT, Dokumentation zu X11 Rev. 5.

S. Feldman, ``make -- A Program for Maintaining Computer Programs'', Software -- Practice and Experience, April 79.

9.  Aussicht in Richtung Betriebssyteme

Literatur

Definition eines Betriebssystems

DIN 44300 (1978):

Die Programme eines digitalen Rechensystems, die zusammen mit den Eigenschaften der Rechenanlage die Grundlage der möglichen Betriebsarten des digitalen Rechensystems bilden und insbesondere die Abwicklung von Programmen steuern und überwachen.

Aus Entwicklersicht:

Die Realisierung von Ideen, mit denen die zur Verfügung stehende Hardware optimal genutzt werden kann.

Aus Benutzersicht:

Ein Betriebssystem ermöglicht die bequeme Verwendung eines Computers zur Lösung von beliebigen (?) Problemen.
Pragmatisch

Ein Betriebssystem soll
1.   alle zur Verfügung stehenden Ressourcen auf alle Beteiligten
     gerecht verteilen
2.   Arbeit erledigen
3.   Daten halten
4.   einen Netzanschluß bereitstellen
5.   Kommunikationswege erschließen (world wide web)
6.   Datenschutz gewährleisten
7.   auftauchende Funktionsfehler erkennen.

Beispiele von Betriebssystemen

CP/M

Control program for microcomputers. Ein einfaches Betriebssytem für 8-Bit Prozessoren, wie z.B. den Z80, 8080/85.
Multics

DOS
Disk operating System.
Das am häufigsten installierte Betriebssytem fuer IBM-kompatible PC's.
VMS

Virtual Machine System. VMS ist ein Betriebssystem für DEC Großrechner welches für jeden Benutzer eine virtuellen Maschine bereitstellt.
UNIX

Unix ist ein Hersteller unabhängiges Betriebssystem, welches in den Bell Labs. entwickelt worden ist.
Windows NT

Windows New Technology
Plan 9

Ed Wood jr: Plan 9 from outer Space. Unix ist ein Hersteller unabhängiges Betriebssystem, welches in den Bell Labs. entwickelt worden ist.

Beispiele von Betriebssystemprojekten

Aegis/Exo-kernel

(Massachusetts Institute of Technology) Group Members: Frans Kaashoek , Dawson Engler. The Aegis kernel is built around he idea of an exo-kernel. An evolution from micro-kernels, exo-kernels export a virtual machine that securely multiplexes resourses among mutually distrusting spplications. The exo-kernel philosophy tries to export as few abstractions besides the basic hardware abstractions as possible, and to implement as little policy in the kernel as possible. Like SPIN, Aegis relies on techniques such as downloading code into the kernel to make the system fast.
Allegro

(Allegro Systems) Allegro 2.0 is a 32-bit pre-emptive, multi-tasking, protected mode operating system with adaptive scheduling intended for time critical applications. Its highly efficient kernel, written specifically for the 386/486/Pentium family of processor, provides an excellent balance between size, speed, and capabilities.
Amoeba

(Vrije Universiteit) Amoeba is a powerful microkernel-based system that turns a collection of workstations or single-board computers into a transparent distributed system. It has been in use in academia, industry, and government for about 5 years. ...

Siehe auch: http://www.cs.arizona.edu/people/bridges/oses.html

9.1.  Hardware

9.2.  Schematische Sicht

[picture]

9.3.  Abstrakte Sicht

  +------------------------------------------------------------+
  |A(u): Die Benutzerschnittstelle zum Rechner.                |
  +------------------------------------------------------------+
  |... verschiedene Schichten Anwendungssoftware               |
  +------------------------------------------------------------+
  |A(s): Die Schnittstelle zum Betriebssystem (Systemaufrufe). |
  +------------------------------------------------------------+
  |A(h): Die Software/Hardware Schnittstelle.                  |
  +------------------------------------------------------------+
  |... Hardware.                                               |
  +------------------------------------------------------------+

9.4.  Aufgaben eines Betriebssystems

9.5.  Betriebssysteme für ...

Betriebsarten

9.6.  Nutzung der Leistungen eines Betriebssystems

9.7.  Bestandteile/Funktionen eines Betriebssystems

Prozesse

1.   Erzeugen
2.   Anhalten (aktiv, passiv)
3.   Reaktivieren (aktiv, passiv)
4.   Terminieren (aktiv, passiv)
5.   Kommunikation
6.   Verteilung der Rechenzeit
7.   Benutzeridentifikation
8.   Rechte

Dateien

1.   Dateisystem als Baum
2.   Pfade zu Dateien
3.   Zugriff, u.U. exklusiv, auf Dateien
4.   Zugriffsrechte

Systemaufrufe

Systemaufrufe sind die Schnittstelle zwischen Betriebssystem und Benutzerprogramm. Die Gesamtmenge alle Systemaufrufe wird als application program interface, API, bezeichnet.

Speicherverwaltung

1.   Swapping
2.   Virtueller Speicher
3.   Seitenaustauschalgorithmen

Input/Output

1.   Eigenschaften von I/O-Hardware
2.   Eigenschaften von I/O-Software
3.   Festplatten
4.   Uhren
5.   Terminals

9.8.  Betriebssytemstrukturen

Mögliche Struktur

[picture]

Hardware

Chips, Platinen, ...
Schnittstelle → Hardware

Interrupts
Steuerung der Hardware

Einheitliche Bereitstellung einer Software-Schnittstelle von Komponenten gleicher Art.
Speicherverwaltung

Überwachung der Betriebsmittel Speicher, die von laufenden Prozessen allokiert werden.
Prozeßverwaltung

Initiierung, Terminierung und Laufzeitverwaltung aller Prozesse.
Geräteverwaltung

Überwachung und Verwaltung der I/O-Geräte.
Kommunikation

Die Kommunikation der einzelnen Teile des Betriebssystems muß koordiniert werden.
Dateiverwaltung

Verwalten von Dateien/Katalogen in hardware-unabhängiger Form.
Schnittstelle Systemaufrufe

Stellt die Leistungen des Betriebssystems nach außen, sprich für Anwendungsprogramme, zur Verfügung.
Dienstprogramme

Datenbanken, Compiler, Texteditoren, Textprozessor, ...
Kommandointerpreter

Shell, Oberfläche
Anwenderprogramme

Selbst entwickelte Kommandos, ...
Anwender

Sie

Mögliche Realisierungen

9.9.  Prozesse

Prozeß-Modell

Prozeß-Hierarchie

Operationen mit Prozessen

Prozeß-Zustände

·   rechnend
·   rechenbereit
·   blockiert

Prozeßübergänge

[picture]

1.   Prozeß wird blockiert
2.   Scheduler  wählt  einen  anderen Prozeß
     aus der      Menge  der  rechenbereiten
     Prozesse.
3.   Dieser Prozeß rechned.

Prozeß-Beschreibung

Prozeß-Verwaltung

Prozeß-Scheduling

Verteilung

Potentielle Algorithmen

Es stellen sich noch folgende Fragen:

9.10.  Einstiegspunkte in den Kern

[picture]

Systemaufrufe

[picture]

Hardware-Interrupts

10.  awk

Dies ist eine kurzen Einführung in awk.

Die Sprache wird an Hand von kleinen Beispielen eingeführt. Ein sehr gute Beschreibung findet sich in: Aho/Kernighan/Weinberge, The AWK Programming Language, ISBN: 0-201-07981-X.

Der Aufruf von awk

1.
Das awk -Programm in einer Datei:
gawk [ POSIX or GNU style options ] -f program-file [ -- ]

2.
Das awk -Programm kann beim Aufruf mit angebeben werden.
gawk [ POSIX or GNU style options ] [ -- ] program-text file

10.1.  Zeilennumerierung:

 1      #
 2      #       numeriert die Zeilen einer Datei durch.
 3      
 4              {       printf "%4d:    %s\n",  NR, $0          }

10.2.  Nutzer ohne Paßwort I:

 1      #!/bin/sh
 2      #       sucht Nutzer ohne Passwort
 3      #       ypcat passwd | sed 2q
 4      #       operator:* noway *:5:5::/bin:
 5      #       odstumpe::26:220:Dieter Stumpe:/home/oop/odstumpe:...
 6      
 7      ypcat passwd | awk '
 8      /::/    {       print $0        }
 9      '

10.3.  Nutzer ohne Paßwort II:

 1      #!/bin/sh
 2      #       sucht Nutzer ohne Passwort
 3      #
 4      #       ypcat passwd | sed 2q
 5      #       operator:* noway *:5:5::/bin:
 6      #       odstumpe::256:220:Dieter Stumpe:/home/oop/odstumpe:...
 7      
 8      ypcat passwd | awk -F: '
 9      $2 == ""        {       print $1, $5    }
10      '

10.4.  Nutzer ohne Paßwort mit 'schöner' Ausgabe III:

 1      #!/bin/sh
 2      #       sucht Nutzer ohne Passwort
 3      #       + BEGIN END
 4      #
 5      #       ypcat passwd | sed 2q
 6      #       operator:* noway *:5:5::/bin:
 7      
 8      ypcat passwd | awk -F: '
 9      BEGIN           {       count = 0               }
10      
11      $2 == ""        {       print $1, $5;
12                              count ++                }
13      
14      END             {       print "Es wurden ", count, "gefunden."
15                      }
16      
17      '

10.5.  Assoziative Felder I:

 1      #!/bin/sh
 2      #       assoziative Felder
 3      #
 4      
 5      cat << EOF      | awk '
 6              {       if ( names[$1] != "" )
 7                              names[$1] ++;
 8                      else
 9                              names[$1] = 1;
10                      print $1
11      
12                      if ( names["aaa"] != "" )
13                              names["aaa"] = 1
14              }
15      END     {       for ( i in names )
16                              print i, ":", names[i]
17              }
18      
19      '
20              one
21              two
22              three
23              four
24              two
25      EOF

10.6.  Assoziative Felder II:

 1      #!/bin/sh
 2      #       assoziative Felder
 3      #
 4      
 5      cat << EOF      | gawk '
 6              {       if ( $1 in names )
 7                              names[$1] ++;
 8                      else
 9                              names[$1] = 1;
10                      print $1
11      
12                      if ( "aaa" in names )
13                              names["aaa"] = 1
14              }
15      END     {       for ( i in names )
16                              print i, ":", names[i]
17              }
18      
19      '
20              one
21              two
22              three
23              four
24              two
25      EOF

10.7.  Das erste und letzte Wort einer Zeile:

 1      #!/bin/sh
 2      #       Das erste und letzte Wort einer Zeile
 3      #
 4      
 5      cat << EOF      | gawk '
 6                      { print NF, $0 ":" ,$1,  $NF    }
 7      
 8      '
 9              one
10              two     TWO
11              three   THREE
12              one
13              
14      EOF

10.8.  Numerischer oder Lexikographischer Vergleich:

 1      #!/bin/sh
 2      #       Number == String
 3      #
 4      
 5      gawk '  
 6      BEGIN   {       number = 99;
 7                      string = "099";
 8                      printf "string = %s, number = %s\n",
 9                                      string, number ;
10                      if ( string == number )
11                              print "( string == number )";
12                      else
13                              print "( string !== number )";
14                      if ( 0 + string == number )
15                              print "( 0 + string = number )";
16                      if ( string == "0" number  )
17                              print "( string = 0number )";
18                      
19                      print string + number
20              }
21      
22      '

10.9.  Pattern:

 1      #!/bin/sh
 2      #       String enthaelt pattern
 3      #       assoziative Felder
 4      #
 5      
 6      cat << EOF      | gawk '
 7              {       if ( $1 ~ /[Bb]+ananen/ )
 8                              print $2, "moegen", $1
 9              }
10      '
11              ananen          Affen
12              Bbananen                Affen
13              Bananen         Affen
14              bananen         affen
15      EOF

10.10.  Feldtrenner:

 1      #!/bin/sh
 2      #       Setzen des Fieldseperators
 3      #
 4      
 5      cat << EOF      | gawk -F'[.,% $PID F`;!?]' '
 6              {       for ( i = 1; i <= NF; i++ )
 7                              print $i
 8              }
 9      '
10      Ein Satz hat ein , und einen . Ende
11      EOF

10.11.  Kommunikation I

Kommunikation zwischen sh und awk I:

 1      #!/bin/sh
 2      #       Kommunikation zwischen sh und awk.
 3      #       1. Shellvariable --> awk
 4      #       operator:* noway *:5:5::/bin:
 5      #       odstumpe:vQ:26:220:Dieter Stumpe:/home/odstumpe:...
 6      #       ypcat passwd | sed 2q
 7      
 8      GID=11
 9      
10      echo $GID
11      ypcat passwd | awk -F: '
12      BEGIN           {       print "GID =", '"$GID"' }
13      $4 == '"$GID"'  {       print $1, $5            }
14      ' | more

10.12.  Kommunikation II

Kommunikation zwischen sh und awk II:

 1      #!/bin/sh
 2      #       Kommunikation zwischen sh und awk.
 3      #       2. awk --> shell
 4      #       ypcat group | fgrep inform
 5      #       inform:*:11:
 6      #       eval GID=333
 7      #       echo "gid = $GID"
 8      
 9      eval `ypcat group | awk -F: '
10      $1 == "inform"  {       print "GID=" $3         }
11      '`
12      
13      echo "gid = $GID"

10.13.  Kommunikation III

Kommunikation zwischen sh und awk III:

 1      #!/bin/sh
 2      #       Kommunikation zwischen sh und awk.
 3      #       2. awk <--> shell
 4      #       ypcat group | fgrep inform
 5      #       inform:*:11:
 6      #       eval GID=333
 7      #       echo "gid = $GID"
 8      
 9      . shell_lib.a
10      
11      case $#
12      in      0)      fatal "`basename $0`: `basename $0` groupname"
13      esac
14      
15      echo "ypcat group | grep -s $1"
16      ypcat group | grep "^$1:" > /dev/null
17      
18      case $?
19      in      0)      eval `ypcat group | awk -F: '
20                      $1 == "'"$1"'"  { print "GID=" $3       }
21                      '`
22      ;;      *)      fatal "`basename $0`: Unknown group $1"
23      esac
24      
25      
26      echo "\$1 = $1, gid = $GID"

10.14.  Getline:

 1      #!/bin/sh
 2      #  a)   getline
 3      #       getline var
 4      #       getline < file
 5      #       getline var < file
 6      #       cmd | getline
 7      #       cmd | getline var
 8      #
 9      
10      cat << EOF      | gawk '
11      BEGIN   {       do
12                              print ;
13                      while ( ( getline > 0 )  && ( $1 !~ /wo/ ))
14              }
15      #BEGIN  {       while ( ( getline > 0 )  && ( $1 !~ /wo/ ))
16      #                       print ;
17      #       }
18      #BEGIN  {       for ( ; ( getline > 0 ) ; )     {
19      #                       print "copy ", $0 ;
20      #                       if ( $1 ~ /wo/ )
21      #                               break
22      #               }
23      #       }
24              {       print "----", $1, $2    }
25      
26      '
27              zero 0000 00 000 0 0 0 00                       copy
28              one AAA AAAA AAAA AAAA                          copy
29              two BBB BBB BBB BB  BBB                         copy
30              three CC CC CCC CCC CCC CCC
31              four DDD DD DDD DDD DDD DDDD
32              two EEE EEE EEE EEE EEE
33      EOF

10.15.  Getline < file:

 1      #!/bin/sh
 2      #       getline
 3      #       getline var
 4      #   b)  getline < file                  
 5      #       getline var < file
 6      #       cmd | getline
 7      #       cmd | getline var
 8      #
 9      
10      gawk '
11      BEGIN   {       i = 0;
12                      while ( ( getline < "file" > 0 ) &&
13                              ( i++ <= 2             )  )     {
14                              print "====", $0;
15                      }
16              }
17      END     {       print "END"
18                      while ( getline < "file" > 0 )  
19                              print "----", $0;
20              } ' $0                  # Inputfile ist notwendig

10.16.  Command | getline:

 1      #!/bin/sh
 2      #       getline
 3      #       getline var
 4      #       getline < file                  Vorsicht
 5      #       getline var < file
 6      #   c)  cmd | getline
 7      #       cmd | getline var
 8      #
 9      
10      gawk '
11      BEGIN   {       n = 0
12                      while  ("who" | getline > 0 )
13                              n++
14      
15                      print "# user = ", n
16              } '

10.17.  Ausgabe in Dateien:

 1      #!/bin/sh
 2      #       output into files
 3      #
 4      
 5      gawk '
 6      BEGIN   {       print "BIG" > "big"
 7                      print "BIG" >> "big"
 8                      close( "big")
 9                      system("cat big")
10              } '

10.18.  Ausgabe auf stderr:

 1      #!/bin/sh
 2      # Ausgabe auf stderr
 3      #
 4      
 5      gawk '
 6      BEGIN   {       print "Message 1: cat 1>&2" | "cat 1>&2"
 7                      print "system(\"echo ^\"Message: :\"^ 1>&2 \")" 
 8                      system("echo '"Message 2: "' 1>&2 ")
 9                      print "Message: > /dev/tty" > "/dev/tty"
10              } '

10.19.  Ausgabe in Pipes:

 1      #!/bin/sh
 2      #       output into pipe        print | cmd
 3      #
 4      
 5      ypcat passwd | gawk -F: '
 6      BEGIN           {       counter = 0
 7                      }
 8      $4 == 11        { print counter
 9                        if ( counter++ < 5 )
10                              print $1 | "sort -r"
11                        else
12                              exit    # go immediately to
13                                      # the END pattern
14                      }
15      END             { print "END"
16                        close("sort -r")
17                      }
18      '

10.20.  Argumente I:

 1      #!/bin/sh
 2      #       arguments
 3      #
 4      
 5      gawk '
 6      BEGIN   {       print "ARGC = ", ARGC
 7                      for ( i = 0; i < ARGC; i ++ )
 8                              print "ARGV[" i "] = ", ARGV[i]
 9              }
10              {       print "---", $0, "-----"                }
11      
12      '  "$@"

10.21.  Argumente II:

 1      #!/bin/sh
 2      #       arguments
 3      #
 4      
 5      gawk '
 6      BEGIN   {       print "ARGC = ", ARGC
 7                      for ( i = 0; i < ARGC; i ++ )   {
 8                              print "ARGV[" i "] = ", ARGV[i]
 9                              ARGV[i] = ""
10                      }
11              }
12              {       print "---", $0, "-----"                }
13      
14      '  "$@"

10.22.  Argumente III:

 1      #!/bin/sh
 2      #       arguments
 3      #
 4      
 5      gawk '
 6      BEGIN   {       print "ARGC = ", ARGC
 7                      for ( i = 0; i < ARGC; i ++ )   {
 8                              if ( ARGV[i] ~ /^-/ )   {
 9                                      print "Flag: ", ARGV[i]
10                                      if ( ARGV[i] ~ /^-x$/ )         {
11                                              print "-x"
12                                              ARGV[i] = ""
13                                      } else if ( ARGV[i] ~ /^-f$/ )  {
14                                              print "-f"
15                                              print "file: ", ARGV[i+1]
16                                              ARGV[i] = ""
17                                              ARGV[i+1] = ""
18                                      }
19                              } else
20                                      if ( ARGV[i] != "" )
21                                              print "Inputfile: ", ARGV[i]
22      
23                      }
24              
25              }
26              {       print "---", $0, "-----"                }
27      
28      '  "$@"

10.23.  Argumente IV:

 1      #!/bin/sh
 2      #       arguments
 3      #
 4      
 5      gawk '
 6      BEGIN   {       print "ARGC = ", ARGC
 7                      i = 1
 8                      while (  ( i < ARGC ) && ( ARGV[i] != "--" ) )  {
 9                              print i ":", ARGV[i]
10                              if ( ARGV[i] ~ /^-x$/ )         {
11                                      print "-x"
12                                      ARGV[i] = ""
13                              } else if ( ARGV[i] ~ /^-f$/ )  {
14                                      print "-f"
15                                      if ( ( i < ARGC - 1 )    &&
16                                           ( ARGV[i+1] != "--" ) )    {
17                                              print "file: ", ARGV[i+1]
18                                              ARGV[i] = ""
19                                              ARGV[++i] = ""
20                                      } else  {
21                                              print "Missing Argument for -f."
22                                              exit
23                                      }
24                              }
25                              i++
26                      }
27                      if ( ARGV[i] == "--" )
28                              ARGV[i] = ""
29              }
30              {       print "---", $0, "-----"                }
31      
32      '  "$@"

10.24.  Funktionen und skalare Argumente:

 1      #!/bin/sh
 2      #       functions
 3      #
 4      
 5      gawk '
 6                                      # call by value
 7      function max(first, second,     m)      {       
 8              m = first > second ? first : second
 9              first = 0
10              second = 0
11              return m
12      }
13      
14      BEGIN   {       f = 2;
15                      s = 3;
16                      print "max(" f "," s ") = " max(f,second)
17                      print "f = ", f                 
18                      print "s = ", s                 
19              }
20      '

10.25.  Funktionen und Vektoren als Argumente:

 1      #!/bin/sh
 2      #       functions
 3      #
 4      
 5      gawk '
 6                              # call by reference
 7      function max(field,     m)      {               
 8              m = field[0] > field[1] ? field[0] : field[1]
 9              field[0] = 0
10              field[1] = 0
11              return m
12      }
13      
14      BEGIN   {       f[0] = 2;
15                      f[1] = 3;
16                      print "max(" f[0] "," f[1] ") = " max(f)
17                      print "f[0] = ", f[0]                   
18                      print "f[1] = ", f[1]                   
19              }
20      '

10.26.  Pattern:

 1      #!/bin/sh
 2      #       pattern
 3      # ^             begin of string
 4      # .             any character
 5      # [c1c2]        any character in c1c2
 6      # [^c1c2]       any character not in c1c2
 7      # [c1-c2]       any character in range beginning with c1
 8      #               ending with c2
 9      # [^c1-c2]      any character not in range beginning with c1
10      #               ending with c2
11      # r1|r2         any string x matched by r1 or r2
12      # (r1)          any string matched byt r1
13      # (r1)(r2)      any string xy where r matches r1 and
14      #               y matches r2
15      # (r1)*         zero or more
16      # (r1)+         one or more
17      # (r1)?         one or zero
18      #
19      
20      cat << EOF      | gawk '
21      /AFFE|affe/             { print "AFFE oder affe: ", $0      }
22      /M([ae]i|ay)er/         { print "M?ier:          ", $0      }
23      /^[^aeM]/               { print "[^aeM]:         ", $0      }
24      /^[0-9]+\.?[0-9]+/      { print "^[0-9]+\\.?[0-9]+:", $0    }
25      /^[0-9]+\.?[0-9]+$/     { print "^[0-9]+\\.?[0-9]+$:", $0   }
26      /^[0-9]+\.?[0-9]+[ ]*(e[+-]?[ ]*[0-9]+)?$/              \
27                      { print "^[0-9]+\\.?[0-9]+(e[+-]?[0-9]+)?$:",
28                             $0                                   }
29      '
30      affe
31      AFfe
32      AFFE
33      Maier
34      Meier
35      Mayer
36      Maiyer
37      100,20
38      100.20
39      100.20 e10
40      100.20 e-10
41      100.20 e+10
42      EOF


end | end | end | end | Inhalt | Uebungen | Complete | Kommentar


Created by unroff & hp-tools. © by Hans-Peter Bischof. All Rights Reserved (1997).

Last modified 03/July/97