int21h
Rozbor příkazového řádku
Řeklo by se banální téma. To je ale jenom zdání. Stejně jako minule, když jsem psal o řetězcových typech, i tady záhy zjistíte, že problematika rozhodně není triviální.
Pascal má na rozbor příkazového řádku dvě funkce:
ParamCount:byte - vrátí počet argumentů
ParamStr(n:byte):string - vrátí N-tý argument příkazového řádku
Když není argument zadán, vrací funkce
ParamStr prázdný řetězec. Volání
ParamStr(0), vrátí jméno EXE souboru. Jestliže je EXE spuštěn z jiného adresáře než z aktuálního
(např. voláte C:TPMUJPRG anebo je program v adresáři pokrytém proměnnou PATH), tak ho vrací i s cestou. Pokud jsme ve stejném adresáři jako EXE, tak ho vrátí bez cesty.
Takže jestliže chceme zjistit jméno spuštěného programu, tak je rozumné volat
prg:=FExpand(ParamStr(0))
Dále doporučuji vyseparovat adresář. K tomu nám slouží funkce
FSplit - tedy:
FSplit(prg,cesta,jmeno,koncovka)
Tímto jsme zjistili adresář, ve kterém se nachází náš exáč. Znovu připomínám, že nemusí být totožný s aktuálním adresářem. Pokud váš program pracuje s jinými soubory, tak je nanejvýš vhodné udávat i zjištěnou cestu. Tedy nikoliv
Assign(f,s) ale
Assign(f,cesta+s) Takto uživateli umožníte spouštět váš program ze vzdáleného adresáře.
Zpět ale k parametrům příkazového řádku.
Funkce
ParamCount a
ParamStr mají dva zásadní nedostatky. Prvním je, že jako oddělovače jednotlivých argumentů chápe znak mezeru. Už ale neodliší, jestli mezi prvním a druhým argumentem je jedna mezera anebo deset mezer. Většinou je to samozřejmě jedno, ale v některých případech to může být toto chování na závadu. Pro tyto účely musíme získat přesnou kopii příkazového řádku.
DOS na to poskytuje jeden anebo dva mechanizmy - podle verze.
První mechanismus funguje ve všech verzích DOSu - zkopírování řetězce z
PSP (prefix segmentu programu)
PSP je tabulka v paměti, kterou DOS vytváří pro každý spuštěný program. Na offsetu
80h je uložena délka příkazového řádku. A od offsetu
81h do
FFh jsou znaky příkazového řádku. Pozor - součástí příkazového řádku není jméno EXE. Příkazy přesměrování vstupu a výstupu se tu také neobjeví. Jinak je to přesná kopie včetně všech mezer.
(volání PROGRAM param1 param2 > vystup.txt
vyprodukuje param1 param2
)
Otázka tedy zní: "Jak se dostat k PSP?"
V turbo pascalu velice jednoduše. TP definuje proměnnou
PrefixSeg, což je ukazatel právě na PSP.
Procedure VratPrRadku:string;
var s:^string;
begin
s:=Ptr(PrefixSeg,$80);
VratPrRadku:=s^;
end;
Jednoduché že?
Možná jste si povšimli, že naznačuji, že ve Freepascalu je to poněkud složitější než v TP. Přesně tak. V prvé řadě Freepascal nedefinuje proměnnou
PrefixSeg. Segmentovou adresu PSP můžeme nicméně zjistit funkcí
INT21h/AH=62. Jenže bacha na jednu věc. Vy to nemůžete napsat přímo.
NELZE udělat toto:
var PrefixSeg:word;
begin
asm
mov ah,62h;int 21h;mov PrefixSeg,bx
end;
...
end;
Uvědomme si, že DPMI program není ve všech aspektech pravý DOSový program. DPMI program není spouštěn DOSem, nýbrž DPMI správcem. Ten pracuje jinak a PSP má jinou. Takže?
Ve čvrtém čísle jsem psal o volání funkcí z DPMI prostředí. Píšu tam, že pokud je návrat funkce uložen v bufferu, je místo přímého volání nutno použít emulaci pomocí služeb DPMI, a že pokud je návrat funkce jenom v registrech, tak přímo volat lze. Nu - tak tohle je zrovna výjimka. Návratová hodnota je jenom v registru, ale funkci volat přímo z výše popsaného důvodu nelze. DPMI nám ale pomůže a službu emuluje.
Pro FP bude tedy celá funkce vypadat takto:
Procedure VratPrRadku:string;
var t:string;
w:word;
r:registers;
begin
r.ah:=$62;MsDos(r);w:=r.bx;
DOSmemGet(w,$80,t,$ff-$80+1);
VratPrRadku:=t;
end;
Výborně. Právě jsme ovládli první metodu. Psal jsem ale, že metody máme dvě. Jaká je druhá metoda?
Otázka v prvé řadě stojí: "Proč se zabývat nějakou druhou metodou?"
PSP totiž udrží jenom řetězec do délky
127 znaků. Pokud očekáváme delší, tak máme smůlu.
Právě proto některé DOSy (MS-DOS 7.10, FreeDOS, různé nestandardní shelly...) mají proměnnou prostředí
CMDLINE. Sem DOS zkopíruje celou příkazovou řádku, a to i pokud je delší než 127 znaků. Narozdíl od metody s PSP je tu obsaženo i jméno spuštěného EXE. Jestliže chceme dosáhnout shodného chování s předchozí metodou, musíme jméno EXE odstranit. Jak odstranit jméno EXE souboru?
Kdo tvrdí, že stačí najít první mezeru a všechno až do této mezery smazat, NEMÁ PRAVDU. Uvědomte si, že názvy souborů v LFN (dlouhá jména souborů) mohou obsahovat i mezeru. V takovém případě je celý název souboru uzavřen do uvozovek.
Zkrátka: tady máte kompletní verzi funkce
VratPrRadku, která zná obě metody a pracuje v TP i FP.
Function VratPrRadku:string;
Var s:pstring;
t:string;
w:word;
r:registers;
b:byte;
begin
t:=GetEnv('CMDLINE');
if t<>'' then
begin
if t[1]='"' then
begin
delete(t,1,1);
b:=Pos('"',t);
delete(t,1,b);
end
else begin
b:=Pos(' ',t);
delete(t,1,b);
end;
VratPrRadku:=t;
end
else begin
r.ah:=$62;MsDos(r);w:=r.bx;
DOSmemGet(w,$80,t,$ff-$80+1);
VratPrRadku:=t;
s:=Ptr(PrefixSeg,$80);
VratPrRadku:=s^;
end;
end;
Báječné. Právě jsme vyřešili problém vícečetných mezer v řádku. Zbývá druhý problém. Už jsme ho naťukli. Názvy souborů s mezerou se uzavírají do uvozovek a pak jsou chápány jako jediný argument.
V DOSu je například přípustný tento příkaz:
md "Moje fotky"
- vytvoří se adresář "Moje fotky". Rovněž například síťová utilita
send z Netware má tento tvar:
send "nejaky klidne dosti dlouhy text"
Toto pascalovské ParamCount/ParamStr neznají.
Postup je tedy nabíledni: pomocí funkce
VratPrRadku zjistit příkazovou řádku a pak ji vlastními silami analyzovat. Myslím, že byste to vymysleli sami, ale pro vaše pohodlí vám dám tyto funkce už hotové:
function LFNParamCount(s:string):integer;
var b,o:boolean;
c,x:byte;
t:string;
begin
if s='' then begin LFNParamCount:=0;Exit;end;
b:=false;
o:=false;
t:='';
x:=1;
for c:=1 to Length(s) do
if s[c]='"' then begin if b=false then inc(x);b:=not b;end else
if b=false then
if s[c]=' ' then o:=false
else if o=false then
begin
inc(x);
o:=true;
end;
if b=true then begin LFNParamCount:=-1;Exit;end;
LFNParamCount:=x-1;
end;
function LFNParamStr(s:string;n:byte):string;
function Mid (S:string; B,E:longint):string;
var i, N:longint;
Pom :string;
begin
N:=Length(S);
Pom:='';
if B>0 then
for i:=B to E do
if i<=N then Pom:=Pom+S[i];
Mid:=Pom;
end;
function Search (Text:ansistring;S:string;Pos:longint):longint;
var i, D, N: longint;
begin
D:=Length(S);
N:=Length(Text);
i:=Pos;
while i<=N-D+1 do
begin
if S = Mid (Text, i, i + D - 1) then begin Search:=i;exit;end;
inc (i);
end;
Search:=0;
end;
var b,o:boolean;
c,d:byte;
x:shortint;
t:string;
begin
if s='' then begin LFNParamStr:='';Exit;end;
b:=false;
o:=false;
t:='';
x:=0;
for c:=1 to Length(s) do
if s[c]='"' then
begin
if b=false then
begin
inc(x);
if x=n then
begin
d:=Search(s,'"',c+1);
if d=0 then begin LFNParamStr:='';Exit;end;
LFNParamStr:=Mid(s,c+1,d-1);
Exit;
end;
end;
b:=not b;
end else
if b=false then
if s[c]=' ' then o:=false
else if o=false then
begin
inc(x);
if x=n then
begin
d:=Search(s,' ',c);
if d=0 then d:=Length(s)+1;
LFNParamStr:=Mid(s,c,d-1);
Exit;
end;
o:=true;
end;
LFNParamStr:='';
end;
Funkce LFNParamStr by se zřejmě dala zjednodušit, ale dávám vám ji prostě tak, jak jsem ji napsal.
Na závěr několik rad starého dosařského pardála:
1) určitě máte nějakou svoji unitku, kterou používáte prakticky ve všech svých programech. Doporučuji vám včlenit do ní uvedené funkce a v inicializační části jednotky volat funkci
VratPrRadku a zjištěný řetězec uložit do nějaké globální proměnné jednotky (deklarované v sekci interface).
2) rovněž si do proměnných uložte adresář s EXE a jméno EXE bez koncovky.
3) jestliže váš program přijímá jako argument specifikaci souboru ve hvězdičkové konvenci (
PROGRAM *.txt
), tak je dobré, aby podporoval i fajllisty (
PROGRAM @filelist.lst
) Jestli jste maximalisti, tak to navíc udělejte tak, aby rovněž ve fajllistech mohly být uvedeny masky ve hvězdičkové konvenci.
A to je vše.
DOS-u-akbar!