Esta es la última entrega de la parte general del curso, y junta todo lo anterior desde VFP. Tu aplicación lanza un programa de consola, espera a que acabe, recoge lo que ha escrito por la salida y por el error, y mira su código para saber si ha ido bien. Con eso puedes usar desde VFP cualquier herramienta que tenga CLI: git, curl, un compresor, un programa de firma.
Hay tres formas de lanzarlo, y cada una se queda corta en algo. Al final va una función que junta lo bueno de las tres.
RUN: la de siempre
RUN ping -n 2 127.0.0.1 > salida.txt
? FILETOSTR("salida.txt")De la entrega 1 ya sabes que RUN espera a que el programa acabe (y VFP se queda parado mientras), que RUN /N no espera y que con /N el > no hace nada. Le faltan dos cosas:
- No te da el código de salida. Después de un
RUNno sabes si el programa ha ido bien. - Enseña la ventana de la consola mientras dura, y en una aplicación eso despista al usuario.
Para una prueba rápida en la ventana de comandos, vale. Para una aplicación, no.
WScript.Shell.Run: con ventana oculta y con código
WScript.Shell es un objeto COM que viene con Windows. Su método Run recibe la orden, cómo enseñar la ventana y si esperar:
LOCAL loShell, lnCodigo
loShell = CREATEOBJECT("WScript.Shell")
lnCodigo = loShell.Run("ping -n 1 -w 500 192.0.2.1", 0, .T.)
? lnCodigoEl 0 quiere decir sin ventana, y el .T., esperar a que termine. Con eso Run devuelve el código de salida del programa: el ping a una máquina que contesta devuelve 0, y ese de arriba, a una que no existe, 1.
Si no esperas (.F.), Run vuelve en el acto (en el mío, 0,015 segundos) y devuelve 0 siempre, porque el programa aún no ha terminado. Ese 0 no quiere decir que haya ido bien.
Run no pasa por cmd, así que las redirecciones y las órdenes propias de cmd (dir, ver, echo) no funcionan tal cual. Para eso se lanza cmd /c delante:
lnCodigo = loShell.Run("cmd /c ping -n 1 127.0.0.1 > salida.txt", 0, .T.)Lo que no te da Run es la salida: si la quieres, hay que mandarla a un fichero y leerlo.
WScript.Shell.Exec: la salida sin ficheros, con dos problemas
Exec lanza el programa y te devuelve un objeto con sus tres canales: StdOut, StdErr y StdIn. La salida se lee sin pasar por ningún fichero:
LOCAL loShell, loProceso
loShell = CREATEOBJECT("WScript.Shell")
loProceso = loShell.Exec("git estado")
? "Salida:", loProceso.StdOut.ReadAll()
? "Error:", loProceso.StdErr.ReadAll()
DO WHILE loProceso.Status = 0
INKEY(0.05, "H")
ENDDO
? "Código:", loProceso.ExitCodeCon git estado la salida llega vacía, el error trae git: 'estado' is not a git command. See 'git --help'. y el código es 1: los tres canales de la entrega 3, cada uno por su lado. Tiene buena pinta, pero Exec tiene dos problemas serios:
- No tiene forma de ocultar la ventana.
Runrecibe cómo enseñarla;Execno tiene ese parámetro. - Se puede quedar colgado.
StdOut.ReadAll()espera a que el programa termine de escribir por la salida. Si mientras tanto el programa escribe mucho por el error, se llena el hueco que Windows le guarda para ese canal, el programa se para a esperar a que alguien lo lea, y nadie lo lee porque VFP sigue esperando la salida. Probé con un programa que escribe 3.000 líneas por el error: VFP se quedó esperando hasta que maté el proceso a mano.
Exec tampoco pasa por cmd: loShell.Exec("ver") da el error 1429, «El sistema no puede encontrar el archivo especificado», porque ver no es un programa, es una orden de cmd.
Una función que junta lo bueno
La salida sale de Run mandándola a un fichero temporal, y el error a otro. Así no hay ventana, no hay bloqueo y tienes el código:
FUNCTION Consola(tcOrden, tcSalida, tcError)
* Lanza una orden de consola sin enseñar ventana, espera a que acabe y
* devuelve su código de salida. La salida y el error llegan por
* referencia (con @), ya pasados de la página 850 de la consola a la
* 1252 de VFP.
LOCAL loShell, lcBase, lcFichSalida, lcFichError, lnCodigo
lcBase = ADDBS(GETENV("TEMP")) + "consola-" + SYS(2015)
lcFichSalida = lcBase + "-salida.txt"
lcFichError = lcBase + "-error.txt"
loShell = CREATEOBJECT("WScript.Shell")
* Las comillas de fuera son para cmd /c, que quita la primera y la última:
* así la orden puede llevar las suyas. 0: sin ventana. .T.: esperar.
lnCodigo = loShell.Run([cmd /c "] + tcOrden + ;
[ > "] + lcFichSalida + [" 2> "] + lcFichError + [""], 0, .T.)
tcSalida = IIF(FILE(lcFichSalida), CPCONVERT(850, 1252, FILETOSTR(lcFichSalida)), "")
tcError = IIF(FILE(lcFichError), CPCONVERT(850, 1252, FILETOSTR(lcFichError)), "")
ERASE (lcFichSalida)
ERASE (lcFichError)
RETURN lnCodigo
ENDFUNCSe usa así:
LOCAL lcSalida, lcError, lnCodigo
lnCodigo = Consola("ver", @lcSalida, @lcError)
? lnCodigo, lcSalidaTres cosas de la función que no se ven a simple vista:
- Las comillas de fuera.
cmd /ctiene una regla rara: si lo que le pasas empieza por comillas, quita la primera y la última de toda la línea. Sin la pareja de fuera, una orden que empezaba por la ruta de un programa entre comillas ("C:\...\Mis programas\curl.exe" ...) perdía la comilla del final y no llegaba a ejecutarse: código 1, sin salida y sin error. Con la pareja de fuera,cmdquita esas dos y lo de dentro llega intacto, tenga las comillas que tenga. CPCONVERT, por lo de la entrega 3: la consola escribe en la página 850, y asíverllega comoVersióny no comoVersi¢n.SYS(2015)da un nombre único a los ficheros temporales, para que dos llamadas a la vez no se pisen. Y se borran al acabar.
Esto es lo que devolvió con cada orden que le pasé:
| Orden | Código | Lo que llegó |
|---|---|---|
ver | 0 | salida: Microsoft Windows [Versión 10.0.26200.9457] |
tasklist /fi "imagename eq explorer.exe" /fo csv /nh | 0 | salida: la línea CSV de explorer.exe |
git estado | 1 | error: git: 'estado' is not a git command. ... |
programa-que-no-existe | 1 | error: "programa-que-no-existe" no se reconoce como un comando interno o externo, ... |
| un programa que escribe 3.000 líneas por el error | 0 | error: las 3.000 líneas, en menos de un segundo |
El código que devuelve es el de cmd, que es el del último programa que ejecutó. Si en tcOrden encadenas varios con &, es el del último.
Un ejemplo real: ¿contesta esa web?
curl viene con Windows desde hace años (C:\Windows\System32\curl.exe) y habla con cualquier servidor web. Sirve para comprobar desde tu aplicación si un servicio contesta antes de mandarle nada:
LOCAL lcSalida, lcError, lnCodigo
lnCodigo = Consola("curl -s -f -o nul --max-time 10 https://irwinrodriguez.dev", @lcSalida, @lcError)
DO CASE
CASE lnCodigo = 0
? "Contesta"
CASE lnCodigo = 6
? "No existe ese nombre de servidor"
CASE lnCodigo = 22
? "Contesta, pero con un error (404, 500...)"
CASE lnCodigo = 28
? "No ha contestado en 10 segundos"
OTHERWISE
? "curl ha fallado con el código", lnCodigo
ENDCASELas opciones, con lo de la entrega 2: -s es una bandera que le quita a curl la barra de progreso (sin ella, la barra va por el canal de error, así que no te ensucia la salida, pero no hace falta); -f hace que un error de la web (una página que no existe, un fallo del servidor) salga con código 22, y -o nul tira el contenido de la página, porque solo quieres saber si contesta. --max-time 10 pone un límite, y si se pasa, el código es 28.
Cada código lo he visto salir aquí: 0 con la web, 22 con una página que no existe, 6 con un nombre de servidor que no existe y 28 con una dirección que no contesta. Sin -f, curl sale con 0 aunque la página no exista: para él, la web ha contestado, aunque sea con un 404. Lee siempre en la ayuda de un programa qué considera un error antes de fiarte de su código.
Para practicar
Copia la función Consola en un .prg, haz SET PROCEDURE TO ese fichero y prueba desde la ventana de comandos:
LOCAL lcSalida, lcError
? Consola("ver", @lcSalida, @lcError), lcSalida
? Consola("where notepad", @lcSalida, @lcError), lcSalida
? Consola("git --version", @lcSalida, @lcError), lcSalida
? Consola("git estado", @lcSalida, @lcError), lcError
? Consola("curl -s -o nul -w %{http_code} https://irwinrodriguez.dev", @lcSalida, @lcError), lcSalida(git --version solo funciona si tienes git instalado; si no, el código será 1 y el error te dirá que no lo encuentra.)
Con esto acaba la parte general del curso.