Das ist der letzte Teil des allgemeinen Abschnitts des Kurses, und er bringt alles Bisherige in VFP zusammen. Ihre Anwendung startet ein Konsolenprogramm, wartet, bis es fertig ist, sammelt ein, was es auf die Ausgabe und auf den Fehlerkanal geschrieben hat, und prüft seinen Code, um zu wissen, ob es gut gelaufen ist. Damit können Sie aus VFP jedes Werkzeug mit CLI benutzen: Git, curl, ein Packprogramm, ein Signaturprogramm.
Es gibt drei Wege, es zu starten, und jeder hat irgendwo eine Lücke. Am Ende steht eine Funktion, die das Gute aus allen dreien vereint.
RUN: der übliche Weg
RUN ping -n 2 127.0.0.1 > ausgabe.txt
? FILETOSTR("ausgabe.txt")Aus Teil 1 wissen Sie schon, dass RUN wartet, bis das Programm fertig ist (und VFP so lange stillsteht), dass RUN /N nicht wartet und dass mit /N das > nichts bewirkt. Zwei Dinge fehlen ihm:
- Es liefert keinen Exit-Code. Nach einem
RUNwissen Sie nicht, ob das Programm gut gelaufen ist. - Es zeigt das Konsolenfenster, solange es läuft, und in einer Anwendung verwirrt das den Benutzer.
Für einen schnellen Test im Befehlsfenster reicht es. Für eine Anwendung nicht.
WScript.Shell.Run: verstecktes Fenster und ein Code
WScript.Shell ist ein COM-Objekt, das mit Windows kommt. Seine Methode Run nimmt den Befehl, die Art, das Fenster zu zeigen, und ob gewartet werden soll:
LOCAL loShell, lnCode
loShell = CREATEOBJECT("WScript.Shell")
lnCode = loShell.Run("ping -n 1 -w 500 192.0.2.1", 0, .T.)
? lnCodeDie 0 bedeutet ohne Fenster, das .T. warten, bis es fertig ist. So gibt Run den Exit-Code des Programms zurück: Ein Ping an einen Rechner, der antwortet, liefert 0, und der oben, an einen Rechner, den es nicht gibt, 1.
Wenn Sie nicht warten (.F.), kommt Run sofort zurück (bei mir nach 0,015 Sekunden) und liefert immer 0, weil das Programm noch nicht fertig ist. Diese 0 heißt nicht, dass es gut gelaufen ist.
Run geht nicht über cmd, also funktionieren Umleitungen und die eigenen Befehle von cmd (dir, ver, echo) nicht einfach so. Dafür setzt man cmd /c davor:
lnCode = loShell.Run("cmd /c ping -n 1 127.0.0.1 > ausgabe.txt", 0, .T.)Was Run nicht liefert, ist die Ausgabe: Wenn Sie sie wollen, müssen Sie sie in eine Datei schicken und diese lesen.
WScript.Shell.Exec: Ausgabe ohne Dateien, mit zwei Problemen
Exec startet das Programm und gibt Ihnen ein Objekt mit seinen drei Kanälen zurück: StdOut, StdErr und StdIn. Die Ausgabe wird ohne Umweg über eine Datei gelesen:
LOCAL loShell, loProzess
loShell = CREATEOBJECT("WScript.Shell")
loProzess = loShell.Exec("git estado")
? "Ausgabe:", loProzess.StdOut.ReadAll()
? "Fehler:", loProzess.StdErr.ReadAll()
DO WHILE loProzess.Status = 0
INKEY(0.05, "H")
ENDDO
? "Code:", loProzess.ExitCodeBei git estado kommt die Ausgabe leer an, der Fehler bringt git: 'estado' is not a git command. See 'git --help'., und der Code ist 1: die drei Kanäle aus Teil 3, jeder für sich. Das sieht gut aus, aber Exec hat zwei ernste Probleme:
- Es kann das Fenster nicht verstecken.
Runbekommt gesagt, wie das Fenster erscheinen soll;Exechat keinen solchen Parameter. - Es kann hängen bleiben.
StdOut.ReadAll()wartet, bis das Programm fertig auf seine Ausgabe geschrieben hat. Schreibt das Programm in der Zwischenzeit viel auf den Fehlerkanal, läuft der Platz voll, den Windows für diesen Kanal bereithält, das Programm hält an und wartet, bis jemand liest, und niemand liest, weil VFP noch auf die Ausgabe wartet. Ich habe es mit einem Programm ausprobiert, das 3.000 Zeilen auf den Fehlerkanal schreibt: VFP wartete, bis ich den Prozess von Hand beendet habe.
Exec geht auch nicht über cmd: loShell.Exec("ver") liefert den Fehler 1429, „Das System kann die angegebene Datei nicht finden“, weil ver kein Programm ist, sondern ein Befehl von cmd.
Eine Funktion, die das Gute vereint
Die Ausgabe holt man aus Run, indem man sie in eine temporäre Datei schickt, und den Fehler in eine zweite. So gibt es kein Fenster, kein Hängen, und Sie haben den Code:
FUNCTION Konsole(tcBefehl, tcAusgabe, tcFehler)
* Startet einen Konsolenbefehl ohne Fenster, wartet auf sein Ende und
* gibt seinen Exit-Code zurück. Ausgabe und Fehler kommen per Referenz
* (mit @) zurück, schon von der Codepage 850 der Konsole in die 1252
* von VFP umgewandelt.
LOCAL loShell, lcBasis, lcDateiAusgabe, lcDateiFehler, lnCode
lcBasis = ADDBS(GETENV("TEMP")) + "konsole-" + SYS(2015)
lcDateiAusgabe = lcBasis + "-ausgabe.txt"
lcDateiFehler = lcBasis + "-fehler.txt"
loShell = CREATEOBJECT("WScript.Shell")
* Die äußeren Anführungszeichen sind für cmd /c, das das erste und das
* letzte entfernt: So darf der Befehl eigene haben. 0: ohne Fenster. .T.: warten.
lnCode = loShell.Run([cmd /c "] + tcBefehl + ;
[ > "] + lcDateiAusgabe + [" 2> "] + lcDateiFehler + [""], 0, .T.)
tcAusgabe = IIF(FILE(lcDateiAusgabe), CPCONVERT(850, 1252, FILETOSTR(lcDateiAusgabe)), "")
tcFehler = IIF(FILE(lcDateiFehler), CPCONVERT(850, 1252, FILETOSTR(lcDateiFehler)), "")
ERASE (lcDateiAusgabe)
ERASE (lcDateiFehler)
RETURN lnCode
ENDFUNCSo wird sie benutzt:
LOCAL lcAusgabe, lcFehler, lnCode
lnCode = Konsole("ver", @lcAusgabe, @lcFehler)
? lnCode, lcAusgabeDrei Dinge an der Funktion, die man nicht auf den ersten Blick sieht:
- Die äußeren Anführungszeichen.
cmd /chat eine seltsame Regel: Beginnt das Übergebene mit einem Anführungszeichen, entfernt es das erste und das letzte der ganzen Zeile. Ohne das äußere Paar verlor ein Befehl, der mit einem Programmpfad in Anführungszeichen begann ("C:\...\Meine Programme\curl.exe" ...), sein letztes Anführungszeichen und lief gar nicht erst: Code 1, keine Ausgabe und kein Fehler. Mit dem äußeren Paar entferntcmddiese beiden, und der Inhalt kommt unversehrt an, egal welche Anführungszeichen er hat. CPCONVERT, wegen Teil 3: Die Konsole schreibt in der Codepage 850, und so kommtveralsVersiónan und nicht alsVersi¢n. Ein deutsches Windows benutzt in der Konsole ebenfalls die 850.SYS(2015)gibt den temporären Dateien einen eindeutigen Namen, damit sich zwei gleichzeitige Aufrufe nicht in die Quere kommen. Und am Ende werden sie gelöscht.
Das kam bei jedem Befehl zurück, den ich ihr gegeben habe:
| Befehl | Code | Was ankam |
|---|---|---|
ver | 0 | Ausgabe: Microsoft Windows [Versión 10.0.26200.9457] |
tasklist /fi "imagename eq explorer.exe" /fo csv /nh | 0 | Ausgabe: die CSV-Zeile von explorer.exe |
git estado | 1 | Fehler: git: 'estado' is not a git command. ... |
programm-das-es-nicht-gibt | 1 | Fehler: die Meldung von cmd, der Befehl sei „nicht erkannt“ (bei mir auf Spanisch) |
| ein Programm, das 3.000 Zeilen auf den Fehlerkanal schreibt | 0 | Fehler: die 3.000 Zeilen, in weniger als einer Sekunde |
Der zurückgegebene Code ist der von cmd, also der des letzten Programms, das es ausgeführt hat. Verketten Sie in tcBefehl mehrere mit &, ist es der des letzten.
Ein echtes Beispiel: Antwortet diese Website?
curl kommt seit Jahren mit Windows (C:\Windows\System32\curl.exe) und spricht mit jedem Webserver. Damit prüft Ihre Anwendung, ob ein Dienst antwortet, bevor sie ihm etwas schickt:
LOCAL lcAusgabe, lcFehler, lnCode
lnCode = Konsole("curl -s -f -o nul --max-time 10 https://irwinrodriguez.dev", @lcAusgabe, @lcFehler)
DO CASE
CASE lnCode = 0
? "Antwortet"
CASE lnCode = 6
? "Diesen Servernamen gibt es nicht"
CASE lnCode = 22
? "Antwortet, aber mit einem Fehler (404, 500...)"
CASE lnCode = 28
? "Keine Antwort innerhalb von 10 Sekunden"
OTHERWISE
? "curl ist mit Code", lnCode, "fehlgeschlagen"
ENDCASEDie Optionen, mit dem Wissen aus Teil 2: -s ist ein Schalter, der curl den Fortschrittsbalken abnimmt (ohne ihn geht der Balken auf den Fehlerkanal, verschmutzt also die Ausgabe nicht, ist aber unnötig); -f sorgt dafür, dass ein Fehler der Website (eine Seite, die es nicht gibt, ein Serverfehler) mit Code 22 endet, und -o nul wirft den Seiteninhalt weg, weil Sie nur wissen wollen, ob sie antwortet. --max-time 10 setzt eine Grenze, und wird sie überschritten, ist der Code 28.
Jeden Code habe ich hier herauskommen sehen: 0 mit der Website, 22 mit einer Seite, die es nicht gibt, 6 mit einem Servernamen, den es nicht gibt, und 28 mit einer Adresse, die nicht antwortet. Ohne -f endet curl mit 0, auch wenn es die Seite nicht gibt: Für curl hat die Website geantwortet, wenn auch mit einem 404. Lesen Sie in der Hilfe eines Programms immer nach, was es als Fehler betrachtet, bevor Sie sich auf seinen Code verlassen.
Zum Üben
Kopieren Sie die Funktion Konsole in eine .prg, machen Sie SET PROCEDURE TO auf diese Datei und probieren Sie im Befehlsfenster:
LOCAL lcAusgabe, lcFehler
? Konsole("ver", @lcAusgabe, @lcFehler), lcAusgabe
? Konsole("where notepad", @lcAusgabe, @lcFehler), lcAusgabe
? Konsole("git --version", @lcAusgabe, @lcFehler), lcAusgabe
? Konsole("git estado", @lcAusgabe, @lcFehler), lcFehler
? Konsole("curl -s -o nul -w %{http_code} https://irwinrodriguez.dev", @lcAusgabe, @lcFehler), lcAusgabe(git --version funktioniert nur, wenn Git installiert ist; sonst ist der Code 1, und der Fehler sagt Ihnen, dass es nicht gefunden wird.)
Damit endet der allgemeine Abschnitt des Kurses.