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 5It 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
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
ENDFUNCIt 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.,-fadds the short form,:intsets the type,:csv|xlscloses 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
Runand hand out the commands with aDO CASE. Every new command is one more branch, and a command you declare and forget to add to theCASEdoes 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 withAsNumber()and the text withGet(). - 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
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
ENDPROCIt 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
Runand noDO CASE. - Each parameter is an option, named without its prefix:
tcFormatois--formatoandtnMaxis--max. The type comes from theAS. - The
*!*block adds what the signature does not say.<>makes the argument unnamed,-fis the short form,:csv|xlsis the list of values and=csvthe default. A parameter without=is required, and the rest of the line is the description. - The values arrive in the parameters, already converted.
tnMaxis a number, andtcFormatoarrives 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_TABLEends with the 10 I declared infoxcli.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 posicionalesA 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, xlsIn 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.dbfThe 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.
tcNombregives--nombre, buttotalstays--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 noAS, the prefix; and failing that, text. AtnwithoutASis 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 Datewould not convert it, and FoxCli warns you when you build. On the command line onlyYYYY-MM-DDis accepted, because03/02/2026is 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.mdwith the commands, their options, their defaults and their exit codes, which works as a README as it is, anddbf 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.