If I had to name what I do, I would say it is making things easy for Fox developers. Over the years I have seen that this has a price, and the one who pays it is whoever builds the tool: the simpler something looks from the outside, the more work there is on the inside.

FoxCli is the example closest to hand. When I started, a console command was declared with the HELP clause and a line with its own grammar. Today it is a VFP method with a comment on top. In this article I write the same command both ways, build both and run them with the same command lines, so you can see what has changed for you and what had to happen behind the scenes.

The command

It is the exportar command from dbf, the tool in the tutorial Your first CLI in Visual FoxPro with FoxCli, with one more option so there is a number involved:

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

It takes two unnamed arguments, the table and the file to create. It has a --formato option that only accepts csv or xls and defaults to csv, and a --max option with an integer, to copy only the first records. Both options have a short form, -f and -m.

I built it in Spanish, like the tutorial, so the names (exportar is “export”, formato is “format”), the descriptions and the messages you will see are in Spanish. The messages come out in your language when the CLI is set to it.

The helper methods, Abrir and Copiar, are identical in both versions, so I do not repeat them. What changes is how the command is declared and how the values reach it.

Then: the HELP clause

exportar with the HELP clause and a Run that dispatches
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

It works well, but to write it you need to know a few things that are not VFP:

  • The grammar of the clause. <tabla:str!> is an unnamed argument, required because of the !. What goes in square brackets is the description of the option in front of it. ,-f adds the short form, :int sets the type, :csv|xls closes the list of values, and whatever follows the colon at the end describes the command. On top of that, <> and [] do not mean here what they mean in the usage line of any help text, where square brackets mark what is optional.
  • That everything goes in a single string, and that string cannot go past 255 characters. At 256, VFP does not compile the file, and the error says nothing about the length. This one has 202: with slightly longer descriptions it would no longer fit.
  • That you have to write Run and hand out the commands with a DO CASE. Every new command is one more branch, and a command you declare and forget to add to the CASE does nothing.
  • How to read each value. The unnamed arguments by position, with Positional(). The options by their long name, even if the caller typed -f. The number with AsNumber() and the text with Get().
  • That default values are not declared: they go in the code, in each Get(). That is why the help does not show them.

Now: a method with a comment

The same exportar, with the native signature
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

It is a PROCEDURE like any other, and you can follow it without knowing anything about FoxCli:

  • A public method of the class is a command, and its name in lower case is the verb. There is no Run and no DO CASE.
  • Each parameter is an option, named without its prefix: tcFormato is --formato and tnMax is --max. The type comes from the AS.
  • The *!* block adds what the signature does not say. <> makes the argument unnamed, -f is the short form, :csv|xls is the list of values and =csv the default. A parameter without = is required, and the rest of the line is the description.
  • The values arrive in the parameters, already converted. tnMax is a number, and tcFormato arrives in lower case even if someone types -f XLS, because a value from the list is delivered as it was declared.
  • Without RETURN, the command ends with 0. RETURN EXIT_NO_TABLE ends with the 10 I declared in foxcli.h.

The symbols are almost the same as in HELP, but now each one sits on its own line, next to the parameter it refers to, and there is no length limit to watch.

What the user of the CLI sees

I built both versions and gave them the same command lines. The output is the same, with one difference in favour of the new one: the help shows the default values, because now they are declared.

> 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)

The errors are the same in both too. A misspelt option (“unknown option, did you mean --formato”, and a leftover argument):

> 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

A value that is not of the declared type (“is not a number”), or is not on the list (“is not valid, values: csv, xls”):

> 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

In all those cases the CLI also shows the command’s help and exits with code 2, without ever running your code. With a well-formed line you get the result, and if the table does not exist, the 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

The CSV files the two versions left behind are identical.

What sits behind it

From then to now you write less and need to know less, which is what I was after. The work is still there, but inside FoxCli, and every thing you no longer have to think about is a decision I made. Nearly all of them have their odd case:

  • Getting the option name from the parameter. tcNombre gives --nombre, but total stays --total, because the prefix is only dropped when it looks like a prefix. And since renaming the parameter would change the option and break the scripts of anyone already using your CLI, the name can be pinned in the comment: @tcPersona --nombre.
  • Deciding the type. What you say in the comment wins; if you say nothing, the AS; if there is no AS, the prefix; and failing that, text. A tn without AS is an integer, because that is almost always what whoever writes it means.
  • Dates. Through COM a date always arrives as a DateTime, so an AS Date would not convert it, and FoxCli warns you when you build. On the command line only YYYY-MM-DD is accepted, because 03/02/2026 is March in one place and February in another, and accepting it would mean reading it wrong without telling anyone.
  • Validating before running anything. If an argument is missing or an option is misspelt, your method never runs. The caller gets the reason, a suggestion if they were close, the command’s help and an exit code a script can read.
  • Keeping the documentation from falling behind. From that same comment come the console help, a docs\dbf.md with the commands, their options, their defaults and their exit codes, which works as a README as it is, and dbf describe, which gives the schema in JSON so an AI agent knows how to use the CLI without reading anything else.
  • No dialogs. A VFP error you do not catch goes out on the error channel with the file and the line, and the CLI ends. It never sits waiting for someone to click OK.

The HELP clause still works

Projects that already use it keep compiling and working the same, and both forms can be mixed in the same class: commands with HELP go through Run, and the rest go straight to their method. So a project can move to the native signature one command at a time, as each one needs changing.

To start from scratch, the tutorial Your first CLI in Visual FoxPro with FoxCli builds a complete tool with the native signature, step by step from the IDE. And if the console feels far away, the course CLIs for Fox developers explains what arguments, options and exit codes are.