Handling input parameters in your application

You are writing a core application or a basic utility and you want it to take parameters like myutility.exe verbose import=entry.log export=output.csv to indicate verbosity in the messages, also the file that will be read and finally, the file that will be generated after processing, but the question is: how to correctly detect them within the program? There are many different methods, I will demonstrate the way I use it over time which is simple and maintenance free. The example is for Delphi and Lazarus.

The examples shown use the command line, without a graphical interface, but it wouldn't make any difference if there was one, I would just change the WriteLn that displays a message in the terminal for another that displays using any dialog window.

Step #1: Where to check or read the parameters that were informed?

The first question to consider is: Where do I put my programming code to read the input parameters? The simplest answer is: the best place to read the input parameters is in the very first lines of execution of your program so that they can be used before the process starts. In Delphi and Lazarus the strictest point would be the Project file (.dpr or .lpr), but some prefer to do it in OnCreate of the first form to be presented. Anyway, the next question will be: How to determine if the informed parameters make the execution of the program or utility eligible, that is, which ones will be optional and which ones will be mandatory? And if no parameter is informed, should I proceed anyway?

Step #2: Are the parameters mandatory or optional?

In general, the main executable of a system would not need parameters, since the general idea is that it has a graphical interface and could have a configuration screen with checkboxes, listboxes and many other visual options to visually indicate the process to follow. will unfold, this is much simpler than using a command line prompt with parameters. However, many programmers create applications or utilities that support a main program and it is the main program that calls these support programs to handle certain functionality, for example, reading the log file that is encoded and exporting it to a decoded spreadsheet. In this case, the main application calls its support utilities using some parameters to inform the desired action. But the user could also – with the technical knowledge to it – run it directly from the command line, the system task scheduler and other methods as long as you pass the parameters correctly. So, the first thing we will need to know how to do is determine which parameters would be mandatory and which optional. Let's see what a code like this would look like?

var ErrorMsg:String; sParam:String; iParamCount:Cardinal; bHasImport:Boolean; bHasExport:Boolean; i:Integer; begin // you will not see the messages on the console using Windows because // there is an option in Project|Project Options| // Compiler Options|Config and Target|Win32 gui application (-WG) // which stays on, so instead of showing the messages in the terminal // it will show them in a ShowMessage. Leave it off // if you want to see the messages directly in the terminal iParamCount:=ParamCount; bHasImport:=false; bHasExport:=false; ErrorMsg:=emptyStr; if iParamCount=0 then begin ErrorMsg:='There are no parameters'; end else begin // in our example, two parameters will be required: // import=xxxx, export=yyyy for i:=1 to iParamCount do begin sParam:=ParamStr(i); if ContainsText(sParam, 'import=') then bHasImport:=true; if ContainsText(sParam, 'export=') then bHasExport:=true; end; if (ErrorMsg=emptyStr) and (not bHasImport) then ErrorMsg:='Missing parameter import=(...)'; if (ErrorMsg=emptyStr) and (not bHasExport) then ErrorMsg:='Missing parameter export=(...)'; end; if ErrorMsg<>emptyStr then begin writeln(stderr, ErrorMsg); Sleep(5000); // 5s wait Halt(2); // <-- Exit Code 2 to indicate to the caller that there was a failure end;

Some important lessons in the code above that we can learn:

  • the count of input parameters starts at index 1 and not at zero, as ParamStr(0) would return the name of the program itself.
  • we use the function ContainText which disregards uppercase and lowercase and correctly handles unicode characters, this is especially important on Windows because scripts can be created in the standard ansi(win1252), latin1(iso8859_1) and unicode. Many scripts just fail because of the encoding of the file and not because of the source code in it.
  • we had the care to give a 'exit code' if things went wrong, imagine your utility was used inside the task scheduler or a script, it would launch your program and need to know if your utility worked or failed to determine the next action.

Step #3: Reading input parameters, validating and processing

After the first check found that the parameters exist and the mandatory ones were informed then it's time to understand the parameters, some like "verbose" don't have an equal sign because it just designates the action, in this case, asking the program to issue more messages of text than it would ordinarily show. But there are also parameters with the equal sign “=”, what does that mean? The concept of equality in a stringlist is usually called a key/value or pairList, but here we are going to give it different names, to the left of the equal sign we will call it action (or verb) and to the right of the equality we have the action detail, for example , import=file.log is to say that I must import(action or verb) the .log file(detail of action). Let's go to the code:

var aList:TStringList; iParamCount:Cardinal; sParam:String; sImportFile:String; sExportFile:String; bHasVerbose:Boolean; ErrorMsg:String; i:Integer; begin ErrorMsg:=emptyStr; sImportFile:=emptyStr; sExportFile:=emptyStr; aList:=TStringList.Create; aList.CaseSensitive:=false; iParamCount:=ParamCount; // including all parameters in a stringlist, // you'll see how much easier it is to read for i:=1 to iParamCount do begin sParam:=ParamStr(i); // we don't want to duplicate equal parameters if aList.IndexOF(sParam)<0 then aList.Add(sParam); end; // Want verbosity? bHasVerbose:=(aList.IndexOfName('verbose')>=0); // looking if the parameter import=archive.log, it must exist to proceed if (ErrorMsg=emptyStr) and (aList.IndexOfName('import')>=0) then begin sImportFile:=aList.Values['import'] ; if not FileExists(sImportFile) then ErrorMsg:='File to import does not exist: '+sImportFile end else begin ErrorMsg:='Where is the parameter import=file.log!'; end; // looking if the parameter export=file.csv, it must not exist to proceed if (ErrorMsg=emptyStr) then begin if(aList.IndexOfName('export')>=0) then begin sExportFile:=aList.Values[' export']; if FileExists(sExportFile) then ErrorMsg:='File to export already exists: '+sExportFile end else begin ErrorMsg:='Where is the parameter export=arquivo.csv!'; end; end; if ErrorMsg<>emptyStr then begin writeln(stderr, ErrorMsg); Sleep(5000); // 5s wait Halt(2); // <-- Exit Code 2 to indicate to the caller that it failed end else begin // processing the rest of the program... if bHasVerbose then writeln(StdOut, 'Importing '+sImportFile+' and exporting '+sExportFile+'. ..') else writeln(StdOut, 'Importing...'); end; // Destroying objects aList.Free;

The other lessons we can learn from the code above:

  • The input parameters have their default values, the bVerbose, for example is false and sImportFile/sExportFile are empty and unless the input parameters change this, empty is not acceptable.
  • code with object creation – we had a TStringlist – we will avoid an 'exit' in the middle of the code, because in each exit we would have to remember to destroy these objects so it is easier instead of an exit, to control the flow through a variable ErrorMsg where instead of Exit, store the reason for the exit and then control the flow, while ErrorMsg is empty all lines are executed, otherwise ignored until they encounter a halt, terminate or end of code where all created objects will be destroyed.
  • we don't have one exit in the middle of the code, not only because we create objects at the beginning, but because it is a go to disguised and there are reasons why some people don't like him, on pascal, exit (or other forms of code bypass) misused creates a macaronic effect on the code making it difficult to understand the code or debug errors. But you don't have to be radical, personally I find an exit after some validations at the beginning of the code before the processing to be quite useful.
  • the process only starts after all validations have been done and ErrorMsg is empty, in fact, we use this variable to control the flow and reuse it to say what went wrong in case an error arose.
  • the Sleep(5000) command is just to be didactic, if you turned off the Project|Project Options|Compiler Options|Config and Target|Win32 gui application (-WG) option you will have time to see the error message in the terminal before it be closed.

Only for Lazarus and FreePascal

Lazarus, actually FreePascal(FPC) has a unit called CostApp, this library abstracts much of what we saw above with little code, we can check the presence of a parameter, even if it is of type key and value:

var ErrorMsg: String; sImportFile:String; begin // quickly checking the parameters ErrorMsg:=CheckOptions('i', 'import'); if ErrorMsg<>emptyStr then begin ShowException(Exception.Create(ErrorMsg)); Terminate; exit; end; if HasOption('i', 'import') then begin sImportFile:=getOptionValue('i', 'import')); end;  

The interesting thing about this library is that it simplifies the many different ways of passing parameters, for example, -v, -verbose, –verbose can be in a single call. But the CostApp for now it is only for Lazarus/FPC and could not be used in Delphi. You can find more information here:

https://wiki.freepascal.org/Command_line_parameters_and_environment_variables

If you need an example to study, download the demos from github:

https://github.com/gladiston/lazdemos_gsl

Criando utilitários de linha de comando com o Lazarus no Linux e Windows

Se estiver usando Lazarus/FPC, você deve tomar muito cuidado com as dependencias de projeto que necessitem do ambiente gráfico, um modo de inibir o uso de interface gráfica é ir nas opções do Projeto, isto é, Project|Options|Compiler Options|Configs and Targets e então ir em Select another LCL widgetset e trocar o conjunto de widgets para nogui, isso indicará ao compilador para não injetar dependencias com win23, gtk, qt,…ou qualquer conjunto de widgets do sistema operacional hospedeiro:

Mas a opção acima não faz milagre, se você acidentalmente usar um Application.MessageBox ou exibir um form acidentalmente, você carregará uma dependencia de pacote chamado de LCL que contém componentes com widgets visuais com interatividade com o usuário e daí então sua aplicação sendo para a linha de comando acidentalmente carregou as dependencia de widgets e ficará dificil usar seu utilitário num Linux, BSD, Solaris… que seja desprovido de interface grafica, mas funcionará no Windows – desde que não seja a versão Windows Core – porque ele tem UI o tempo inteiro.

Se sua intenção é criar um aplicativo cmd para interagir com aplicações REST, php, node ou qualquer linguagem server-side que chame pelo seu programa então é vital que nenhuma dependencia de interface gráfica seja incluida; Daí componentes não visuais e geradores de relatórios ao invés de usar forms, devem usar datamodules. Alguns dizem que formulários podem ser acrescidos ao projeto, mas jamais usar Show ou Showmodal neles, mas isso é ainda lenda para mim, terei de testar isso na prática mais tarde, por ora, recomendo que use apenas datamodules.

Ecoando mensagens no terminal do Windows

Diferentemente do Linux, BSD, Solaris, Mac,.. onde os programas mesmo com interface gráfica “ecoam” mensagens no terminal, no Windows os programas precisam ser disparados pelo terminal para que suas mensagens sejam vistas. Se você usa o Lazarus, você teria de compilar o programa e depois ir para o terminal para executá-lo para ver tais mensagens enviadas pelo comando Write. Porém há uma configuração que nos permite executar estes programas de terminal no Lazarus sem precisar ir para o prompt, vá em Project|Project Options|Compiler Options|Config and Target e desligue a opção Win32 gui aplication (-WG):

A opção Win32 gui aplication (-WG) nos permite ver a execução do programa no terminal sem abandonar a IDE. Mas quando o projeto estiver concluído, não é preciso estar habilitado.

Conclusion

In this article we learned some techniques for dealing with input parameters and other things besides, such as validations and the flow of execution of a program.