En este tutorial vamos a hacer dbf, una herramienta de consola escrita en Visual FoxPro que mira y exporta tablas sin abrir VFP:

dbf info customers.dbf
dbf campos customers.dbf --format json
dbf exportar customers.dbf clientes.csv

Tiene tres comandos, una ayuda que sale sola, errores que explican qué pasa y códigos de salida que un script puede leer. Se reparte copiando una carpeta, en un equipo sin Visual FoxPro instalado. Y todo el código es un .prg de VFP de 76 líneas.

Trabajamos con customers.dbf, la tabla de clientes de los datos de ejemplo Northwind que trae VFP, así que lo que ves en las capturas lo puedes repetir tal cual. dbf solo lee: nunca escribe en la tabla.

Si la consola te queda lejos, antes de empezar tienes el curso CLIs para foxeros: qué es una orden, qué son los argumentos y las opciones y qué es un código de salida. Aquí doy todo eso por sabido.

Lo que necesitas

  • Visual FoxPro 9 (o VFP Advanced).
  • FoxCli instalado. Trae FoxForge, el taller que se engancha al IDE de VFP. Sin licencia funciona igual; la diferencia la vemos al final.
  • Windows 10 u 11.

Abre VFP como siempre, sin «Ejecutar como administrador». Al compilar como administrador, VFP registra el componente en HKLM, y ese registro estorba a las compilaciones siguientes. Y no uses el botón Build del Administrador de Proyectos: compila con Ctrl+F7, que es el que deja la CLI completa.

Paso 1. Cargar FoxForge

En la ventana de comandos de VFP:

Cargar FoxForge en el IDE
DO "C:\Program Files (x86)\FoxForge\FoxForge.app"

Cambia la ruta si instalaste FoxForge en otra carpeta. Aparece un menú nuevo en la barra de VFP, FoxForge, y desde ahí se hace todo lo demás.

Paso 2. Crear el proyecto

FoxForge > Nuevo proyecto > Aplicación de consola (CLI). En el asistente:

  • Nombre: dbf. Es el nombre del programa, el que se escribe en la consola.
  • Carpeta: la tuya de proyectos. Yo he usado C:\a1\tutorial-b.
  • Plantilla mínima, sin comentarios: marcada. La otra plantilla trae el código explicado línea a línea, que está bien para leerla una vez, pero hoy vamos a escribir el nuestro.

El asistente de FoxForge «Nueva aplicación de consola», con el nombre dbf, la carpeta y la plantilla mínima marcada

Pulsa Crear. FoxForge crea el proyecto y lo abre en el Administrador de Proyectos:

dbf\
├── dbf.pjx          el proyecto de VFP
├── foxforge.json    la marca de proyecto CLI
├── pack.ps1         empaqueta lo que se reparte
├── src\
│   ├── main.prg     tus comandos: lo único que editas
│   ├── foxcli.h     los códigos de salida con nombre
│   └── foxcli.prg   la clase base, que es de FoxCli
└── dist\            aquí sale el .exe al compilar

Paso 3. Lo que trae la plantilla

Abre src\main.prg:

main.prg recién creado, con la plantilla mínima
#INCLUDE foxcli.h

*!* dbf: describe aquí lo que hace tu CLI
DEFINE CLASS DbfCommand AS FoxCliCommand OF foxcli.prg OLEPUBLIC

    *!* Saluda a alguien
    *!* @tcNombre  -n  A quién saludar
    PROCEDURE Saludar(tcNombre AS String)
        Console.WriteLine("Hola " + tcNombre + "!")
    ENDPROC

ENDDEFINE

Con esto ya se ve cómo funciona todo:

  • Cada método público de la clase es un comando. Saludar se usa como dbf saludar.
  • Cada parámetro es una opción, con el nombre sin el prefijo: tcNombre es --nombre. El tipo sale del AS.
  • El bloque *!* de encima es la documentación. La primera línea es la descripción del comando, y cada línea @ anota un parámetro: -n le da un alias corto y el resto es su descripción. De ahí sale la ayuda.
  • Console es la consola. Console.WriteLine() escribe por la salida y Console.Error(), por el canal de error.

Paso 4. Escribir los comandos

Sustituye el contenido de main.prg por esto:

main.prg de dbf: tres comandos para mirar y exportar tablas
#INCLUDE foxcli.h

*!* dbf: mira y exporta tablas de VFP desde la consola, sin abrir Visual FoxPro
*!* @exit 10 La tabla no existe
DEFINE CLASS DbfCommand AS FoxCliCommand OF foxcli.prg OLEPUBLIC

	*!* Cuántos registros y campos tiene una tabla
	*!* @tcTabla <>  La tabla (.dbf)
	*!* @example dbf info customers.dbf
	PROCEDURE Info(tcTabla AS String)
		IF !THIS.Abrir(tcTabla)
			RETURN EXIT_NO_TABLE
		ENDIF
		Console.WriteLine("Registros:  " + TRANSFORM(RECCOUNT()))
		Console.WriteLine("Campos:     " + TRANSFORM(FCOUNT()))
		Console.Write("Modificada: ")
		Console.WriteLine(LUPDATE())
		USE
	ENDPROC

	*!* La estructura de una tabla: nombre, tipo y ancho de cada campo
	*!* @tcTabla <>  La tabla (.dbf)
	*!* @example dbf campos customers.dbf --format json
	PROCEDURE Campos(tcTabla AS String)
		LOCAL laCampos[1], lnCampos, lnI, lcJson
		IF !THIS.Abrir(tcTabla)
			RETURN EXIT_NO_TABLE
		ENDIF
		lnCampos = AFIELDS(laCampos)
		USE
		IF Console.IsJson()
			lcJson = ""
			FOR lnI = 1 TO lnCampos
				lcJson = lcJson + IIF(lnI > 1, ",", "") + ;
					'{"nombre":"' + LOWER(laCampos[lnI, 1]) + '","tipo":"' + laCampos[lnI, 2] + ;
					'","ancho":' + TRANSFORM(laCampos[lnI, 3]) + '}'
			ENDFOR
			Console.WriteLine("[" + lcJson + "]")
		ELSE
			FOR lnI = 1 TO lnCampos
				Console.WriteLine(PADR(LOWER(laCampos[lnI, 1]), 14) + laCampos[lnI, 2] + ;
					STR(laCampos[lnI, 3], 5))
			ENDFOR
		ENDIF
	ENDPROC

	*!* Copia una tabla a un fichero CSV o XLS
	*!* @tcTabla   <>             La tabla (.dbf)
	*!* @tcDestino <>             El fichero que se crea
	*!* @tcFormato :csv|xls =csv  Formato del fichero
	*!* @example dbf exportar customers.dbf clientes.csv
	*!* @example dbf exportar customers.dbf clientes.xls --formato xls
	PROCEDURE Exportar(tcTabla AS String, tcDestino AS String, tcFormato AS String)
		IF !THIS.Abrir(tcTabla)
			RETURN EXIT_NO_TABLE
		ENDIF
		IF tcFormato == "xls"
			COPY TO (tcDestino) TYPE XL5
		ELSE
			COPY TO (tcDestino) TYPE CSV
		ENDIF
		Console.Error(TRANSFORM(RECCOUNT()) + " registros copiados a " + tcDestino)
		USE
	ENDPROC

	*-- Abre la tabla solo para leer. Si no existe, lo dice por el canal de error.
	PROTECTED FUNCTION Abrir(tcTabla)
		IF !FILE(FORCEEXT(tcTabla, "dbf"))
			Console.Error("dbf: no encuentro la tabla " + tcTabla)
			RETURN .F.
		ENDIF
		USE (FORCEEXT(tcTabla, "dbf")) AGAIN SHARED NOUPDATE ALIAS tabla
		RETURN .T.
	ENDFUNC

ENDDEFINE

Y añade al final de src\foxcli.h el código de salida propio:

El código de salida de dbf, en foxcli.h
#DEFINE EXIT_NO_TABLE 10    && la tabla no existe

Lo que hay que mirar en ese código:

  • <> hace un posicional. @tcTabla <> dice que la tabla se escribe sin nombre delante: dbf info customers.dbf, y no dbf info --tabla customers.dbf.
  • :csv|xls =csv es una lista cerrada con valor por defecto. --formato solo acepta csv o xls, y si no se escribe vale csv. De validarlo se encarga FoxCli, antes de llegar a tu código.
  • Lo que devuelve el método es el código de salida. Sin RETURN, el comando acaba con 0. RETURN EXIT_NO_TABLE acaba con 10, y el @exit 10 de arriba lo documenta. Los tuyos van del 10 al 125: los números bajos son de FoxCli.
  • Console.IsJson() dice si quien llama ha pedido --format json. Esa opción no la declaras tú: la trae FoxCli en todos los comandos.
  • El resultado va por la salida y los mensajes por el error. exportar avisa de cuántos registros ha copiado con Console.Error(), así que si alguien redirige su salida a un fichero, el aviso no se mezcla.
  • Abrir es PROTECTED: un método auxiliar como cualquier otro, que no sale en la lista de comandos. Por eso su comentario va con el *-- de siempre: *!* es para lo que sale en la ayuda de la CLI, y a un método auxiliar no se le llama desde la consola.
  • Console.WriteLine(LUPDATE()) escribe la fecha en formato ISO (2004-12-09) en cualquier equipo, sin depender de SET DATE. La salida de una CLI la leen otros programas, y tiene que ser igual en todas partes.

Paso 5. Compilar

FoxForge > Proyecto actual > Compilar CLI, o Ctrl+F7. Sin licencia de FoxCli, sale este aviso:

Aviso de FoxForge al compilar: CLI compilada en C:\a1\tutorial-b\dbf\dist\dbf.exe; sin licencia de FoxCli sale en evaluación y escribe una línea por stderr

Ya tienes dist\dbf.exe. De paso, FoxForge ha generado la documentación en docs\dbf.md, con la descripción de cada comando sacada de los bloques *!*.

Paso 6. Probarla desde el IDE

Antes de abrir una consola, se puede probar sin salir de VFP. FoxForge > Proyecto actual > Argumentos…, o Shift+F8, y escribe la línea como la escribirías en una consola, sin el nombre del programa:

El diálogo de argumentos de FoxForge, con info y la ruta completa de customers.dbf, y abajo la orden que se va a ejecutar

Se ejecuta dentro de dist\, así que aquí va la ruta completa de la tabla. Ejecutar (o Ctrl+F8 la próxima vez, que recuerda los argumentos) abre una consola con el resultado, y debajo el código de salida:

Una consola con la salida de dbf info: 91 registros, 11 campos, modificada el 2004-12-09, la línea de evaluación de FoxCli y exit 0

Si algo falla, FoxForge > Proyecto actual > Banco de pruebas ejecuta un comando desde tu fuente, con el depurador de VFP delante y sin compilar.

Paso 7. Repartirla y usarla desde la consola

En la carpeta del proyecto, pack.ps1 arma un zip con lo que hace falta para ejecutar la CLI y nada más. Desde una consola en esa carpeta:

powershell -ExecutionPolicy Bypass -File pack.ps1
9 ficheros -> C:\a1\tutorial-b\dbf\dbf-dist.zip (3.634.677 bytes)
Se despliega descomprimiendo: no hace falta registrar nada ni ser administrador.

Esos 9 ficheros son tu CLI y el runtime de VFP 9, para que funcione en un equipo sin Visual FoxPro. Descomprímelos donde quieras (yo, en una carpeta de herramientas) y ya está instalada. No se registra nada, así que tampoco hay que desinstalar: se borra la carpeta.

Para no tocar la carpeta de VFP, copia los datos de Northwind a tu carpeta de pruebas. Ojo: la carpeta entera, porque customers.dbf pertenece a la base de datos northwind.dbc:

La consola copiando la carpeta Northwind con xcopy a C:\pruebas-cli: 33 archivos copiados

Entra en la copia y añade la carpeta de dbf al PATH de esa consola, para escribir dbf sin la ruta:

cd C:\pruebas-cli\northwind
set PATH=%PATH%;C:\herramientas\dbf

(Cambia C:\herramientas\dbf por la carpeta donde lo descomprimiste. Ese set vale solo para esa ventana.)

La ayuda la has escrito sin darte cuenta, en los bloques *!*:

La ayuda general de dbf: la descripción de la CLI, el uso y la lista de comandos info, campos, exportar y describe

describe también lo trae FoxCli: vuelca en JSON todos los comandos con sus opciones, para que un agente de IA descubra la CLI sin leer nada más.

Si te falta un argumento, dbf no llega a ejecutar tu código: dice qué falta, enseña la ayuda del comando y sale con 2. Con la tabla, da el resultado:

dbf info sin argumentos: faltan argumentos posicionales y la ayuda del comando; después dbf info customers.dbf con 91 registros y 11 campos

campos saca la estructura en una tabla que se lee en pantalla:

dbf campos customers.dbf: la lista de campos con su tipo y su ancho

Y con --format json, en una línea que otro programa puede leer:

dbf campos customers.dbf --format json
[{"nombre":"customerid","tipo":"C","ancho":5},{"nombre":"companyname","tipo":"C","ancho":40},{"nombre":"contactname","tipo":"C","ancho":30},...,{"nombre":"fax","tipo":"C","ancho":24}]

exportar deja el fichero y avisa por el canal de error. Con una tabla que no existe, sale con el 10 que declaramos:

dbf exportar a ..\clientes.csv: 91 registros copiados y código 0; dbf info facturas.dbf: no encuentro la tabla y código 10

El CSV sale con las tildes en su sitio (Avda. de la Constitución 2222, México D.F.), porque lo escribe VFP con la página de códigos de la tabla.

Y un formato que no está en la lista no llega a tu código: FoxCli dice cuáles valen, enseña la ayuda de exportar y sale con 2.

dbf exportar con el formato txt: no es válido, los valores son csv y xls, la ayuda del comando y código 2

Una tabla lejos de su base de datos. Si copias solo customers.dbf a otra carpeta, sin northwind.dbc, VFP intentará preguntarte dónde está la base de datos. En una CLI no hay nadie a quien preguntar, así que dbf sale con código 1 y el error de VFP por el canal de error («Operación de interfaz de usuario no permitida en este momento», el 2031). No se queda colgada esperando una respuesta: FoxCli nunca deja un diálogo abierto.

Paso 8. La licencia

Sin licencia, la CLI funciona igual y sin fecha de caducidad. Lo único que cambia es la línea que has visto al final de cada salida:

Hecho con FoxCli (evaluación) - irwinrodriguez.dev/foxcli

Va por el canal de error, así que no se mete en un CSV ni en un JSON aunque redirijas la salida. Con una licencia de FoxCli cargada en FoxForge > Licencia de FoxCli…, Ctrl+F7 sella la CLI con tu nombre: la línea desaparece, y --version dice a quién está licenciada.

dbf --version
dbf (FoxCli 1.3.0, con licencia de Irwin Rodríguez)

Las CLIs que compilas las repartes gratis y a quien quieras: la licencia es del programador que compila, no de cada copia.

Y ahora qué

  • La guía de FoxCli está en FoxForge > Ayuda > Guía de FoxCli: leer la entrada, las fechas, el formato de salida, la cláusula HELP y todo lo que hoy no ha salido.
  • La documentación de tu CLI la tienes en docs\dbf.md, y se regenera en cada compilación: sirve tal cual como README.
  • Un ejemplo grande: FoxPack, el gestor de librerías para proyectos VFP, es una CLI hecha con FoxCli, y su código está publicado.
  • Ideas para seguir con dbf: un comando contar con un filtro (--donde "country = 'Spain'"), o que exportar acepte --campos para elegir las columnas.