Lidando com parâmetros de entrada em seu aplicativo

Você esta escrevendo um aplicativo principal ou um utilitário básico e deseja que ele receba parâmetros como meuutilitario.exe verbose import=entrada.log export=saida.csv para indicar verbosidade nas mensagens, também o arquivo que será lido e por fim, o arquivo que será gerado após o processamento, mas a pergunta é: como detectá-los corretamente dentro do programa? Há muitos métodos diferentes, vou demonstrar a forma que uso ao longo do tempo que é simples e não dá manutenção. O exemplo serve para Delphi e Lazarus.

Os exemplos demonstrados usam a linha de comando, sem interface gráfica, mas não faria diferença se houvesse uma, apenas trocaria o WriteLn que exibe mensagem no terminal por outra que exiba usando uma janela de dialogo qualquer.

Passo #1: Onde verificar ou ler os parâmetros que foram informados?

A primeira pergunta a ser considerada é: Onde coloco meu código de programação para ler os parâmetros de entrada? A resposta mais simples é: o melhor lugar para ler os parâmetros de entrada é logo as primeiras linhas de execução de seu programa para que os mesmos possam ser usados antes do processo iniciar-se. No Delphi e no Lazarus o ponto mais estrito seria o arquivo de Projeto(.dpr ou .lpr), mas alguns preferem fazê-lo no OnCreate do primeiro formulário a ser apresentado. De qualquer forma, a pergunta seguinte será: Como determinar se os parâmetros informados tornam elegíveis a execução do programa ou utilitário, isto é, quais serão opcionais e quais serão obrigatórios? E se nenhum parâmetro for informado, devo prosseguir assim mesmo?

Passo #2: Os parâmetros são obrigatórios ou opcionais?

De maneira geral, o executável principal de um sistema não teria porque necessitar parâmetros, pois a ideia geral é que ele possui interface gráfica e poderia ter uma tela de configuração com checkboxes, listboxes e tantas outras opções visuais para indicar visualmente como o processo a seguir se desenrolará, isso é bem mais mais simples do que usar uma linha de comando do prompt com parâmetros. Contudo, muitos programadores criam aplicativos ou utilitários que servem de apoio a um programa principal e é o programa principal que chama esses programas de apoio para tratar de certas funcionalidades, por exemplo, ler o arquivo de log que está codificado e exportá-lo para uma planilha decodificada. Neste caso, o aplicativo principal chama seus utilitários de apoio com uso de alguns parâmetros para informar a ação desejada. Mas o usuário também poderia – com o conhecimento técnico para isso – executá-lo diretamente da linha de comando, o agendador de tarefas do sistema e outros métodos desde que passe os parâmetros corretamente. Assim, a primeira coisa que precisaremos saber como fazer é determinar, quais parâmetros seriam obrigatórios e quais opcionais. Vamos ver como ficaria um código assim?

var
  ErrorMsg:String;
  sParam:String;
  iParamCount:Cardinal;
  bHasImport:Boolean;
  bHasExport:Boolean;
  i:Integer;
begin
  // você não verá as mensagens na console usando o Windows porque
  // há uma opção em Project|Project Options|
  // Compiler Options|Config and Target|Win32 gui aplication (-WG)
  // que fica ligada, assim ao inves de mostrar no terminal
  // as mensagens ele a mostrará num ShowMessage. Deixe-a desligada
  // se quiser ver as mensagens diretamente no terminal
  iParamCount:=ParamCount;
  bHasImport:=false;
  bHasExport:=false;
  ErrorMsg:=emptyStr;
  if iParamCount=0 then
  begin
    ErrorMsg:='Não há parâmetros';
  end
  else
  begin
    // em nosso exemplo, dois parâmetros serão obrigatórios:
    //  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:='Falta o parametro import=(...)';
    if (ErrorMsg=emptyStr) and (not bHasExport) then
      ErrorMsg:='Falta o parametro export=(...)';
  end;
  if ErrorMsg<>emptyStr then
  begin
    writeln(stderr, ErrorMsg);
    Sleep(5000); // 5s de espera
    Halt(2); // <-- Exit Code 2 para indicar a quem o chamou que houve falha
  end;

Algumas lições importantes no código acima que podemos aprender:

  • a contagem dos parâmetros de entrada começa no índice 1 e não no zero, pois ParamStr(0) retornaria o nome do próprio programa.
  • usamos a função ContainText que desconsidera maiúsculos e minúsculos e trata corretamente caracteres unicode, isso é importante especialmente no Windows porque scripts podem ser criados no padrão ansi(win1252), latin1(iso8859_1) e unicode. Muitos scripts falham apenas por causa da codificação do arquivo e não por causa do código fonte nele.
  • tivemos o esmero de dar um ‘exit code‘ caso as coisas dessem errado, imagine que seu utilitário fosse usado dentro do agendador de tarefas ou um script, ele dispararia seu programa e precisaria saber se seu utilitário funcionou ou falhou para determinar a ação seguinte.

Passo #3: Lendo os parâmetros de entrada, validando e processando

Depois que a primeira checagem considerou que os parâmetros existem e os obrigatórios foram informados então é hora de compreender os parâmetros, alguns como “verbose” não tem sinal de igualdade porque designa apenas a ação, neste caso, solicitando que o programa emita mais mensagens de texto do que comumente mostraria. Mas também há parâmetros com o sinal de igualdade “=”, o que isso significa? O conceito de igualdade numa stringlist normalmente é chamado de chave/valor ou pairList, mas aqui vamos dar nomes diferentes, a esquerda do sinal de igualdade vamos chamar de ação(ou verbo) e a direita da igualdade temos o detalhe da ação, por exemplo, import=arquivo.log é para dizer que devo importar(ação ou verbo) o arquivo.log(detalhe da ação). Vamos ao código:

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;
  // incluindo todos os parâmetros numa stringlist, 
  // verá como isso facilitará muito a leitura
  for i:=1 to iParamCount do
  begin
    sParam:=ParamStr(i);
    // não queremos duplicar parâmetros iguais
    if aList.IndexOF(sParam)<0 then
      aList.Add(sParam);
  end;
  // Quero verbosidade?
  bHasVerbose:=(aList.IndexOfName('verbose')>=0);
  // olhando se o parametro import=arquivo.log, ele deve existir para prosseguir
  if (ErrorMsg=emptyStr) and (aList.IndexOfName('import')>=0) then
  begin
    sImportFile:=aList.Values['import'];
    if not FileExists(sImportFile) then
      ErrorMsg:='Arquivo para importar não existe: '+sImportFile
  end
  else
  begin
    ErrorMsg:='Cadê o parâmetro import=arquivo.log!';
  end;
  // olhando se o parâmetro export=arquivo.csv, ele não deve existir para prosseguir
  if (ErrorMsg=emptyStr) then
  begin
    if(aList.IndexOfName('export')>=0) then
    begin
      sExportFile:=aList.Values['export'];
      if FileExists(sExportFile) then
        ErrorMsg:='Arquivo para exportar já existe: '+sExportFile
    end
    else
    begin
      ErrorMsg:='Cadê o parâmetro export=arquivo.csv!';
    end;
  end;

  if ErrorMsg<>emptyStr then
  begin
    writeln(stderr, ErrorMsg);
    Sleep(5000); // 5s de espera
    Halt(2); // <-- Exit Code 2 para indicar a quem o chamou que houve falha
  end
  else
  begin
    // processando o restante do programa...
    if bHasVerbose then
     writeln(StdOut, 'Importando '+sImportFile+' e exportando '+sExportFile+'...')
    else
     writeln(StdOut, 'Importando...');
  end; 

  // Destruindo objetos
  aList.Free;

O utras lições que podemos aprender com o código acima:

  • Os parâmetros de entrada tem seus valores padrões, o bVerbose, por exemplo é falso e sImportFile/sExportFile são vazios e a menos que os parâmetros de entrada mudem isso, vazio não é será aceitável.
  • códigos com criação de objetos – tivemos um TStringlist – evitaremos um ‘exit’ no meio do código, pois em cada exit teríamos de lembrar de destruir esses objetos então é mais fácil ao invés de um exit, controlar o fluxo por meio de uma variável ErrorMsg onde ao invés do Exit, guardar nela o motivo do exit e daí então controlar o fluxo, enquanto ErrorMsg for vazia todas as linhas são executadas, de outra forma, ignoradas até que encontrem um halt, terminate ou o final do código onde todos os objetos criados serão destruídos.
  • Não temos um exit no meio do código, não apenas porque criamos objetos no inicio, mas porque ele é um goto disfarçado e há razões para alguns não gostarem dele, no pascal, exit (ou outras formas de desvio de código) mal utilizado cria um efeito macarrônico no código tornando difícil o entendimento do código ou depuração de erros. Mas não é preciso ser radical, pessoalmente considero um exit depois de algumas validações no inicio do código que antecede ao processamento bastante útil.
  • o processo só começa depois que todas as validações foram feitas e ErrorMsg está vazio, de fato, usamos essa variável para controlar o fluxo e reutilizamos ela para dizer o que houve de errado caso tenha surgido um erro.
  • o comando Sleep(5000) é apenas para ser didático, se você desligou a opção Project|Project Options|Compiler Options|Config and Target|Win32 gui aplication (-WG) você terá tempo de ver a mensagem de erro no terminal antes da mesma ser fechada.

Só para Lazarus e FreePascal

O Lazarus, na realidade o FreePascal(FPC) possui uma unit chamada CustApp, essa biblioteca abstrai muito do que vimos acima com pouco código podemos conferir a presença de um parâmetro, mesmo que ele seja do tipo chave e valor:

var
  ErrorMsg: String;
  sImportFile:String;
begin
  // conferindo rapidamente os parametros
  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;  

O interessante dessa biblioteca é que ela simplifica as várias formas diferentes de passagem de parâmetros, por exemplo, -v, -verbose, –verbose podem estar numa única chamada. Mas o CustApp por enquanto é apenas para Lazarus/FPC e não poderia ser usado no Delphi. Você encontra mais informações aqui:

https://wiki.freepascal.org/Command_line_parameters_and_environment_variables

Se precisar de um exemplo para estudar, baixe os demos no 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.

Conclusão

Neste artigo aprendemos algumas técnicas para lidar com parâmetros de entrada e outras coisas além, como validações e fluxo de execução de um programa.