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.