Program Chge;

{ Copyright 1990 Trevor J Carlsen Version 1.06  24-07-90                    }
{ This Program may be used and distributed as if it was in the Public Domain}
{ With the following exceptions:                                            }
{    1.  If you alter it in any way, the copyright notice must not be       }
{        changed.                                                           }
{    2.  If you use code excerpts in your own Programs, due credit must be  }
{        given, along With a copyright notice -                             }
{        "Parts Copyright 1990 Trevor J Carlsen"                            }
{    3.  No Charge may be made For any Program using code from this Program.}

{ Changes (or deletes) a String in any File. If an .EXE or .COM File then  }
{ the change must be of a similar length inorder to retain the executable  }
{ integrity.                                                               }

{ If you find this Program useful here is the author's contact address -   }

{      Trevor J Carlsen                                                    }
{      PO Box 568                                                          }
{      Port Hedland Western Australia 6721                                 }
{      Voice 61 [0]91 72 2026                                              }
{      Data  61 [0]91 72 2569                                              }

Uses
  BmSrch,
  Dos;

Const
  space       = #32;
  quote       = #34;
  comma       = #44;
  copyright1  = 'CHGE - version 1.06 Copyright 1989,1990 Trevor Carlsen';
  copyright2  = 'All rights reserved.';

Var
  dirinfo     : SearchRec; { Dos }
  f           : File;
  FDir        : DirStr;    { Dos }
  mask,
  fname,
  oldstr,
  newstr      : String;
  oldlen      : Byte Absolute oldstr;
  newlen      : Byte Absolute newstr;
  changes     : Word;
  time        : LongInt Absolute $0000:$046C;
  start       : LongInt;

Function ElapsedTime(start : LongInt): Real;
  begin
    ElapsedTime := (time - start) / 18.2;
  end; { ElapsedTime }

Procedure ReportError(e : Byte);
begin
  Writeln('CHGE [path]Filename searchstr replacementstr|NUL');
  Writeln(' eg:  CHGE c:\autoexec.bat "color" "colour"');
  Writeln('      CHGE c:\autoexec.bat 12 13,10,13,10,13,10,13,10');
  Writeln('      CHGE c:\wp\test.txt "Trevor" NUL');
  Writeln;
  Writeln('The first example will change every occurrence of the Word "color" to "colour"');
  Writeln('The second will replace every formfeed Character (ascii 12) With 4 sets of');
  Writeln('carriage return/linefeed combinations and the third will delete every');
  Writeln('occurrence of "Trevor"');
  Writeln('The prime requirements are:');
  Writeln('  There MUST always be exactly three space delimiters on the command line -');
  Writeln('  one between the Program name and the Filename, one between the Filename and');
  Writeln('  the search String and another between the search String and the replacement');
  Writeln('  String. Any other spaces may ONLY occur between quote Characters.');
  Writeln('  The Program will not permit you to change the length of an .EXE or .COM File,');
  Writeln('  therefore the replacement String MUST be the same length as the String');
  Writeln('  that it is replacing in these cases.');
  Writeln;
  Writeln('  If using ascii codes, each ascii Character must be separated from another');
  Writeln('  by a comma. The same rule applies to spaces as above - three required - no');
  Writeln('  more - no less. If just deleting the NUL must not be in quotes.');
  halt(e);
end; { ReportError }

Function StUpCase(Str : String) : String;
Var
  Count : Integer;
begin
  For Count := 1 to Length(Str) do
    Str[Count] := UpCase(Str[Count]);
  StUpCase := Str;
end;

Procedure ParseCommandLine;
Var
  parstr,                                      { contains the command line }
  temp      : String;
  len       : Byte Absolute parstr;           { the length Byte For parstr }
  tlen      : Byte Absolute temp;               { the length Byte For temp }
  CommaPos,
  QuotePos,
  SpacePos,
  chval     : Byte;
  error     : Integer;
  DName     : NameStr;
  DExt      : ExtStr;

  Function right(Var s; n : Byte): String;{ Returns the n right portion of s }
  Var
    st : String Absolute s;
    len: Byte Absolute s;
  begin
    if n >= len then
      right := st
    else
      right := copy(st,succ(len)-n,n);
  end; { right }

begin
  parstr        := String(ptr(PrefixSeg,$80)^);     { Get the command line }
  if parstr[1]   = space then
    delete(parstr,1,1);               { First Character is usually a space }
  SpacePos      := pos(space,parstr);
  if SpacePos    = 0 then                                      { No spaces }
    ReportError(1);
  mask          := StUpCase(copy(parstr,1,pred(SpacePos)));
  FSplit(mask,Fdir,DName,DExt);       { To enable the directory to be kept }
  delete(parstr,1,SpacePos);
  QuotePos      := pos(quote,parstr);
  if QuotePos   <> 0 then begin          { quotes - so must be quoted Text }
    if parstr[1] <> quote then               { so first Char must be quote }
      ReportError(2);
    delete(parstr,1,1);                       { get rid of the first quote }
    QuotePos    := pos(quote,parstr);            { and find the next quote }

    if QuotePos  = 0 then                    { no more - so it is an error }
      ReportError(3);
    oldstr    := copy(parstr,1,pred(QuotePos));{ search String now defined }
    if parstr[QuotePos+1] <> space then            { must be space between }
      ReportError(1);
    delete(parstr,1,succ(QuotePos));             { the quotes - else error }
    if parstr[1] <> quote then begin                     { may be a delete }
      tlen      := 3;
      move(parstr[1],temp[1],3);
      if temp <> 'NUL' then                              { is not a delete }
        ReportError(4)                  { must be quote after space or NUL }
      else
        newlen  := 0;               { is a delete - so nul the replacement }
    end
    else begin
      delete(parstr,1,1);                           { get rid of the quote }
      QuotePos   := pos(quote,parstr); { find next quote For end of String }
      if QuotePos = 0 then                            { None? - then error }
        ReportError(5);
      newstr := copy(parstr,1,pred(QuotePos));{ Replacement String defined }
    end;
  end
  else begin                                   { must be using ascii codes }
    oldlen       := 0;
    SpacePos     := pos(space,parstr);     { Find end of search Characters }
    if SpacePos   = 0 then                           { No space - so error }
      ReportError(6);
    temp         := copy(parstr,1,SpacePos-1);
    delete(parstr,1,SpacePos);          { get rid of the search Characters }
    CommaPos     := pos(comma,temp);                    { find first comma }
    if CommaPos   = 0 then             { No comma - so only one ascii code }
      CommaPos   := succ(tlen);
    Repeat                                      { create the search String }
      val(copy(temp,1,CommaPos-1),chval,error); { convert to a numeral and }
      if error <> 0 then                   { if there is an error bomb out }
        ReportError(7);
      inc(oldlen);
      oldstr[oldlen] := Char(chval);{ add latest Char to the search String }
      delete(temp,1,CommaPos);
      CommaPos   := pos(comma,temp);
      if CommaPos = 0 then
        CommaPos := succ(tlen);
    Until tlen = 0;
    newlen       := 0;
    CommaPos     := pos(comma,parstr);
    if CommaPos   = 0 then
      CommaPos   := succ(len);
    Repeat                                 { create the replacement String }
      val(copy(parstr,1,pred(CommaPos)),chval,error);
      if error <> 0 then                              { must be ascii code }
        ReportError(8);
      inc(newlen);
      newstr[newlen] := Char(chval);
      delete(parstr,1,CommaPos);
      CommaPos   := pos(comma,parstr);
      if CommaPos = 0 then CommaPos := len+1;
    Until len = 0;
  end; { else }
  if ((right(mask,3) = 'COM') or (right(mask,3) = 'EXE')) and
    (newlen <> oldlen) then
    ReportError(16);
end; { ParseCommandLine }

Function OpenFile(fn : String): Boolean;
  begin
    assign(f,fn);
    {$I-} reset(f,1); {$I+}
    OpenFile := IOResult = 0;
  end; { OpenFile }

Procedure CloseFile;
  begin
    {$I-}
    truncate(f);
    Close(f);
    if IOResult <> 0 then;                          { dummy call to IOResult }
    {$I+}
  end; { CloseFile }

Procedure ChangeFile(Var chge : Word);
  Const
    bufflen     = 65000;                    { This is the limit For BMSearch }
    searchlen   = bufflen - 1000;      { Allow space For extra Characters in }
  Type                                              { the replacement String }
    buffer      = Array[0..pred(bufflen)] of Byte;
    buffptr     = ^buffer;
  Var
    table       : BTable;                         { Boyer-Moore search table }
    old,                                             { Pointer to old buffer }
    nu          : buffptr;                           { Pointer to new buffer }
    count,
    result,
    oldpos,
    newpos      : Word;
    oldfpos,
    newfpos     : LongInt;
    finished    : Boolean;

  Procedure AllocateMemory(Var p; size : Word);
    Var
      buff : Pointer Absolute p;
    begin
      if MaxAvail >= size then
        GetMem(buff,size)
      else begin
        Writeln('Insufficient memory available.');
        halt(10);
      end;
    end; { AllocateMemory }

  begin
    oldfpos := 0; newfpos := 0;
    chge := 0;
    AllocateMemory(old,searchlen);
    AllocateMemory(nu,bufflen);      { make room on the heap For the buffers }
    BMMakeTable(oldstr,table);           { Create a Boyer-Moore search table }
    {$I-}
    BlockRead(f,old^,searchlen,result);                    { Fill old buffer }
    oldfpos := FilePos(f);
    {$I+}
    if IOResult <> 0 then begin
      CloseFile; ReportError(11);
    end;
    Repeat
      oldpos := 0; newpos := 0; count := 0;
      finished := (result < searchlen); { if buffer<>full then no more reads }
      Repeat                              { Do a BM search For search String }
        count := BMSearch(old^[oldpos],result-oldpos,table,oldstr);
        if count = $FFFF then begin   { search String not found so copy rest }
          move(old^[oldpos],nu^[newpos],result-oldpos);   { of buffer to new }
          inc(newpos,result-oldpos);  { buffer and update the buffer markers }
          inc(oldpos,result-oldpos);
        end
        else begin                                     { search String found }
          if count <> 0 then begin       { not at position one in the buffer }
            move(old^[oldpos],nu^[newpos],count);{ transfer everything prior }
            inc(oldpos,count);          { to the search String to new buffer }
            inc(newpos,count);               { and update the buffer markers }
          end;
          move(newstr[1],nu^[newpos],newlen);  { copy the replacement String }
          inc(oldpos,oldlen);        { to the new buffer and update the buffer }
          inc(newpos,newlen);                                      { markers }
          inc(chge);
        end;
      Until oldpos >= result;               { keep going Until end of buffer }
      if not finished then begin       { Fill 'er up again For another round }
        {$I-}
        seek(f,oldfpos);
        BlockRead(f,old^,searchlen,result);
        oldfpos := FilePos(f);
        {$I+}
        if IOResult <> 0 then begin
          CloseFile; ReportError(13);
        end; { if IOResult }
      end; { if not finished }
      {$I-}
      seek(f,newfpos);
      BlockWrite(f,nu^,newpos);                   { Write new buffer to File }
      newfpos := FilePos(f);
      {$I+}
      if IOResult <> 0 then begin
        CloseFile; ReportError(12);
      end;
    Until finished;
    FreeMem(old, searchlen); FreeMem(nu,bufflen);
  end;  { ChangeFiles }

Procedure Find_and_change_all_Files;
  Var
    Filefound : Boolean;

  Function padstr(ch : Char; len : Byte): String;
  
    Var
      temp : String;
    
    begin
      FillChar(temp[1],len,ch);
      temp[0] := chr(len);
      padstr  := temp;
    end; { padstr }

  begin
    Filefound := False;
    FindFirst(mask,AnyFile,dirinfo);
    While DosError = 0 do begin
      Filefound := True;
      start := time;
      fname := FDir + dirinfo.name;
      if OpenFile(fname) then begin
        Write(fname,PadStr(space,30-length(fname)),FileSize(f):7,'  ');
        ChangeFile(changes);
        CloseFile;
        if changes = 0 then
          Writeln
        else
          Writeln('Made ',changes,' changes in ',ElapsedTime(start):4:2,' seconds.')
      end
      else
        Writeln('Unable to process ',fname);
      FindNext(dirinfo);
    end; { While DosError = 0 }
    if not Filefound then
      Writeln('No Files found.');
  end; { Find_and_change_all_Files }

begin { main }
  Writeln(copyright1);
  Writeln(copyright2);
  ParseCommandLine;
  Find_and_change_all_Files;
end.

