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

Lanzar una orden con RUN
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 RUN no 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:

Lanzar un programa sin ventana y leer su código de salida
LOCAL loShell, lnCodigo
loShell  = CREATEOBJECT("WScript.Shell")
lnCodigo = loShell.Run("ping -n 1 -w 500 192.0.2.1", 0, .T.)
? lnCodigo

El 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:

Run con la salida a un fichero
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:

Leer la salida y el error de un programa con Exec
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.ExitCode

Con 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:

  1. No tiene forma de ocultar la ventana. Run recibe cómo enseñarla; Exec no tiene ese parámetro.
  2. 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:

Función para lanzar un programa de consola desde VFP
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
ENDFUNC

Se usa así:

Llamar a la función Consola
LOCAL lcSalida, lcError, lnCodigo
lnCodigo = Consola("ver", @lcSalida, @lcError)
? lnCodigo, lcSalida

Tres cosas de la función que no se ven a simple vista:

  • Las comillas de fuera. cmd /c tiene 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, cmd quita 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í ver llega como Versión y no como Versi¢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é:

OrdenCódigoLo que llegó
ver0salida: Microsoft Windows [Versión 10.0.26200.9457]
tasklist /fi "imagename eq explorer.exe" /fo csv /nh0salida: la línea CSV de explorer.exe
git estado1error: git: 'estado' is not a git command. ...
programa-que-no-existe1error: "programa-que-no-existe" no se reconoce como un comando interno o externo, ...
un programa que escribe 3.000 líneas por el error0error: 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:

Comprobar desde VFP si una web contesta, con curl
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
ENDCASE

Las 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:

Pruebas de la función Consola
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.