Ir para o conteúdo

Passagem de Parâmetros em linha de comandos

Aqui fica um pequeno tutorial sobre como lidar com parâmetros em pascal.

Os interfaces por linha de comando (CLI, command-line interface) são, na minha opinião, os mais flexíveis e eficientes. Longe de serem os mais amigáveis para utilizadores novos, os CLI permitem aos utilizadores experientes comunicar aos programas, de forma rápida, concisa e precisa, o que pretendem fazer e como querem fazê-lo.

Uma vez que a maioria dos programadores iniciados faz apenas programas CLI (pelo menos nos exercícios académicos), penso que é interessante demonstrar como lidar com parâmetros passados aos nossos programas na linha de comandos.

ParamCount e ParamStr

Nos sistemas operativos que suportem passagem de parâmetros aos programas, os programas Pascal disponibilizam duas funções importantes:

  • ParamCount
  • ParamStr

Podemos então obter o número de parâmetros passados ao programa com ParamCount e os parâmetros específicos com ParamStr(idx), sendo idx um inteiro contido em 1..ParamCount.

Existe uma peculiaridade sobre ParamStr. Se lhe passarmos o valor 0, esta função irá devolver-nos o caminho até ao nosso programa, acrescido do nome do executável.

program args1;

begin
  writeln(ParamCount, ' parâmetros.');
  writeln('caminho: ', ParamStr(0));
end.
C:\> args1.exe
0 parâmetros.
caminho: c:\args1.exe

Se quisermos então enumerar os parâmetros passados, poderíamos fazer algo como:

program args2;

var
  i: integer;
begin
  writeln(ParamCount, ' parâmetros.');
  for i := 1 to ParamCount do
    writeln(i, 'º parâmetro: ', ParamStr(i));
end.
C:\> args2.exe --isto --aquilo
2 parâmetros.
1º parâmetro: --isto
2º parâmetro: --aquilo

Qual é a utilidade de tudo isto? Bem, imaginem que querem fazer um programa que leia ou escreva um conjunto de registos de alunos num ficheiro. Podemos fazer com que o nome desse ficheiro seja especificado nos parâmetros que o utilizador lhe passar. Para isso, basta guardarmos essa string numa variável para utilização posterior. Por exemplo:

program args3;

var
  f: file of integer;
begin
  assign(f, ParamStr(1));
  rewrite(f);
  write(f, 42);
  close(f);
end.
program args4;

var
  f: file of integer;
  i: integer;
begin
  assign(f, ParamStr(1));
  reset(f);
  read(f, i);
  close(f);
  writeln('Número lido: ', i);
end.
C:\> args3.exe teste
C:\> args4.exe teste
Número lido: 42

Podíamos agora fazer coisas mais complexas, como, por exemplo, um programa que adicione um aluno a uma base de dados presente num ficheiro (deixo o código a cargo da vossa imaginação):

C:\> programa.exe adicionar Pedro 24
Adicionado aluno "Pedro" de 24 anos.

O que interessa reparar neste último exemplo é que os parâmetros estão numa ordem definida. O que nós queremos mesmo é fazer algo como isto:

C:\> programa.exe alunos.db --name Pedro --age 24

Uma pequena nota: eu utilizo o prefixo -- para os parâmetros por ser o mais comum em *nix; pessoas do Windows estarão mais habituadas a algo como /add /name Pedro /age 24.

Vamos então ver como seria um programa desses:

program args5;

uses sysutils;

type
  RAluno = record
    nome: string[60];
    idade: byte;
  end;

var
  aluno: RAluno;
  db: file of RAluno;
  i: integer;
  fname: string;
begin
  fname := ParamStr(1);
  i := 2;
  while i < ParamCount do
  begin
    if ParamStr(i) = '--name' then
    begin
      // Falaremos deste ParamStr(i + 1) a seguir
      aluno.nome := ParamStr(i + 1);
      inc(i);
    end
    else if ParamStr(i) = '--age' then
    begin
      // Atenção nesta linha:
      // a função ParamStr devolve-nos strings, mas o
      // campo idade é um byte (uma espécie de inteiro)
      // e temos que fazer a conversão com StrToInt.
      aluno.idade := strtoint(ParamStr(i + 1));
      inc(i);
    end;
    inc(i);
  end;

  assign(db, fname);
  if fileexists(fname) then
    reset(db)
  else
    rewrite(db);
  // colocamos o cursor no final do ficheiro
  seek(db, filesize(db));
  write(db, aluno);
  close(db);
end.

Relativamente ao ParamStr(i + 1), basta pensar um pouco: o parâmetro actual (ex.: --name) tem índice i, mas nós queremos guardar em alunos.nome o parâmetro seguinte, de índice i + 1. Por esta razão, tecnicamente nós lemos 2 parâmetros e não apenas um, o que implica a utilização de um inc(i) no final da condição, de forma a que o ciclo não tente processar o parâmetro que contém o nome.

Se quiserem confirmar que funciona, compilem e executem este programa:

program args6;

type
  RAluno = record
    nome: string[60];
    idade: byte;
  end;

var
  f: file of RAluno;
  n: integer = 0;
  a: RAluno;
begin
  assign(f, ParamStr(1));
  reset(f);
  while not eof(f) do
  begin
    read(f, a);
    writeln(a.nome, ', ', a.idade, ' anos.');
    inc(n);
  end;
  close(f);
  writeln(n, ' aluno(s) lido(s).');
end.
C:\> args5.exe teste --name Pedro --age 24
C:\> args6.exe teste
Pedro, 24 anos.
1 aluno(s) lido(s).
C:\> args5.exe teste --age 22 --name Raquel
C:\> args6.exe teste
Pedro, 24 anos.
Raquel, 22 anos.
2 aluno(s) lido(s).

E pronto, é tudo. Espero ter avivado a curiosidade de alguns de vós acerca deste tema. Atenção, o código acima foi testado com fpc em linux. Alterei parte dos outputs para ficar mais familiar (i.e. Windows). Peço desculpa pelo comprimento do tutorial e por eventuais desvios do assunto. Em compensação, aqui fica um zip com os ficheiros utilizados: pascal-arguments.zip.

Adenda importante

No código exemplificado optei por não fazer verificações essenciais como certificar-me que o utilizador introduziu o número de parâmetros necessários, verificar se o comando assign não deu erro e outros que tais pela simples razão de que isto são meros exemplos, e essas verificações iriam certamente aumentar o tamanho do código desnecessariamente. Obviamente, programas seguros e robustos devem conter sempre essas verificações.

O ParamStr(0)

Como foi dito, os parametros vão de 1 a ParamCount. Mas temos ainda um outro parâmetro válido, que é o próprio executável.

Se usarmos:

  s:= ParamStr(0);

A variável s irá guardar o caminho completo (inclusivé o nome do executável) do nosso programa.

Isto é útil para pelo menos duas coisas: * Verificar se o executável tem o nome original (caso não queiramos que o utilizador possa mudar o nome do executável por algum motivo)

  • Saber em que pasta está guardardo o executável.

Esta última, é lógico, é a mais importante. Se quisermos que o nosso software abra ou grave ficheiros na mesma pasta onde está guardado o executável (por exemplo, um ficheiro de configuração ou uma base de dados), podemos usar o seguinte: Em FreePascal, Delphi, e outros com acesso à função ExtractFilePath (SysUtils)

  Path:=ExtractFilePath(ParamStr(0));

Outros compiladores Pascal sem acesso à ExtractFilePath:

function GetExePath:String;
var
   s,r:String;
   Copia:Boolean;
   i:integer;
begin
   s:=ParamStr(0); // Atribuimos à variável S o caminho completo do executável
   r:=''; // Limpa a variável de resultado
   Copia:=False;  // Assume à partida que não é para copiar caracteres
   i:=length(s);  // Atribui a i o tamanho do caminho completo
   repeat
      if s[i] in ['\','/'] then Copia:=True;  // Quando encontrar a primeira '\' ou '/' activa a cópia (a contar da direita para a esquerda)
      if Copia then r:=s[i]+r;  // Se já tiver encontrado uma '\' ou '/' (já descartou o ficheiro), copia o caracter lido para a variável de resultado
      dec(i);  // Anda para trás um caracter
   until i=0;  // Pára quando I for 0
   GetExePath:=r;  // Devolve o resultado
end;

Deste modo é fácil atribuir a ficheiros o caminho do executável.