Helmut Schellong, 30.03.2002-28.06.2002

Unix Shell Programming Considered Harmful?

Absicht der bsh-Entwicklung / bsh-Konzept

bsh im Vergleich - Vorteile/Nachteile


Es gibt seit geraumer Zeit einen Artikel namens "Csh Programming Considered Harmful"
Man war nach Betrachtung der Unix-csh der Ansicht, daß das Programmieren damit schädlich ist.

Die csh kam einige Jahre nach der Bourne-Shell (sh) auf die Unix-Systeme und ist seit Jahrzehnten
die zweite der heute drei Unix-Standard-Shells:  sh, csh, ksh.
Sie ist bei interaktivem Arbeiten wegen ihrer history- und alias-Funktionalitäten wesentlich besser als sh,
hat jedoch beim Scripting tatsächlich einige heftige Nachteile und Fehler, die teils schon im Konzept liegen.

Ich habe seit etwa 1989 Scripting-Erfahrungen mit den drei Unix-Standard-Shells und den DOS-Shells gesammelt.
Diese Erfahrungen haben mich 1994 dazu motiviert, eine eigene Shell -diebsh- zu entwickeln (ab 1995),
weil ich letztlich doch reichlich unzufrieden war mit den Möglichkeiten, Leistungen und sonstigen Merkmalen
all dieser Shell-Programme, bei denen ich zudem auch (allerdings nur ein paar) häßliche Fehler entdeckte.

Im unteren Fenster ist eine Datei sichtbar, die einen repräsentativen Querschnitt aus über 5000
gesammelten, thematisch passenden Newsgroup-Artikeln zeigt, bestehend aus etwa 100 Artikeln,
die Probleme bei der Shell-Programmierung aufzeigen und/oder Programmierhilfe erbitten.

Unix Shell Programming Considered Harmful?:
Anhand dieser Artikel ist überdeutlich erkennbar, daß es ein wahrhaft gigantisches,  119,120,121,122
nahezu unentwirrbares Durcheinander gibt, hinsichtlich Unterschieden bei:

Echte Bourne-Shell / Bourne-Shell-Kompatible / So-als-ob-Bourne-Shell
/bin/sh als Link auf ...
Plattform / Betriebssystem
BS-, Shell- und Tool-Versionen / -Varianten
Shell-Extensions per Option
Shell-Tools / Zugehörige Werkzeuge
Spezifische Optionen und Merkmale
Shell- und tool-relevante Manual-Dokumentationen
Shell-Standards / POSIX-Standards
Ich bin jedenfalls zu folgender Erkenntnis gelangt:  Unix Shell Programming Considered Sceptical
Und zwar mindestens das - eigentlich sogar wie bei csh: ... Harmful
Also:  Bedenklich bis Schädlich.

Man kann für die Bourne-Shell große und größte Scripts programmieren, die gut und sicher funktionieren
und zusätzlich portabel sind.
Aber, dies nur, wenn man sich dabei auf den kleinsten gemeinsamen Nenner einigt!
Das bedeutet, es muß so programmiert werden, wie vor 30 Jahren!:

entweder:
Anspruchslos, simpel und klein.
oder aber:
Umständlich, aufgeblasen, unübersichtlich und langwierig.
Oft mit aberwitzigen, krampfhaften Not-Konstruktionen.
Unter Zuhilfenahme sehr vieler externer Werkzeuge.
Mit Erzeugung sehr vieler neuer Prozesse (fork()).
Ressourcenverschlingend und sehr, sehr laufzeitlangsam.
Um diesen Mangel abzustellen, habe ich einen konsequenten Schnitt vorgenommen
und vollkommen frei und unabhängig ein eigenes Shell-Programm (bsh) entwickelt.
Die Zielsetzungen dabei waren: [1]  Es gibt wohl 2 Unix-Systeme, wo /bin/sh eine POSIX-Bourne-Shell ist
und die 'echte' Bourne-Shell -ebenfalls- den Namen bsh hat.
Das konnte ich damals mangels Internetzugang leider nur ungenügend recherchieren.


Die obenstehenden Zielsetzungen wurden alle voll zufriedenstellend erreicht:

Die Größen der statisch gelinkten Executables ca.:   120, 130, 170, 200, 220, 340 KB
(Je nach BS und Variante.)
Das sind angesichts der enormen Anzahl von Buildins vorzügliche Werte.

Die Arbeitsgeschwindigkeit ist im Vergleich extrem gut.
Sie erreicht manchmal sogar das Tempo des kompilierenden(!)  perl-Interpreters [1] (der keine Shell ist).
Nachfolgend wurde die Geschwindigkeit der 'internen Maschine' der Shells gemessen:

Test-Quellen
=========================================================================
 T A B E L L E   über Arbeitstempo und zugehörige Zeitfaktoren
=========================================================================
 bsh    ************************************************************
 ksh    *******************************************
 zsh    *****
 bash   ***
 csh    **
 4dos   *
 sh     -
-------------------------------------------------------------------------
 Script      bsh     ksh     zsh     bash     csh     sh     4dos.com
-------------------------------------------------------------------------
 loop        1       1.11    11.4    19.4     39.0    205.    37.2   u+s
 loop        1       1.07     9.1    19.3     30.6     33.9   27.0   u
 s70         1       1.64    12.9    ----     29.3    265.    52.2   u+s
 s70         1       1.64    12.7    ----     29.1     51.8   47.2   u
-------------------------------------------------------------------------
Oben sind als Besonderheit die sehr schlechten Werte der sh auffallend.
Und zwar der große Unterschied zwischen real- und user-time. (real=user+sys)
Das liegt daran, daß die sh keine eingebaute Arithmetik hat und daher das externe Kommando expr
per Kommando-Substitution verwendet werden mußte.  78
Von dieser Sachlage läßt sich ableiten, daß die bsh vergleichsweise noch viel schneller arbeitet, wenn
ihre Möglichkeiten (die die anderen Shells nicht besitzen) voll ausgenutzt werden:
======================================================================
 Mit Ausnutzung der bsh-Buildins:
======================================================================
 bsh    ************************************************************
 ksh    ***
 zsh    -
 bash   -
 csh    -
 4dos   -
 sh     -        ('-': nicht mehr darstellbar)
----------------------------------------------------------------------


Diverse Geschwindigkeitsmessungen, Kompaktheits- und Syntaxbetrachtungen:
(PIII/700MHz - UnixWare7.1.1)
Nicht-bsh-Code stammt aus einer Shell-Newsgroup, nicht von mir.


[1]  perl ist ein Interpreter, den ich wegen seiner Mächtigkeit und Leistung bewundere.
Seine Syntax ist allerdings oft implizit, knapp und kryptisch. Beispielsweise sind Funktionsaufrufe
innerhalb (!) von Regulären Ausdrücken möglich und üblich.
An anderen Stellen ist die Syntax wiederum recht umständlich - man muß viel schreiben.
Es fehlt eine harmonische Gleichstimmung, ein roter Faden (ähnl. bei 4dos.com), so als ob
der Autor nach jeweils langen Pausen mit immer wieder anderem Syntax-Gefühl entworfen hätte.
Und über perl wird nachhaltig berichtet, daß er Speicher-Lecks erzeugt!
Desweiteren ist er natürlich ressourcen-hungrig.
Ich hatte deshalb nie richtig Lust, mich mit perl intensiver zu beschäftigen.


Ein Vermeiden von C-Programmierung (und ähnl.) ist mit Hilfe der bsh sehr weitgehend realisierbar.
Das spart Zeit, senkt die Fehlerwahrscheinlichkeit und bringt Übersicht und leichte Wartbarkeit.
Mit anderen Shells ist das kaum oder -fast immer- gar nicht machbar!

Die unten gezeigte bsh-Funktion ist ein Grenzfall, ein Scheidepunkt.
Bei intensivem Einsatz sollte sie nicht mehr als bsh-Funktion verwendet werden, denn sie benötigt
etwa 0.3 Sekunden pro KByte Datenmenge.
Dennoch ist das relativ schnell, da jedes Bit aufwendig einzeln bearbeitet wird.
 
# SLE<--  Buf $offs $len
Sle16()  {
   local n=00 sle_=000000 word=000000
   for word in $( catv $3,$4,$2 | base -w +10 )
   do
      for n from 0 to 15 repeat
      do
         if let "{{sle_&16#8000} ^ {[word&(1<<n)]<<(15-n)}}&16#ffff"
         then
            ((
sle_=  ((((sle_^2064)&2064)<<1)|(sle_<<1))
                     &((((sle_^2064)&2064)<<1)|~(2064<<1)),
               sle_&=16#ffff, sle_|=1
           
))
         else
            ((
sle_=  ((((sle_^~2064)&2064)<<1)|(sle_<<1))
                     &((((sle_^~2064)&2064)<<1)|~(2064<<1)),
               sle_&=~1, sle_&=16#ffff
           
))
         fi
      done
   done
   $1=$sle_
   return 0
}

Gvim-Datei zur bsh
 

Die Größe von Scripten und Syntax-Konstruktionen darf vom bsh-Konzept her beliebig sein!
Das Limit kommt vom Betriebs-/Datei-System, nicht von der bsh.
bsh64 UnixWare7/OpenUnix8 hat hier ein Limit von 2^63-1 Byte.


Syntax-Portabilität und Plattform-Portabilität:
Ich habe bsh auch auf ein Embedded-Multitasking-DOS auf Basis 80186-Prozessor 20 MHz portiert.
Es handelt sich um den Beck@IPC SC12-Chip, kleiner als eine Streichholzschachtel,
mit Speicher-Ressourcen, die jeweils 'nur' bei etwa 200 KB netto liegen.
Es wurden dazu die Kommandos com,ext,sem und eine eigene Handle-Verwaltung zusätzlich implementiert.
bsh.exe wurde auf etwa 50 KB komprimiert, mit einem Exe-Packer ähnlich PKLITE.

Realisiert wurde ein Verbindungsmanager, der per Modem und Telefonnetz mehrere Hundert elektronische
Einheiten verwalten, steuern, überwachen und konfigurieren kann, von beliebigen Browser-Arbeitsplätzen aus.
Von den Anforderungen an das fertige Produkt her war dieses Projekt überhaupt nur mit bsh realisierbar!

Ein weiterer Verbindungsmanager wurde realisiert, der eine mehrere tausendfach größere Datenbasis unterhält
und daher unter Unix mit Apache-WebServer und einer Unix-bsh realisiert wurde.
Dabei konnten große Script-Teile zu 97-100% unverändert übernommen werden!

Und auf meinem gemieteten WebSpace läuft ebenfalls bsh -unter Linux- als alleiniger CGI-Interpreter
seit Jahren und bedient mein 1000-fach-WebCounter-System.  Neuerdings auch mit Referrer-Listing.

bsh ist, wie sie ist, war immer so, wie sie ist, wird immer so bleiben, wie sie ist.
(Ausnahme: neue, hinzugefügte Merkmale, die die alten nicht ändern.)
Ein schädliches Mammut-Durcheinander, wie oben angesprochen, gibt es nicht und wird es nie geben.


Interne (vorteilhafte) Konzepte der bsh: Die anderen Shells sind hinsichtlich der oben genannten Punkte -nachteilig- anders, oft gegensätzlich.
Das hat dazu geführt, daß ich mit anderen Shells nur noch so einfach programmiere, wie man es in etwa
mit command.com unter DOS machen kann.  Alles Anspruchsvollere erledige ich mit bsh.


Unterschiede:  Vorteile / Nachteile:
  1. In anderen Shells sind Variablen in Funktionen implizit lokal.
    In bsh nur explizit per local oder static.
  2. In anderen Shells sind Variablen in Funktionen in allen darin
    (verschachtelt) aufgerufenen Funktionen ebenfalls sichtbar!
    In bsh sind sie (per local) wirklich lokal, ansonsten global.
  3. $1=$serial
    ist in anderen Shells nur per teurem eval möglich.
    In bsh kann man damit ganz einfach Funktionsergebnisse in globalen Variablen übermitteln,
    ohne das sehr teure  var=$(foo)  verwenden zu müssen.
    ksh: a=b
    ksh: b=bbb
    ksh: $a=bbb
    b=bbb: not found
    ksh: _
  4. In bsh werden einheitlich in allen ungequoteten Bereichen alle Zwischenraumzeichen entfernt
    und zwischen Worten durch ein Leerzeichen bzw. das erste Zeichen in IFS ersetzt.
    In anderen Shells geht das kunterbunt durcheinander, mit allerlei Mischformen und überraschenden
    Nichtunterschieden zwischen gequotet und ungequotet.
  5. In bsh besteht zwischen folgenden Ausdrücken ein definierter Unterschied:
    "$(...)"
    "$(-...)"
    $(...)
    In anderen Shells hat man diese Wahl nicht.
  6. Quoting in bsh wirkt stets nur auf der Ebene des Quoting-Beginns.
    Eventuelles Quoting in einer verschachtelten Ebene bleibt unbeeinflußt:
    ab="$( cmd "efta  -wr" "$Klb" && let ++K; srb -s )"
    [ K -gt 4 ] && srb -t "$ab"
  7. In bsh kann ein ' innerhalb von '' definiert werden:  '---''---'.   104
    Das geht in anderen Shells nicht.
    Die konkrete Praxis lehrt aber, daß das z.B. bei der Produktion von HTML-Inhalten wichtig ist.
    Ich weiß, ich weiß: "Man will mit Shells keine HTML-Inhalte produzieren!" - Wirklich? Tatsächlich?
    Die Wahrheit ist:  Man kann nur mit bsh vernünftig HTML-Inhalte produzieren.
    Ich weiß, ich weiß: "Man will mit Shells keine binären Inhalte verarbeiten!" - Tatsächlich?
    usw. usw. usw. usw. usw. usw. usf.  wikiservice.at/dse/wiki.cgi?ReligionsKriege .
  8. cmda && cmdb || cmdc
    In anderen Shells wird bei false-Exit von cmda das cmdb nicht ausgeführt, aber cmdc wird ausgeführt.
    In bsh gilt hier KO-Logik, wie in C:  Beim ersten Nichtpassen wird die ganze Kette abgebrochen.
  9. Pipes  |  arbeiten per Default mit Dateien, wahlweise mit pipe().
    Das hat auch auch Nachteile; die Vorteile überwiegen allerdings. (s.u. bsh-Manual)   116
  10. bsh kann -absichtlich- keine Vorrausschau-Umlenkungen (|<>) nach Blockkonstruktionen
    (wie { }, Schleifen u. dergl.) verarbeiten.
    Das würde bei jedem Vorkommnis ein pauschales zweimaliges Parsen (inaktiv+aktiv) erfordern.
    Außerdem können solche Blöcke sehr groß -beliebig groß- sein, so daß man dauernd an's Ende
    zum Nachschauen gehen müßte, um die Vorgänge im Block richtig zu verstehen.
    Die Bourne-Shell erzeugt hier -nachteilige- Subprozesse, woran die Problematik sichtbar wird.
    bsh ist ein 100%ig reiner, linearer Interpreter, woran ich auch nichts ändern will.
  11. Für bsh können verschachtelte und bedingte Funktionsdefinitionen programmiert werden.
    Keine andere Shell bietet dies.  csh und ganz alte sh-Versionen können gar keine Funktionen.
  12. bsh hat ein goto-Kommando.  Wegen allgemeiner Abneigung jedoch:
    Die Anzahl der Sprungziele pro lokaler Ebene wurde absichtlich begrenzt.
  13. Arrays in bsh sind keine separaten Datentypobjekte.  Es werden normale Variablen erzeugt,
    und  a23=aaa; echo ${a[23]} "$a[23]"  funktioniert (daher) ebenfalls.
    Das ist sicher nicht ideal, aber es erhält Kompaktheit und Geschwindigkeit und stellt in der
    realen Praxis kein Problem dar - wenn man's weiß.
    Arrays werden zudem unterstützt durch die außerordentlichen Binär-Möglichkeiten der bsh.


Newsgroup-Artikelsammlung (im unteren Frame).
Bemerkungen und bsh-Lösungen dazu:
(bsh-Manual)
Datum, Uhrzeit, Dateizeit, Dateigröße:  A  B  C  D  E  F  G  H  I
systime sec; echo $sec      # dezimal
systime -t dati             # JJJJMMTTssmm.ss
systime $sec                # Setzen
ctime J M T st mi se $sec   # Einzeln
tz -1                       # Zeitzone
fstat -smv size mtime $File
fstat -mvT mTime $File      # JJJJMMTTssmm.ss
fstat -shv size $Handle
fstat +m $sec $File         # Setzen
# usw. usw. usw.
Datei-Listings:  A  B
list  [-fdoecpRF0]  dir file ...
Die Optionen -fdo: file directory other
gestatten eine unmittelbare Selektion.
Option -F gestattet eine Selektion aus einem Wildcard-Resultat.
Eine störende Blocksumme und . .. werden nicht ausgegeben.
Option -0: '\0' als Zeilenende! (siehe auch readl)
fstat [-|+FitfplugJNsamcnhTv] [name...|wert...] file...|handle...
prints [[v]format[-]] [vname] [arg ...]
ermöglichen ein beliebig ausführliches (formatiertes) Listing.
sleep:  A  B
sleep $sec
sleep -m $millisec
Globale Handle-Verknüpfungen:  A  B  C  D  E
1.)  exec ist (als Krücke) nicht notwendig.
2.)  Eine vorherige Duplizierung ist nicht notwendig.
3.)  Rückverknüpfung ist einfacher.
In der bsh ganz einfach so:
< file
...
><
Auch verschachtelt; das auch mit ein und demselben Handle.
Zeilenorientierte Reguläre Ausdrücke:  A  B  C  D
bsh-BREs arbeiten mit einem terminierenden NULL-Zeichen;
Sonderzeichen sind per %t %r %n %z definierbar.
Das $ schaut über eine Folge \n bzw. \r\n hinweg.
Alle Zeichen 1...255 sind testbar.
[Zusammenarbeit von readl und expr]
Dateinamen, die Zeilenvorschübe enthalten:  manchmal ein Problem!
sind identifizierbar und verarbeitbar!:
# list -p ../u/../u
../u/../u/cie
../u/../u/bsh
# _
Der redundante Vorsatz ../u/../u fehlt zu Anfang, wenn ein \n
in einem vorher gelisteten Dateinamen vorkam.
Noch besser ist Option -0 bei list und readl.
Dabei wird mit dem '\0'-Zeichen als Zeilenende gearbeitet.
Somit sind in Dateinamen alle Zeichen 1..255 verarbeitbar!
Exit in Pipelines:  A
cmd1 | e=$? cmd2 | { cmd3; [ e -eq 0 ]; } || echo cmd1_fail
GetIDs mt $1 "$lz" | { nop && pop3 - $IP "$User" "$Pass"; }
eval-Kommando:  A  B
gibt es auch in bsh, man braucht es aber in bsh kaum,
weil es massenhaft andere 'preiswertere' Möglichkeiten gibt.
Upper case / Lower case:  A
conv -u name1 name2 ...     # arbeitet binär
catv name | tr '[a-z]' '[A-Z]' | catv =name:
expr "$var" ...... + '%U%1%L%2%E&'
# usw. usw.
Externes dd in anderen Shells:  A  B
bsh:  catv 1,0 =key:

catv 792,$((178*64*1024)),0 =1 <infile >extract.dat
Dateinamen manipulieren:  A
conv '-t _' datei1 datei2 ...
Zahlenbasis:  A  B
echo $((16#4f))
79
echo $((16#, 79))
4f
base -10 dez +16 hex
echo $dez : $hex
catv $offs,1,0 <$file | base -b +16 hex     # binär, -b: byte
typeset
# usw. usw. usw.
Manipulation Variableninhalt:  A  B  C
echo $tnam
test01082001_0711.jpg
catv  6,2,tnam  4,2,tnam  8,,tnam   =0,,tnam:
echo $tnam
08012001_0711.jpg
var="12.45-"
expr "$var" :var '^%([0-9.]%{1,}%)-$'  '-%1'
echo $var
-12.45
Veränderung von Dateizeilen:  A  B
< datei
while readl line
do
   catv '.#' line '/ -1000%n'
done
><

23ste Zeile löschen:
line +23-23 datei | cat >datei

line  [ {-|+}a[-[e]] ] [ datei ... ]
Pipes und stderr:  A  B
print -u2 abc |2  catv =2
Tja, so einfach ist das mit bsh.
Führende Nullen:  A
n=1
prints sf03 $n
001
prints vsf05 n $n
echo $n
00001

prints [[v]format[-]] [vname] [arg ...]
format:  [v]s[.][u[d]][-:][{ +}][fx|Fddd][feldlänge][{btn}]...
Indirekte Zugriffe:  A
Message()  { local ca=variable$1; command ${{ca}; }
Message 1
Message 2
Dateiinhalt binär --> Variable:  A
catv $doffs,$max,0  =$voffs,,var:  <datei
Variablenleere, -existenz und -test:  A
ifset VAR ...
ifdef VAR ...
usw. usw.
a='='
[ = = $a ] && echo gleich
[ -z $a ] && nop
So etwas ist in bsh kein Fehler!

Nur erste Fundzeile mit grep + Broken Pipe: A B

grep -m1 ...
grep -m4 ...

Broken Pipe (SIGPIPE):
Dieses Problem kann es in bsh nicht geben.

Verschachteltes Sortieren:  A

sortl -f2,1n -odatei datei

Mittels -r und -f2,1rn kann verschachtelt 'reverse'
sortiert werden.

350000 Symlinks schneller herstellen:  A

Das bsh-interne 'link -s ...' ist schon mal etwa 10-fach schneller.

Durcheinander bei impliziten Shell-Subprozessen:  A

In bsh gibt es gar keine impliziten Subshells!

Diverse Shell-Probleme - kein Problem für bsh: A B C D

readl Zeile [Z2 ...]

fstat +s ... Datei um 1 kürzen.
catv :-1,1,0 =C: <datei Eventuell vorher prüfen.

echo $((2910865710+2910865710))
5821731420 Mit bsh64-Varianten kein Problem.

Null-Zeichen in Variablen sind für bsh absolut gar kein Problem.
Es gibt da soviel Möglichkeiten, daß ich hier nichts extra nenne.

Logfile-Problem: A

<logfile
while readl Z
do
expr "$Z" :p '%<proto=%([^ ]%{1,}%)' || continue
expr "$Z" :r '%<rcvd=%([0-9]%{1,}%)' || continue
expr "$Z" :s '%<sent=%([0-9]%{1,}%)' || continue
conv -u p
expr "$P" :: "[rs]_$p" || P="$P r_$p s_$p"
let "r_$p+=r" "s_$p+=s"
done
><
for pname in $P; do echo $pname: ${{pname}}; done

Mit bsh sind solche Algorithmen plötzlich ganz einfach und übersichtlich;
und: 0 Subprozesse, 0 falscher Namespace, 10..50mal schneller.