Konsolprogrammer - En parser

Tags:    delphi
Skrevet af Bruger #58 @ 13.10.2002
Konsolprogrammer part 2 - Objekt-orienteret parser

I denne artikel vil jeg fortælle hvordan man laver en klasse (en støbeform til objekter) og hvordan man kan parse en kommando ind i ord. Der er meget teori om opbygningen af units og klasser i Delphi.

Denne artikel er en direkte fortsættelse af part 1 "Start med konsolprogrammer". Hvis du ikke har læst den bør du gøre det nu.


En generisk kommando-parser

En generisk (ikke genetisk :) kommando-parser er meget praktisk og kan bruges til mere end bare en kommandofortolker! Et andet eksempel på hvor sådan en kunne være brugbar ville være en IRC bot/klient (som nærmest er en kommandofortolker som modtager kommandoer over netværket). Du kan selv prøve at finde på andre steder sådan en kunne være brugbar.

Siden din første kommandofortolker er temmelig lille og simpel kan det lige så godt betale sig at skrive den helt om hvis den skal behandle kommandoer på en helt ny måde. Så før vi går videre med den nye kommandofortolker, gem den gamle (hvis du vil) og lav endnu et nyt konsolprogram. Denne gang skriver vi ikke nogen kode i hovedprogrammet endnu, men vi vil i stedet lave en ny unit for bedre at holde styr på koden. Så når du har dit nye, tomme konsolprogram, så vælg File -> New -> Unit. Den tomme unit skal se sådan her ud:

unit Unit1;

interface

implementation

end.
Gem allerede nu det tomme projekt og den tomme unit. Jeg vil foreslå at du kalder projektet for ”fortolker2”. Unitten skal du kalde ”Kommandoparser”. Delphi ændrer automatisk unittens navn (det efter unit nøgleordet) til Kommandoparser.

De to nøgleord interface og implementation deler unitten op i to logiske dele som det er vigtigt at forstå forskellen på og sammenhængen mellem. Al din egentlige kode (dvs. funktionskald, if-sætninger og så videre) vil altid være i implementationen, det er det eneste sted der kan være eksekverbar kode. Interfacet kan kun indeholde erklæringer (deklarationer) af symboler/navne og ikke eksekverbar kode. Alting som er erklæret i interfacet skal implementeres i implementationen i samme unit. Man kan dog også have erklæringer i implementationen af sin unit, forskellen på om en erklæring ligger i interfacet eller implementationen er, at alting som er erklæret i interfacet kan ses og benyttes af andre units der har unitten i sin uses klausul, mens ting som er erklæret i implemen­tationen er private for denne unit. Jeg håber det var til at forstå, men ellers kommer det normalt hen ad vejen.

Nu skal vi definere en klasse. En klasse er en ”skabelon” til et objekt, som er en variabel som kan have metoder og egenskaber med værdier. Et eksempel på objekter du allerede kender er forms og kontroller. Disse klasser implementerer GUI funktionerne i Windows, altså gør det lettere at arbejde med den grafiske side af Windows. Dette er et glimrende eksempel på en af de mange anvendelser af klasser: indkapsling af komplekse ting i nogen logisk opbyggede klasser som gør det nemt at arbejde med, uden at den som bruger klasserne har behov for at vide hvordan de virker ”bag ved scenen”.

Det er til en vis grad også en indkapsling vi skal lave med en kommando-parser klasse, vi skal indkapsle funktionerne til at parse en kommando så man ikke behøver tænke nærmere over hvordan det rent teknisk foregår når først klassen er erklæret og impålementeret.

Lad os starte med at erklære vores klasse. Denne erklæring skal stå i interfacet fordi andre units gerne skulle kunne have gavn af vores klasse.

type
  TKommandoParser = class
  private
    FKommando: string;
    FParametre: TStringList;
    FKommandolinie: string;
    function GetParameterCount: integer;
    function GetParameter(Index: integer): string;
  public
    constructor Create(const AKommandolinie: string);
    destructor Destroy; override;
    function GetRange(Fra, Til: integer): string;
    propertyKommando: string
      read  FKommando;
    property Kommandolinie: string
      read  FKommandolinie;
    property ParameterCount: integer
      read  GetParameterCount;
    property Parameter[Index: integer]: string
      read  GetParameter;
  end;
Første linie, type, fortæller simpelthen Delphi at vi skal til at erklære en eller flere datatyper. Den næste linie definerer TKommandoParser typen som en klasse. (Det er god kodeskik at starte navnet på alle sine datatyper med et stort T for at vise ”dette er en type”.) Denne klasse er delt ind i to dele, private og public. Alting som ligger under private kan kun ses af klassen selv og er altså skjult for alle som benytter klassen. Det som ligger i public kan alle derimod se og benytte sig af.

I den private sektion erklærer vi først tre feltvariabler, dvs. private datavariabler. (Navnene på feltvariabler bør altid starte med F.) Efter de tre variabler erklærer vi to funktioner til at hente værdierne for to egenskaber (properties).

I den offentlige (public) sektion er der først erklæret to specielle metoder (metode er et fælles navn for funktioner og procedurer i klasser), Create og Destroy. Dem bruger man når man hhv. instantierer klassen (opretter et objekt af den klasse) eller destruerer en instans (sletter det oprettede objekt). Efter Create og Destroy er der erklæret en metode, GetRange, som skal hente et antal parameter som følger efter hinanden.

Derefter følger fire egenskaber. En egenskab (property) ligner (når man bruger den) en variabel på de fleste punkter, men forskellen er at man grundlæggende kan hente og tildele værdier på to måder, enten direkte fra en feltvariabel eller vha. en metode i klassen. Egenskaber behøver ikke både kunne læses og skrives, man kan godt lade være at give en metode til at læse eller skrive, så bliver egenskaben read-only eller write-only.

I vores klasse er alle egenskaber read-only, fordi der ikke er defineret en write klausul for dem. De to første egenskaber er helt standard read-only egenskaber som får deres værdi fra en feltvariabel, som der er givet navnet på i read klausulen. ParameterCount egenskaben har i stedet for en feltvariabel en privat metode til at hente værdien. Den sidste egenskab er indekseret, dvs. at den ligner en array (tabel, liste). Indekserede egenskaber skal have en læse-metode som tager et argument med det givne indeks.

Hvis du prøver at kompilere din kode nu vil Delphi give en fejl, fordi ingen af de erklærede metoder i klassen er implementeret. Hvis du har Delphi 4 eller nyere, og mindst en Professional version kan du placere tekstmarkøren et sted i erklæringen af klassen og trykke Ctrl+Shift+C. Så vil Delphi automatisk oprettet skeletter til implementationen af alle metoder i klassen. (Pas på, ICQ bruger også den genvejstast! Hvis du har ICQ bør du enten lukke det eller omdefinere den tast.)

Hvis du ikke har en version af Delphi med Class Completion bliver du nødt til manuelt at skrive skeletterne til alle metoderne. For at gøre det lettere dig har jeg lagt unitten med kodeskeletter til download.

Nu kan unitten næsten kompilere, bortset fra en enkelt ting, Delphi klager over at den ikke kender TStringList som vi bruger i FParameter feltet. TStringList er defineret i Classes unitten, så den skal vi putte i vores uses klausul. Vi vil også få brug for ting fra SysUtils unitten, så den kan vi tilføje på samme tid. Fordi Classes er nødvendig allerede i interface-delen af vores unit skal vi sætte uses klausulen lige under implementation nøgleordet, så det kommer til at se således ud:

// ...
interface

uses Classes, SysUtils;

type
// ...
Nu kan vores unit endelig kompilere, så kan vi begynde at fylde kode i alle metoderne. For at begynde et sted, kan vi tage constructoren Create som et naturligt sted at starte. Den skal tildele en værdi til FKommandolinie feltet, oprette et TStringList objekt til FParametre feltet, og parse kommandolinien ind i kommando og parametre. Vi starter med de to første ting:

constructor TKommandoParser.Create(const AKommandolinie: string);
begin
  FKommandolinie := Trim(Akommandolinie);
  FParametre := TStringList.Create;
end;
Parseren skal fungere på følgende måde:

  1. Løb gennem alle tegnene i kommandolinien, for hvert tegn:
    1. Hvis det ikke er et mellemrum, tilføj tegnet i slutningen af en midlertidig string
    2. Ellers tilføj den midlertidige string til listen over parametre, og tøm den midlertidige string
  2. Hvis den midlertidige string ikke er tom (dvs. vi har en del af en parameter, men der ikke er flere tegn i kommandolinien), tilføj den midlertidige string til parameterlisten.
  3. Tag første parameter og gem som kommando, slet første parameter fra listen
Det kræver to variabler: en string og en tæller-variabel (for at holde styr på hvor langt vi er kommet i kommandolinien.)

var
  tmp: string;
  i: integer;
Her giver jeg bare resten af koden til parseren, den kræver vist ikke yderligere forklaring. Koden skal ind lige før end i Create metoden.

  s := '';
  for i := 1 to Length(FKommandolinie) do
    if (FKommandolinie[i] = ' ') and (s <> '') then
    begin
      FParametre.Add(s);
      s := '';
    end
    else
      s := s + FKommandolinie[i];
  if s <> '' then
    FParametre.Add(s);
  FKommando := LowerCase(FParametre[0]);
  FParametre.Delete(0);
Nu til Destroy. Der skal vi bare rydde op efter os, i det her tilfælde er der kun vores FParametre felt som skal frigøres.

destructor TKommandoParser.Destroy;
begin
  FParametre.Free;
  inherited;
end;
Læg mærke til at Free metoden som findes i alle klasser, kalder Destroy metoden, men først efter at have tjekket af objektet ikke allerede er frigjort/slettet.

De sidste tre metoder er trivielle og forklarer vist sig selv:

function TKommandoParser.GetParameter(Index: integer): string;
begin
  Result := FParametre[Index];
end;

function TKommandoParser.GetParameterCount: integer;
begin
  Result := FParametre.Count;
end;

function TKommandoParser.GetRange(Fra, Til: integer): string;
var
  i: integer;
begin
  Result := FParametre[Fra];
  for i := Fra+1 to Til do
    Result := Result + ' ' + FParametre[i];
end;

Videre med kommandofortolkeren

Nu hvor vi har en kommando-parser klasse kan vi gå videre med selve fortolkeren. Meget af koden er den samme som i vores første fortolker, forskellen kommer der hvor vi skal finde ud af hvilken kommando der blev skrevet, og hvordan den skal udføres. Her kommer noget kode som skal stå i projektfilen, ligesom i den første kommandofortolker:

var
  kommandolinie: string;
  kommando: TKommandoParser;
  koerer: boolean;
begin
  koerer := True;
  Writeln('Velkommen til Kommandofortolker version 2.0');
  while koerer do
  begin
    Write('>');
    readln(kommandolinie);
    kommandolinie := Trim(kommandolinie);
    if kommandolinie <> '' then
    begin
      kommando := TKommandoParser.Create(kommandolinie);
      koerer := UdfoerKommando(kommando);
      kommando.Free;
    end;
  end;
end.
Det, der sker efter vi har tjekket at kommandolinien ikke er tom, er at vi opretter et nyt TKommandoParser objekt med den indtastede kommandolinie. Derefter kalder vi en funktion der hedder UdfoerKommando (den kommer vi til), og til sidst frigør/sletter vi kommandolinie-objektet.

Funktionen UdfoerKommando skal tage et TKommandoParser objekt som parameter, og returnere om vores program stadig skal køre. (Dvs. den skal returnere False hvis det var en quit kommando, og i alle andre tilfælde skal den returnere True.)

Lav endnu en ny unit og gem den som ”Kommandoer”. UdfoerKommando funktionen er centrum i denne unit, så vi starter med at erklære den:

function UdfoerKommando(AKommando: TKommandoParser): Boolean;
Fordi denne funktion bruger TKommandoParser klassen skal vi have Kommandoparser unitten i vores uses klausul. Vi skal også bruge SysUtils.

uses KommandoParser, SysUtils;
Delphi har ikke nogen funktion til automatisk at indsætte kodeskeletter for normale procedurer/funktioner i implementationen, ligesom det har for klasser, så vi bliver nødt til selv at lave kodeskelettet. Det ser sådan ud:

function UdfoerKommando(AKommando: TKommandoParser): Boolean;
begin

end;
Nu kan vi gøre næsten som i første version, bortset fra at koden bliver lettere at læse, og der er mindre redundans. Det første vi skal sørge for er dog, at funktionen altid har et meningsfyldt resultat, så det første vi gør i funktionen er at sætte Result til True (så programmet ikke afslutter). Derefter kan vi bare lave en række if-sætninger der tester kommandoen.

Det ser sådan ud:

function UdfoerKommando(AKommando: TKommandoParser): Boolean;
begin

end;
Nu kan vi gøre næsten som i første version, bortset fra at koden bliver lettere at læse, og der er mindre redundans. Det første vi skal sørge for er dog, at funktionen altid har et meningsfyldt resultat, så det første vi gør i funktionen er at sætte Result til True (så programmet ikke afslutter). Derefter kan vi bare lave en række if-sætninger der tester kommandoen.

function UdfoerKommando(AKommando: TKommandoParser): Boolean;
begin
  Result := True;
  if AKommando.Kommando = 'quit' then
    Result := False
  else if AKommando.Kommando = 'info' then
    Writeln('Kommandofortolker version 2.0')
  else if AKommando.Kommando = 'help' then
    Writeln('Kendte kommandoer: quit info help echo')
  else if AKommando.Kommando = 'echo' then
    Writeln(AKommando.GetRange(0, AKommando.ParameterCount-1))
  else
    Writeln('Ukendt kommando: ', AKommando.Kommando);        
end;
Man kan dog godt se at selv denne metode kan blive uoverskuelig når man får flere kommandoer, men der findes skal også andre metoder, som jeg vil komme ind på en anden gang.

Delphis sæt af funktioner til konsolprogrammer er desværre ret begrænset, så i det øjeblik snart du vil lidt mere end bare læse/skrive tegn til skærmen (f.eks. tømme skærmen eller skrive oven på noget gammel tekst) får du brug får et specielt library. Et sådant kan f.eks. være Win32Crt som du kan downloade her: http://dwp42.org/dwp/bazaar/snippets/. Indtil videre må du selv finde ud af hvordan det virker, men der kommer måske senere en artikel om det.




Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (0)

Du skal være logget ind for at skrive en kommentar.
t