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:
{v uses musi byt uvedena jednotka Go32}
Procedure VratPrRadku:string;
var t:string;
    w:word;
    r:registers;
begin
r.ah:=$62;MsDos(r);w:=r.bx;       {ziskani segmentu PSP}
DOSmemGet(w,$80,t,$ff-$80+1);     {kopirovani z PSP do programu}
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           {existuje promenna CMDLINE?}
   begin
   if t[1]='"' then     {jmeno souboru je v LFN a je v nem mezera?}
      begin
      delete(t,1,1);    {odstran prvni uvozovky}
      b:=Pos('"',t);    {najdi dalsi}
      delete(t,1,b);    {a vsechno az k nim smaz}
      end
      else begin
      b:=Pos(' ',t);    {najdi prvni mezeru}
      delete(t,1,b);    {vsechno az k ni smaz}
      end;
   VratPrRadku:=t;
   end
   else begin
   {$IFDEF FPC}
   r.ah:=$62;MsDos(r);w:=r.bx;       {a mame segment PSP}
   DOSmemGet(w,$80,t,$ff-$80+1);
   VratPrRadku:=t;
   {$ELSE}
   s:=Ptr(PrefixSeg,$80);
   VratPrRadku:=s^;
   {$ENDIF}
   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;
   { Replaces standard "ParamCount" function }
   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;
   { Vraci cast podretezce S pocinaje B-tym znakem a E-tym konce}
   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;
   { Funkce hleda string S v textu Text od pozice Pos (vcetne)}
   var i, D, N: longint;
   begin {Search}
   D:=Length(S);       {delka hledaneho retezce}
   N:=Length(Text);    {delka textu}
   i:=Pos;
   while i<=N-D+1 do
      begin {while}
      if  S = Mid (Text, i, i + D - 1) then begin Search:=i;exit;end;
      inc (i);
      end;  {while}
   Search:=0;
   end; {Search}


   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!
2006-12-06 | Laaca
Reklamy:
„Pes, kterého uzdravíš, tě nikdy nekousne. To je hlavní rozdíl mezi zvířetem a člověkem.“ Mark Twain