Unterabschnitte

Scripting

Damit auch Shell-Skripte mit einer interaktiven CUI Benutzerschnittstelle versehen werden können, wurde die libCUI um die Funktion einer Shell-API erweitert. Dabei wurde bewusst auf die Einbindung einer Skript-Sprache wie Perl oder Python verzichtet, da damit die Integration in ein Basissystem, wie das von eisfair, problematisch wäre. Statt dessen stehen die Funktionen direkt für die Bash zur Verfügung, die als Hintergrundprozess von einem Frontend-Programm ausgeführt wird.

Die Programmierung entspricht in weiten Teilen der CUI-Programmierung in der Sprache C. Darum ist es in jedem Fall ratsam, das Kapitel ''Elementare Fenstertechnik'' gelesen zu haben. Zudem wird hier lediglich eine prinzipielle Beschreibung der Einbindung von Shell-Skripten geliefert. Eine Refenrez aller API-Funktionen befindet sich im Anhang A.

Funktionsprinzip

Damit ein Shell-Skript eine CUI-Oberfläche benutzen kann, muss es als Hintergrundprozess von einem Frontend-Programm ausgeführt werden. Das Frontend für allgemeine Skripte heißt ''shellrun.cui'', aber auch der Konfigurationseditor, ''edit-conf.cui'', kann Skripte als Co-Prozess ausführen.

Im Falle von ''shellrun.cui'' wird das jeweilige Skript dem Frontend-Programm als Argument übergeben. Man kann damit z.B. Curses-Menüs zur Auswahl von Funktionen o.ä. realisieren, wie sie häufig Paketentwicklung vorkommen. Man kann aber auch Oberflächen schreiben, mit denen der Anwender seine Daten pflegen oder Dateien auswählen kann. Generell ist hier nahezu alles denkbar, was mit einem Shell-Skript machbar ist.

Auch der Konfigurationseditor (allgemein als ECE bezeichnet) verfügt über die Möglichkeit Shell-Skripte auszuführen. Dies dient dazu, die Eingabezeile im ECE gegen einen Dialog zu ersetzen, der z.B. eine Auswahlliste für Werte anbietet oder die Benutzereingabe auf andere Weise erleichtert. Welches Skript aufgerufen werden soll ermittelt das Programm dabei aus der Paketkonfiguration.

Das Frontend-Programm erzeugt nach dem Programmstart zwei Named-Pipes für die Client-Server Kommunikation ($HOME/.cui/<proc-id>rp und $HOME/.cui/<proc-id>wp) und erwartet von dem Shell-Skript, dass dieses die Pipes öffnet und eine Verbindung initiiert. Dazu gibt es bereits vorbereitete Funktionen die in das Shell-Skript auf einfache Weise eingebunden werden können.

Ist die Verbindung einmal hergestellt, dann fungiert das Frontend als eine Art Window-Manager (Server), der vom Shell-Skript (Client) API-Befehle z.B. zur Konstruktion von Fenstern entgegen nimmt. Letzteres wiederum wird vom Frontend mit Informationen über Benutzereingaben in Form von Call-Back Routinen benachrichtigt.

Der Programmablauf in der Shell erfolgt nicht mehr, wie gewohnt, in linearer Form, sondern ist Ereignisorientiert, wie man das von der Programmierung graphischer Oberflächen her kennt. Diese Vorgehensweise ist für einen routinierten Skript-Programmierer anfänglich sicher etwas ungewohnt.

Der grundlegende Aufbau eines CUI-fähigen Shell-Skripts sieht wie folgt aus:

    #!/bin/sh

    . /var/install/include/cuilib
    [ evtl. weitere includes ]

    [ Skript-Funktionen ]

    cui_init
    cui_run

    exit 0

Die vollständige Funktionalität des Skriptes steckt in Funktionen, die vom Frontend aufgerufen werden. Der Programmfluss läuft linear bis zu der Funktion ''cui_run'' durch und bleibt dort ein einer Schleife hängen, die ständig die Eingabe-Pipe überwacht. Trifft dort ein Funktionsaufruf vom Frontend ein (Ereignis), dann wird eine Funktion unter dem entsprechenden Namen aufgerufen. Diese sollte natürlich unbedingt im Skript existieren! Da bis auf die Einstiegsfunktioin alle Ereignisroutinen vom Shell-Programmierer explizit beim Frontend angemeldet werden, ist das aber kein Problem. Wichtig ist jedoch, dass die Funktionen oberhalb von ''cui_run'' und nach dem Source-Include der cuilib implementiert sind.

Welche Funktion vom Frontend zuerst aufgerufen wird (Einstiegsfunktion), ist vom jeweiligen Programm abhängig. Bei shellrun.cui heißt sie ''init'' beim ECE werden nacheinander die Funktionen ''setdata'', ''exec_dialog'' und ''getdata'' aufgerufen. In jedem Fall beendet sich die Warteschleife in ''cui_run'', wenn die Funktion ''exit'' aufgerufen wird (die nicht implementiert sein muss / darf).

Eine Funktion, die vom Frontend aufgerufen wurde, muss unbedingt mit ''cui_return'' beendet werden. Anderenfalls wartet das Frontend vergeblich auf eine Rückmeldung, was ein beliebter Fehler ist, nach dem man beliebig lange suchen kann. Die übergabeparameter vom Frontend an das Skript sind in den Variablen $p2..$pn gespeichert (das ist für die jeweilige Funktion fest definiert). Der Rückgabewert wird an ''cui_return'' übergeben.

Innerhalb der Funktionen, die vom Frontend aus aufgerufen werden, kann das Skript wiederum API-Funktionen des Frontends nutzen. Diese sind in der Referenz im Anhang aufgeführt und dienen zum Anlegen und Steuern von Kontrollelementen und vielem mehr.

Der Rückgabewert der API-Script-Funktion ($?) zeigt an, ob der Aufruf erfolgreich war oder ob während der Verarbeitung etwas schiefgegangen ist. Bei den meisten Funktionen kann man ihn ignorieren. Die Rückgabewerte des Frontends wiederum sind wiederum in den Variablen $p2..$pn zu finden und für die jeweilige API-Funktion genau definiert.

Beispiel:

    local ctrl="0"

    cui_window_getctrl "$win" "$IDC_COLORS"
    if [ "$p2" != "0" ]
    then
        ctrl="$p2"     # gefunden!
    fi

sucht das Kindfenster von $win mit der ID $IDC_COLORS. Der Rückgabewert in $p2 ist das Window-Handle des Kindfensters oder 0, falls dieses nicht gefunden wurde.

Wichtig ist, dass man in Funktionen, wann immer es möglich ist, mit lokalen Variablen arbeitet. Dafür kennt die bash das Schlüsselwort ''local''. Anderenfalls riskiert man, dass die gleichlautende Variable in einer anderen Funktion unbemerkt überschrieben wird.

Ablaufverfolgung

Die Ausgabe von Text über die Standardausgabe ist unter dem Curses-Frontend (z.B. mit echo) nicht mehr möglich. Für Debug Zwecke gibt es jedoch den Kommandozeilenschalter --debug über den sowohl die Frontend-Backend Kommunikation, als auch die Standardausgabe in die Datei /tmp/outcui.log ausgegeben wird. Mit

less +F /tmp/outcui.log

erhält man eine Art Trace-Output, über den der Programmablauf in Echtzeit verfolgt werden kann.

Der Trace ist vor allem immer dann interessant, wenn man verfolgen möchte, welche API-Funktionen und Callbacks in welcher Reihenfolge aufgerufen werden, da auf diese Weise sichtbar wird wo sich ggf. die Kommunikation aufgehängt oder das Backend-Skript beendet hat.

Ein Ausschnitt aus einem typischen Trace sieht beispielsweise wie folgt aus:

-> H    init
<- C    1               0       0       0       0       45058
-> R    0       134624472
<- C    32      134624472       DESKTOP
-> R    0
<- C    40      134624472       Eisfair User Manager
-> R    0
<- C    44      134624472       Commands: F10=Exit
-> R    0
<- C    45      134624472       V1.0.0
-> R    0
...
<- H    1

In der ersten Spalte steht in welche Richtung die Daten geflossen sind und um welche Art von Aufruf es sich handelt. Dies entspricht der folgenden Tabelle:

Aufruf Bedeutung
-> H Der Server Ruft eine Hook-Funktion auf. Der Name der Funktion steht im ersten Parameter.
<- H Der Client beendet eine Hook-Funktion mit ''cui_return''. Der Rückgabewert steht im ersten Parameter.
<- C Der Client ruft eine API-Funktion auf. Die Nummer der Funktion steht im ersten Parameter.
-> R Das Frontend beendet eine API-Funktion. Der erste Parameter ist bei Erfolg immer 0, dann folgen die Rückgabewerte.

API-Aufrufe werden durch ihre Nummer repräsentiert. Die entsprechenden Nummern können in der Datei ''/var/install/include/cuilib'' nachgelesen werden. Für das Beispiel oben finden sich z.B. die folgenden Definitionen in der Datei:

API_WINDOWNEW = 1
API_WINDOWCOLSCHEME = 32
API_WINDOWSETTEXT = 40
API_WINDOWSETLSTATUSTEXT = 44
API_WINDOWSETRSTATUSTEXT = 45

Scriptfähige Anwendungen und deren API

shellrun.cui

Mit ''shellrun.cui'' lassen sich eigenständige interaktive CUI Skript-Programme realisieren. Die Einstiegsfunktion die ''shellrun.cui'' im Skript-Programm zwingend voraussetzt heißt ''init()''. Alles weitere hängt davon ab, wie die Anwendung in der Initialisierungsfunktion eingerichtet wurde.

Das wohl einfachste Programm für ''shellrun.cui'' sieht folgendermaßen aus:

#!/bin/sh

. /var/install/include/cuilib

#-----------------------------------------------------------------
# init routine (entry point of all shellrun.cui based programs)
#    $p2 --> desktop window handle
#-----------------------------------------------------------------

init()
{
    cui_message "$p2" "Hallo Welt" "Hallo" "$MB_OK"
    cui_return 0
}

#-----------------------------------------------------------------
# main routines (always at the bottom of the file)
#-----------------------------------------------------------------

cui_init
cui_run

exit 0

Im vorliegenden Fall wird in der init-Routine lediglich ein Mitteilungsfenster angezeigt. Anschließend wird die Routine beendet, ohne dass weitere Fenster erzeugt werden. Gibt es nach Beenden von ''init()'' kein aktives Fenster mehr, dann beendet sich ''shellrun.cui'' automatisch - wurden dagegen Fenster angelegt, dann läuft das Programm weiter, bis die Funktion ''cui_window_quit'' explizit aufgerufen wird.

Der Aufruf von ''cui_message'' öffnet ein modales Mitteilungsfenster und zeigt darin den angegebenen Text an. Der Programmfluss bleibt bei ''cui_message'' stehen, bis der Anwender das Fenster mit OK schließst. Die Definition der API-Funktion ''cui_message'' kann der Referenz entnommen werden.

Wurde das Programm unter dem Namen ''sayhallo.sh'' abgespeichert, dann gibt es zwei Möglichkeiten es aufzurufen - direkt und indirekt:

eis # /var/install/bin/shellrun.cui ./sayhallo.sh

oder

eis #./sayhallo.sh

im zweiten Fall sorgt das Skript selbst für den Aufruf von ''shellrun.cui''.

Ein für ''shellrun.cui'' erstelltes Skript-Programm kann selbstverständlich um einiges komplexer sein als das hier gezeigte Beispiel. Für eine weiterführende Beschreibung wird an anderer Stelle ein Tutorial entstehen.

edit-conf.cui

Der Konfigurationseditor ''edit-conf.cui'' zeigt im Normalfall einen Dialog mit einer einfachen Eingabezeile um einen Wert in der Konfiguration zu bearbeiten. Dieses Verhalten läßt sich mit einem Shell-Skript ändern, das im Verzeichnis ''/var/install/dialog.d'' abgelegt wird. Man kann auf diese Weise Dialoge schreiben, die beispielsweise Auswahllisten anzeigen oder komplexe Werte aus mehreren Eingabeelementen zusammensetzen.

Soll eine Konfigurationsvariable bearbeitet werden, dann sucht der Editor im Verzeichnis ''/var/install/dialog.d'', ob dort eine Datei liegt, die sich einer Prüfregel der Variablen aus der Datei ''/etc/check.d/$package'' zuordnen läßt. Wird eine solche Datei gefunden, dann wird sie als Shell-Skript ausgeführt.

Beispiel:

Eine Zeile in der Datei ''/etc/check.d/foo''...

FOO_VALUE       -           -         FOO_EDIT

...veranlasst den Editor im Verzeichnis ''/var/install/dialog.d'' nach einer Datei mit dem Namen ''FOO_EDIT.sh'' zu suchen. Ist sie vorhanden, dann wird sie als Backend-Skript ausgeführt. Anderenfalls wird der Standarddialog verwendet.

Das Programm ''edit-conf.cui'' erwartet drei Einstiegsfunktionen im Skriptprogramm: ''setdata()'', ''exec_dialog()'' und ''getdata()''. Zunächst wird die Funktion ''setdata()'' aufgerufen, über die dem Skript der aktuelle Wert der zu ändernden Variablen mitgeteilt wird. Anschließend führt ''edit-conf.cui'' die Funktion ''exec_dialog()'' aus, die den Dialog zur Bearbeitung des Wertes anzeigt. Wird ''exec_dialog()'' mit dem Rückgabewert ''$IDOK'' beendet, dann wird der geänderte Wert über ''getdata()'' abgerufen, geprüft und der Variablen zugewiesen. Ein Rückgabewert von ''$IDCANCEL'' dagegen führt dazu, dass die Bearbeitung ohne Zuweisung abgebrochen wird.

Ein einfaches (und zugleich völlig sinnloses) Beispielprogramm sieht wie folgt aus:

#!/bin/sh

. /var/install/include/cuilib

value=""

#-----------------------------------------------------------------
# setdata routine (set value to be modified)
#    $p2 --> value
#-----------------------------------------------------------------

setdata()
{
    value="$p2"
    cui_return 1
}

#-----------------------------------------------------------------
# getdata routine (return modified value)
#-----------------------------------------------------------------

getdata()
{
    cui_return "$value"
}

#-----------------------------------------------------------------
# exec_dialog routine (show edit dialog)
#    $p2 --> parent window handle
#    $p3 --> name of config variable
#-----------------------------------------------------------------
exec_dialog()
{
    local dlg="$p2"

    cui_message "$dlg" "Wert wirklich "andern?" "Frage" "$MB_YESNO"
    if [ "$p2" == "$IDYES" ]
    then
        value="ge"andert"
        cui_return "$IDOK"
    else
        cui_return "$IDCANCEL"
    fi
}

#-----------------------------------------------------------------
# main routines (always at the bottom of the file)
#-----------------------------------------------------------------

cui_init
cui_run

exit 0

Die Programmierung von Dialogen für den ECE läßt sich vereinfachen, indem die Include-Datei ''/var/install/include/ecelib'' in das Skript eingebunden wird. Sie implementiert die Funktionen ''getdata()'' und ''setdata()'' und bietet ein paar Standarddialoge für Auswahllisten und ähnliche Standardaufgaben.

ece_select_list_dlg

Der in der ecelib definierte Dialog ''ece_select_list_dlg'' implementiert eine einfache Auswahlliste, die zentriert über dem Editorfenster angezeigt wird. Der Anwender kann dabei aus einer Reihe möglicher Werte wählen und den gewünschten Wert (z.B. mit Enter) bestätigen. Die Auswahl wird dann in die bearbeitete Konfigurationsvariable des ECE übertragen.

Eine mögliche Verwendung des Dialogs ist im folgenden Listing dargestellt:

#!/bin/sh

. /var/install/include/cuilib
. /var/install/include/ecelib

#-----------------------------------------------------------------
# exec_dailog
# ece --> request to create and execute dialog
#         $p2 --> main window handle
#         $p3 --> name of config variable
#-----------------------------------------------------------------
exec_dialog()
{
    local win="$p2"

    sellist="BLACK,RED,GREEN,BROWN,BLUE,MAGENTA,CYAN,LIGHTGRAY,"
             DARKGRAY,LIGHTRED,LIGHTGREEN,YELLOW,LIGHTBLUE,
             LIGHTMAGENTA,LIGHTCYAN,WHITE"

    ece_select_list_dlg "$win" "Colors" "$sellist"
}

#-----------------------------------------------------------------
# main routine
#-----------------------------------------------------------------

cui_init
cui_run

#-----------------------------------------------------------------
# end
#-----------------------------------------------------------------

exit 0

Bei diesem Beispiel wird folgende Auswahlliste ausgegeben:

+----------[ Colors ]-----------+
|                               |
| +-------------------------+^| |
| | BLACK                   | | |
| | RED                     | | |
| |<GREEN =================>| | |
| | BROWN                   | | |
| | BLUE                    | | |
| | MAGENTA                 | | |
| | CYAN                    | | |
| +-------------------------+v+ |
|     [< OK >] [ Cancel ]       |
|                               |
+-------------------------------+

Zu beachten ist, dass die Funktion ''ece_select_list_dlg'' die Behandlung des Rückgabewertes an den ECE übernimmt. Deshalb ist der Aufruf von ''cui_return'' in der Funktion ''exec_dialog'' nicht mehr nötig und auch nicht mehr erlaubt.

ece_comment_list_dlg

Der Dialog ''ece_comment_list_dlg'' ist eine Erweiterung des ''ece_select_list_dlg'' Dialogs. Hier ist es möglich neben den Auswahloptionen Kommentare anzugeben, die gemeinsam mit den Auswahlwerten in einer zweispaltigen Auswahlliste dargestellt werden. Auf diese Weise sieht der Anwender nicht nur die Optionen (die für sich gesehen u.U. vergleichsweise nichtssagend sind), sondern auch einen erklärenden Text, der die Auswahl erleichtert.

Ein Beispiel ist im folgenden Listing dargestellt:

#!/bin/sh

. /var/install/include/cuilib
. /var/install/include/ecelib

#-----------------------------------------------------------------
# exec_dailog
# ece --> request to create and execute dialog
#         $p2 --> main window handle
#         $p3 --> name of config variable
#-----------------------------------------------------------------
exec_dialog()
{
    local win="$p2"

    sellist="option1|Dies ist Option1,option2|Dies ist Option2,
             option3|Dies ist Option3,option4|Dies ist Option4"

    ece_comment_list_dlg "$win" "Optionen" "$sellist"
}

#-----------------------------------------------------------------
# main routine
#-----------------------------------------------------------------

cui_init
cui_run

#-----------------------------------------------------------------
# end
#-----------------------------------------------------------------

exit 0

Bei diesem Beispiel wird folgende Auswahlliste ausgegeben:

+-------------[ Optionen ]----------------+
|                                         |
| +-----------------------------------+^| |
| | option1    | Dies ist Option1     | | |
| | option2    | Dies ist Option2     | | |
| |<option3 ===| Dies ist Option3 ===>| | |
| | option4    | Dies ist Option4     | | |
| +-----------------------------------+v+ |
|          [< OK >] [ Cancel ]            |
|                                         |
+-----------------------------------------+

Wie schon beim vorhergehenden Beispiel darf die Funktion ''exec_dialog'' auch hier nicht mit einem Aufruf von ''cui_return'' abgeschlossen werden, da dies bereits im Dialog selbst erfolgt ist.

ece_select_cblist_dlg

Der in der ecelib definierte Dialog ''ece_select_cblist_dlg'' implementiert eine Auswahlliste unter Nutzung von Check Boxen, die zentriert über dem Editorfenster angezeigt wird. Der Anwender kann dabei einer Reihe möglicher Werte unter Nutzung der Cursortasten ansteuern und über die Space Taste auswählen. Ausgewählte Werte werden mit [x] markiert. Nicht ausgewählte Werte haben die Markierung [ ]. Die gewünschten Werte können (z.B. mit Enter) bestätigen werden. Die Auswahl wird dann in die bearbeitete Konfigurationsvariable des ECE übertragen.

Beispiel für die Verwendung des Dialogs ece_select_cblist_dialog :

#!/bin/sh

. /var/install/include/cuilib
. /var/install/include/ecelib

#-----------------------------------------------------------------
# exec_dailog
# ece --> request to create and execute dialog
#         $p2 --> main window handle
#         $p3 --> name of config variable
#-----------------------------------------------------------------
exec_dialog()
{
  local win="$p2"

  sellist='apache2,ldapserver,mail,mini_httpd,partimg,pure-ftpd,ssmtp'

  ece_select_cblist_dlg "$win" \
      "Select one or more elements from a list" "$sellist"
}

#-----------------------------------------------------------------
# main routine
#-----------------------------------------------------------------

cui_init
cui_run

#-----------------------------------------------------------------
# end
#-----------------------------------------------------------------

exit 0

Bei diesem Beispiel wird folgende Auswahlliste ausgegeben:

+-[ Select one or more elements from a list ]-+
|                                             |
|  [x] apache2                                |
|  [ ] ldapserver                             |
|  [ ] mail                                   |
|  [ ] mini_httpd                             |
|  [ ] partimg                                |
|  [x] pure-ftpd                              |
|  [ ] ssmtp                                  |
|                                             |
|           [<  OK  >]  [ Cancel ]            |
|                                             |
+---------------------------------------------+

Für maximal 15 Elemente ist Platz auf einem 80/25 Display, daher wird bei mehr als 15 Elemente eine Fehlermeldung erzeugt:

+----[ Checkbox selection ]----+
|                              |
|  Too many elements (16/15).  |
|                              |
|          [<  OK  >]          |
+------------------------------+
Es können selbstverständlich auch längere Elemente angezeigt und ausgewählt werden, allerdings ist die Ausgabe auf 58 Zeichen begrenzt. Die restlichen Zeichen werden durch ein .. angedeutet.

Beispiel:

+-----------[ Select one or more elements from a list ]------------+
|                                                                  |
|  [ ] dies ist ein ziemlich langer Text der auch ziemlich viel U..|
|  [ ] dies nicht                                                  |
|                                                                  |
|                      [<  OK  >]  [ Cancel ]                      |
|                                                                  |
+------------------------------------------------------------------+

ACHTUNG: Es wird nicht geprüft, ob ein Element mehrfach vorkommt.

Mittels der Variablen ECE_SELECT_CBLIST_VAL_SEPARATOR kann der Separator der Select-Liste eingestellt werden.

ECE_SELECT_CBLIST_VAL_SEPARATOR=':'
setzt z.B. den Separator auf den Doppelpunkt.

Holger Bruenjes 2016-12-12