Si tuviera que ponerle nombre a lo que hago, diría que es hacerle las cosas fáciles al foxero. Con los años he visto que eso tiene un precio, y lo paga quien hace la herramienta: cuanto más sencillo queda algo por fuera, más trabajo hay por dentro.

FoxCli es el ejemplo que tengo más a mano. Cuando empecé, un comando de consola se declaraba con la cláusula HELP y una línea con su propia gramática. Hoy es un método de VFP con un comentario encima. En este artículo escribo el mismo comando de las dos formas, compilo las dos y las pruebo con las mismas órdenes, para que se vea qué ha cambiado para ti y qué ha tenido que pasar por detrás.

El comando

Es el exportar de dbf, la herramienta del tutorial Tu primera CLI en Visual FoxPro con FoxCli, con una opción más para que haya un número de por medio:

dbf exportar customers.dbf clientes.csv
dbf exportar customers.dbf clientes.xls --formato xls
dbf exportar customers.dbf muestra.csv --max 5

Recibe dos argumentos sin nombre, la tabla y el fichero que se crea. Tiene una opción --formato que solo admite csv o xls y vale csv si no se escribe, y otra --max con un número entero, para copiar solo los primeros registros. Las dos opciones tienen su forma corta, -f y -m.

Los métodos auxiliares, Abrir y Copiar, son iguales en las dos versiones, así que no los repito. Lo que cambia es cómo se declara el comando y cómo le llegan los valores.

Antes: la cláusula HELP

exportar con la cláusula HELP y un Run que despacha
DEFINE CLASS DbfCommand AS FoxCliCommand OF foxcli.prg OLEPUBLIC

	FUNCTION Run(oCon, oArgs)
		LOCAL lnExit
		lnExit = EXIT_OK
		DO CASE
		CASE LOWER(oArgs.Command()) == "exportar"
			lnExit = THIS.Exportar(oCon, oArgs)
		ENDCASE
		RETURN lnExit
	ENDFUNC

	PROTECTED FUNCTION Exportar(oCon, oArgs) ;
		HELP "exportar <tabla:str![La tabla (.dbf)]> <destino:str![El fichero que se crea]> --formato,-f:csv|xls[Formato del fichero] --max,-m:int[Como mucho tantos registros] : Copia una tabla a un fichero CSV o XLS"
		LOCAL lcTabla, lcDestino, lcFormato, lnMax
		lcTabla   = oArgs.Positional(1)
		lcDestino = oArgs.Positional(2)
		lcFormato = oArgs.Get("formato", "csv")
		lnMax     = oArgs.AsNumber("max", 0)
		IF !THIS.Abrir(lcTabla)
			RETURN EXIT_NO_TABLE
		ENDIF
		THIS.Copiar(lcDestino, lcFormato, lnMax)
		USE
		RETURN EXIT_OK
	ENDFUNC

Funciona bien, pero para escribirlo hace falta saber unas cuantas cosas que no son VFP:

  • La gramática de la cláusula. <tabla:str!> es un argumento sin nombre, obligatorio por el !. Lo que va entre corchetes es la descripción de la opción que tiene delante. ,-f añade la forma corta, :int pone el tipo, :csv|xls cierra la lista de valores, y lo que va detrás de los dos puntos del final describe el comando. Además, <> y [] no significan aquí lo mismo que en la línea de uso de cualquier ayuda, donde los corchetes marcan lo opcional.
  • Que todo va en una sola cadena, y que esa cadena no puede pasar de 255 caracteres. Con 256, VFP no compila el fichero, y el error no dice nada de la longitud. Esta tiene 202: con descripciones un poco más largas ya no cabría.
  • Que hay que escribir Run y repartir los comandos con un DO CASE. Cada comando nuevo es una rama más, y un comando que declaras y te olvidas de añadir al CASE no hace nada.
  • Cómo se lee cada valor. Los argumentos sin nombre, por su posición con Positional(). Las opciones, por su nombre largo aunque quien llame haya escrito -f. El número con AsNumber() y el texto con Get().
  • Que los valores por defecto no se declaran: van en el código, en cada Get(). Por eso la ayuda no los enseña.

Ahora: un método con un comentario

El mismo exportar, con la firma nativa
DEFINE CLASS DbfCommand AS FoxCliCommand OF foxcli.prg OLEPUBLIC

	*!* Copia una tabla a un fichero CSV o XLS
	*!* @tcTabla   <>                La tabla (.dbf)
	*!* @tcDestino <>                El fichero que se crea
	*!* @tcFormato -f :csv|xls =csv  Formato del fichero
	*!* @tnMax     -m =0             Como mucho tantos registros
	PROCEDURE Exportar(tcTabla AS String, tcDestino AS String, tcFormato AS String, tnMax AS Integer)
		IF !THIS.Abrir(tcTabla)
			RETURN EXIT_NO_TABLE
		ENDIF
		THIS.Copiar(tcDestino, tcFormato, tnMax)
		USE
	ENDPROC

Es un PROCEDURE como cualquier otro, y se entiende sin saber nada de FoxCli:

  • Un método público de la clase es un comando, y su nombre en minúsculas es el verbo. No hay Run ni DO CASE.
  • Cada parámetro es una opción, con el nombre sin el prefijo: tcFormato es --formato y tnMax es --max. El tipo sale del AS.
  • El bloque *!* añade lo que la firma no dice. <> hace que el argumento vaya sin nombre, -f es la forma corta, :csv|xls es la lista de valores y =csv el valor por defecto. Un parámetro sin = es obligatorio, y el resto de la línea es la descripción.
  • Los valores llegan en los parámetros, ya convertidos. tnMax es un número, y tcFormato llega en minúsculas aunque alguien escriba -f XLS, porque un valor de la lista se entrega tal como está declarado.
  • Sin RETURN, el comando termina con 0. RETURN EXIT_NO_TABLE termina con el 10 que declaré en foxcli.h.

Los símbolos son casi los mismos que en HELP, pero ahora cada uno va en su línea, junto al parámetro al que se refiere, y no hay ningún límite de longitud que vigilar.

Lo que ve quien usa la CLI

Compilé las dos versiones y les pasé las mismas órdenes. La salida es la misma, con una diferencia a favor de la nueva: la ayuda enseña los valores por defecto, porque ahora están declarados.

> dbf exportar --help
exportar -- Copia una tabla a un fichero CSV o XLS

Uso: dbf exportar <tabla> <destino>

Opciones:
  -f, --formato csv|xls       Formato del fichero (por defecto: csv)
  -m, --max <int>             Como mucho tantos registros (por defecto: 0)

Argumentos:
  tabla                       La tabla (.dbf) (requerido)
  destino                     El fichero que se crea (requerido)

Los errores también son iguales en las dos. Una opción mal escrita:

> dbf exportar customers.dbf clientes.csv --fromato xls
dbf: opción desconocida '--fromato'. Quizá querías '--formato'
dbf: sobra el argumento 'xls': 'exportar' lleva 2 posicionales

Un valor que no es del tipo declarado, o que no está en la lista:

> dbf exportar customers.dbf clientes.csv --max diez
dbf: '--max diez' no es un número

> dbf exportar customers.dbf clientes.csv -f txt
dbf: '--formato txt' no es válido. Valores: csv, xls

En todos esos casos la CLI enseña además la ayuda del comando y sale con código 2, sin llegar a ejecutar tu código. Con la línea bien escrita sale el resultado, y si la tabla no existe, el 10:

> dbf exportar customers.dbf muestra.csv -m 5
5 registros copiados a muestra.csv

> dbf exportar facturas.dbf x.csv
dbf: no encuentro la tabla facturas.dbf

Los CSV que dejaron las dos versiones son idénticos.

Lo que hay detrás

Del antes al ahora tú escribes menos y tienes que saber menos, que es lo que yo buscaba. El trabajo sigue estando, pero en FoxCli, y cada cosa que ya no tienes que pensar es una decisión que he tomado yo. Casi todas tienen su caso raro:

  • Sacar el nombre de la opción del parámetro. tcNombre da --nombre, pero total se queda en --total, porque el prefijo solo se quita cuando tiene forma de prefijo. Y como renombrar el parámetro cambiaría la opción y rompería los scripts de quien ya use tu CLI, el nombre se puede fijar en el comentario: @tcPersona --nombre.
  • Decidir el tipo. Gana lo que digas en el comentario; si no dices nada, el AS; si no hay AS, el prefijo; y si tampoco, texto. Un tn sin AS es un entero, porque es lo que casi siempre quiere decir quien lo escribe.
  • Las fechas. Por COM una fecha llega siempre como DateTime, así que un AS Date no la convertiría, y FoxCli lo avisa al compilar. En la consola solo se admite AAAA-MM-DD, porque 03/02/2026 es marzo en un sitio y febrero en otro, y aceptarlo sería leerlo mal sin avisar a nadie.
  • Validar antes de ejecutar nada. Si falta un argumento o una opción está mal escrita, tu método no llega a correr. Quien llama recibe el motivo, una sugerencia si se ha equivocado por poco, la ayuda del comando y un código de salida que un script puede leer.
  • Que la documentación no se quede atrás. De ese mismo comentario salen la ayuda de la consola, un docs\dbf.md con los comandos, sus opciones, sus valores por defecto y sus códigos de salida, que sirve tal cual como README, y dbf describe, que da el esquema en JSON para que un agente de IA sepa usar la CLI sin leer nada más.
  • Ningún diálogo. Un error de VFP que no atrapas sale por el canal de error con el fichero y la línea, y la CLI termina. Nunca se queda esperando a que alguien pulse Aceptar.

La cláusula HELP sigue valiendo

Los proyectos que ya la usan siguen compilando y funcionando igual, y las dos formas se pueden mezclar en la misma clase: los comandos con HELP pasan por Run, y los demás van directos a su método. Así un proyecto se puede pasar a la firma nativa comando a comando, a medida que haya que cambiar cada uno.

Para empezar desde cero, el tutorial Tu primera CLI en Visual FoxPro con FoxCli hace una herramienta completa con la firma nativa, paso a paso desde el IDE. Y si la consola te queda lejos, el curso CLIs para foxeros explica qué son los argumentos, las opciones y los códigos de salida.