home *** CD-ROM | disk | FTP | other *** search
/ The World of Computer Software / World_Of_Computer_Software-02-385-Vol-1of3.iso / o / opuniq.zip / DUPFIND.PAS next >
Pascal/Delphi Source File  |  1991-03-08  |  16KB  |  561 lines

  1. {************************************************************************
  2.  Find all files with duplicate names on specified drive(s) and write a list
  3.  of them. Run DUPFIND without command line parameters to get help.
  4.  
  5.  Requires the commercial product Object Professional to compile.
  6.  
  7.  DUPFIND provides the following general-purpose objects:
  8.    Tree - a binary tree (in OPTREE.PAS)
  9.    WildMatcher - a string matcher that understands * and ?
  10.  
  11.  Written 1/13/90, Kim Kokkonen, TurboPower Software
  12.  Updated 3/08/91 to allow specification of a dup trigger point (/n)
  13.  CompuServe ID [76004,2611]
  14. ************************************************************************}
  15.  
  16. {$R-,S-,I-,V-,B-,F-}
  17.  
  18. program DupFind;
  19.   {-Find duplicate files}
  20.  
  21. uses
  22.   Dos, OpString, OpDos, OpRoot, OpTree;
  23.  
  24. const
  25.   FileAttr = AnyFile;         {File attributes being checked}
  26.   MinDups : Word = 2;         {Minimum number of dups to report file}
  27.  
  28. type
  29.   FileStr = String[12];
  30.  
  31.   DirNodePtr = ^DirNode;
  32.   DirNode =
  33.     object(SingleListNode)
  34.       Time : Longint;         {Date/time stamp of file in this directory}
  35.       Size : Longint;         {Size of file in this directory}
  36.       DNameP : StringPtr;     {Pointer to name of directory}
  37.       constructor Init(FileTime, FileSize : Longint; Dirname : PathStr);
  38.         {-Initialize a DirNode}
  39.       destructor Done; virtual;
  40.         {-Destroy a DirNode}
  41.     end;
  42.  
  43.   FileNodePtr = ^FileNode;
  44.   FileNode =
  45.     object(TreeNode)
  46.       NameP : StringPtr;      {Pointer to name of file}
  47.       DirList : SingleList;   {List of DirNodes}
  48.       constructor Init(FileName : FileStr);
  49.         {-Initialize a FileNode}
  50.       destructor Done; virtual;
  51.         {-Destroy a FileNode}
  52.     end;
  53.  
  54.   FileTreePtr = ^FileTree;
  55.   FileTree =
  56.     object(Tree)
  57.       ftUnique : LongInt;     {Number of uniquely named files in tree}
  58.       ftTotal : LongInt;      {Total number of files in tree}
  59.       constructor Init;
  60.         {-Initialize FileTree}
  61.       procedure FileInsert(FileName : FileStr;
  62.                            FileTime, FileSize : LongInt;
  63.                            Dirname : PathStr);
  64.         {-Add a file and directory name to the tree list}
  65.       procedure DumpDups;
  66.         {-Dump files that appear at least twice}
  67.       procedure GetCounts(var Unique, Total : LongInt);
  68.         {-Return the number of unique names and the total number of names}
  69.       {-- override virtual methods required by Tree object --}
  70.       function Compare(Key1, Key2 : Pointer) : CompareType; virtual;
  71.         {-Compare two keys, returning Less, Equal, Greater}
  72.       function GetKey(N : TreeNodePtr) : Pointer; virtual;
  73.         {-Return a pointer to the key value for node N}
  74.     end;
  75.  
  76. const
  77.   AnyChar = '*';         {Match zero or more characters}
  78.   OneChar = '?';         {Match exactly one character}
  79.   EndChar = #255;        {Terminator to match strings}
  80.  
  81. type
  82.   WildMatcherPtr = ^WildMatcher;
  83.   WildMatcher =
  84.     object(Root)
  85.       maCase : Boolean;       {True if case-sensitive matching}
  86.       maMask : String[128];   {Mask used for matching}
  87.       constructor Init(Mask : String; CaseSensitive : Boolean);
  88.         {-Initialize the mask string. May fail if Mask is invalid}
  89.       function Matches(Name : String) : Boolean;
  90.         {-Return True if Name matches Mask}
  91.       function GetMask : String;
  92.         {-Return the simplified mask}
  93.       procedure SimplifyMask;
  94.         {-Used internally to simplify mask when object instantiated}
  95.     end;
  96.  
  97. var
  98.   StdErr : Text;              {File where messages are written}
  99.   FileNames : FileTree;       {Stores all the files}
  100.   FileMask : WildMatcher;     {Used for wildcard matching}
  101.   DefaultMask : Boolean;      {True if mask is *.* and FileMask isn't used}
  102.  
  103. procedure WriteCopyright;
  104. begin
  105.   WriteLn(StdErr,
  106.   'DUPFIND 1.1 - Duplicate file finder. Copyright (c) TurboPower Software, 1990'^M^J);
  107. end;
  108.  
  109. procedure WriteHelp;
  110.   {-Write a help message and halt}
  111. begin
  112.   WriteLn('Usage: DUPFIND Drive [Drive ...] [/S FileMask] [/n] [>OutputRedirection]');
  113.   WriteLn;
  114.   WriteLn('DUPFIND creates an alphabetized list of all duplicate files on the');
  115.   WriteLn('specified disk drives. One or more valid drive letters must be given.');
  116.   WriteLn('By default, DUPFIND scans all files on the drives. Use the /S option');
  117.   WriteLn('to limit the search. FileMask is an exact filename or an extended DOS');
  118.   WriteLn('wildcard pattern like *.PAS or OP??????.* or even ARC*X.*. By default,');
  119.   WriteLn('DUPFIND reports files that are duplicated two or more times. Specify a');
  120.   WriteLn('different trigger point with /n, e.g., /1 reports all files.');
  121.   WriteLn;
  122.   WriteLn('DUPFIND writes a report to standard output. The report looks like the');
  123.   WriteLn('following:');
  124.   WriteLn('  README.COM');
  125.   WriteLn('        4217 89/06/28 05:50:00 c:\t55\rtl');
  126.   WriteLn('        4200 88/08/29 05:00:00 c:\t5');
  127.   WriteLn('which shows the size, date, time, drive, and directory of the dup files.');
  128.   WriteLn('The command line');
  129.   WriteLn('  DUPFIND C D E /S READ*.* >JUNK');
  130.   WriteLn('searches drives C, D, and E for all files starting with READ.');
  131.   Halt;
  132. end;
  133.  
  134. procedure Abort(Msg : String);
  135.   {-Report error message and abort}
  136. begin
  137.   WriteLn(StdErr, Msg);
  138.   Halt(1);
  139. end;
  140.  
  141. procedure InsufficientMemory;
  142.   {-Report insufficient memory and abort}
  143. begin
  144.   Abort('Insufficient memory');
  145. end;
  146.  
  147. constructor DirNode.Init(FileTime, FileSize : Longint; Dirname : PathStr);
  148.   {-Initialize the node}
  149. begin
  150.   if not SingleListNode.Init then
  151.     Fail;
  152.   DNameP := nil;
  153.   if not GetMemCheck(DNameP, Length(Dirname)+1) then begin
  154.     Done;
  155.     Fail;
  156.   end;
  157.   DNameP^ := Dirname;
  158.   Time := FileTime;
  159.   Size := FileSize;
  160. end;
  161.  
  162. destructor DirNode.Done;
  163.   {-Destroy the node}
  164. begin
  165.   if DNameP <> nil then
  166.     FreeMem(DNameP, Length(DNameP^)+1);
  167.   SingleListNode.Done;
  168. end;
  169.  
  170. constructor FileNode.Init(FileName : FileStr);
  171.   {-Initialize a FileNode}
  172. begin
  173.   if not TreeNode.Init then
  174.     Fail;
  175.   NameP := nil;
  176.   if not(DirList.Init and GetMemCheck(NameP, Length(FileName)+1)) then begin
  177.     Done;
  178.     Fail;
  179.   end;
  180.   NameP^ := FileName;
  181. end;
  182.  
  183. destructor FileNode.Done;
  184.   {-Destroy a FileNode}
  185. begin
  186.   if NameP <> nil then
  187.     FreeMem(NameP, Length(NameP^)+1);
  188.   DirList.Done;
  189. end;
  190.  
  191. constructor FileTree.Init;
  192.   {-Initialize FileTree}
  193. begin
  194.   ftUnique := 0;
  195.   ftTotal := 0;
  196.   if not Tree.Init then
  197.     Fail;
  198. end;
  199.  
  200. procedure FileTree.FileInsert(FileName : FileStr;
  201.                               FileTime, FileSize : LongInt;
  202.                               Dirname : PathStr);
  203.   {-Add a file and directory name to the tree list}
  204. var
  205.   DirIndex : Word;
  206.   FileNP : FileNodePtr;
  207.   DNodeP : DirNodePtr;
  208. begin
  209.   {See if filename is already in tree}
  210.   FileNP := FileNodePtr(Find(@FileName));
  211.   if FileNP = nil then begin
  212.     {Insert filename in tree}
  213.     New(FileNP, Init(FileName));
  214.     if FileNP = nil then
  215.       InsufficientMemory;
  216.     Insert(FileNP);
  217.     Inc(ftUnique);
  218.   end;
  219.  
  220.   {Create a directory node to add to the dictionary}
  221.   New(DNodeP, Init(FileTime, FileSize, Dirname));
  222.   if DNodeP = nil then
  223.     InsufficientMemory;
  224.  
  225.   {Add directory node to list}
  226.   FileNP^.DirList.Append(DNodeP);
  227.  
  228.   inc(ftTotal);
  229. end;
  230.  
  231. function FileTree.Compare(Key1, Key2 : Pointer) : CompareType;
  232.   {-Compare two keys, returning Less, Equal, Greater}
  233. begin
  234.   Compare := CompString(StringPtr(Key1)^, StringPtr(Key2)^);
  235. end;
  236.  
  237. function FileTree.GetKey(N : TreeNodePtr) : Pointer;
  238.   {-Return a pointer to the key value for node N}
  239. begin
  240.   GetKey := FileNodePtr(N)^.NameP;
  241. end;
  242.  
  243. function DateTimeStr(DT : LongInt) : String;
  244.   {-Return a formatted date-time string}
  245. type
  246.   String2 = String[2];
  247. var
  248.   T : DateTime;
  249.  
  250.   function W2S2(W : Word) : String2;
  251.   var
  252.     S : String2;
  253.   begin
  254.     Str((W mod 100):2, S);
  255.     if S[1] = ' ' then
  256.       S[1] := '0';
  257.     W2S2 := S;
  258.   end;
  259.  
  260. begin
  261.   UnpackTime(DT, T);
  262.   with T do
  263.     DateTimeStr := W2S2(Year)+'/'+W2S2(Month)+'/'+W2S2(Day)+' '+
  264.                    W2S2(Hour)+':'+W2S2(Min)+':'+W2S2(Sec);
  265. end;
  266.  
  267. {$F+}
  268. procedure DumpNode(N : TreeNodePtr; T : TreePtr);
  269.   {-Dump one tree node}
  270. var
  271.   DNodeP : DirNodePtr;
  272. begin
  273.   if FileNodePtr(N)^.DirList.Size >= MinDups then begin
  274.     {At least two instances of file, write the filename}
  275.     WriteLn(FileNodePtr(N)^.NameP^);
  276.     {Scan the list of directories}
  277.     DNodeP := DirNodePtr(FileNodePtr(N)^.DirList.Head);
  278.     while DNodeP <> nil do begin
  279.       WriteLn('  ', DNodeP^.Size:8, ' ', DateTimeStr(DNodeP^.Time), ' ',
  280.               StLocase(DNodeP^.DNameP^));
  281.       DNodeP := DirNodePtr(FileNodePtr(N)^.DirList.Next(DNodeP));
  282.     end;
  283.   end;
  284. end;
  285. {$F-}
  286.  
  287. procedure FileTree.DumpDups;
  288.   {-Dump files that appear at least Min times}
  289. begin
  290.   VisitNodesUp(DumpNode);
  291. end;
  292.  
  293. procedure FileTree.GetCounts(var Unique, Total : LongInt);
  294.   {-Return the number of unique names and the total number of names}
  295. begin
  296.   Unique := ftUnique;
  297.   Total := ftTotal;
  298. end;
  299.  
  300. constructor WildMatcher.Init(Mask : String; CaseSensitive : Boolean);
  301.   {-Initialize the mask string. May fail}
  302. begin
  303.   if not Root.Init then
  304.     Fail;
  305.   if Length(Mask) > 127 then
  306.     Fail;
  307.   if Pos(EndChar, Mask) <> 0 then
  308.     Fail;
  309.   maCase := CaseSensitive;
  310.   maMask := Mask;
  311.   SimplifyMask;
  312.   maMask[Length(maMask)+1] := EndChar;
  313. end;
  314.  
  315. function WildMatcher.Matches(Name : String) : Boolean;
  316.   {-Return True if Name matches Mask}
  317. var
  318.   NLen : Byte absolute Name;
  319.   MPos : Word;
  320.   NPos : Word;
  321.   MPSave : Word;
  322.   NPSave : Word;
  323.   AnyOn : Boolean;
  324.   Ch : Char;
  325. begin
  326.   Matches := False;
  327.  
  328.   {Add terminator to input string}
  329.   Name[NLen+1] := EndChar;
  330.  
  331.   AnyOn := False;
  332.   MPos := 1;
  333.   NPos := 1;
  334.  
  335.   while (maMask[MPos] <> EndChar) or (Name[NPos] <> EndChar) do begin
  336.     {Look for '*'}
  337.     if maMask[MPos] = AnyChar then begin
  338.       if MPos >= Length(maMask) then begin
  339.         {Last character in maMask is '*', rest must match}
  340.         Matches := True;
  341.         Exit;
  342.       end;
  343.       AnyOn := True;
  344.       NPSave := NPos;
  345.       inc(MPos);
  346.       MPSave := MPos;
  347.     end;
  348.  
  349.     {Get next character from Name string}
  350.     if maCase then
  351.       Ch := Name[NPos]
  352.     else
  353.       Ch := UpCase(Name[NPos]);
  354.  
  355.     {Look for literal match}
  356.     if (Ch <> EndChar) and ((maMask[MPos] = OneChar) or (maMask[MPos] = Ch))
  357.     then begin
  358.       {Matching character}
  359.       inc(MPos);
  360.       inc(NPos);
  361.     end else begin
  362.       {Mismatched character}
  363.       if not AnyOn or (NPSave >= Length(Name)) then
  364.         {Fatal mismatch, no '*' in effect or no way to advance past mismatch}
  365.         Exit;
  366.       {Increment restart point}
  367.       inc(NPSave);
  368.       {Try again at next Name position}
  369.       NPos := NPSave;
  370.       {Restart maMask just after the '*'}
  371.       MPos := MPSave;
  372.     end;
  373.   end;
  374.  
  375.   Matches := True;
  376. end;
  377.  
  378. function WildMatcher.GetMask : String;
  379.   {-Return the simplified mask}
  380. begin
  381.   GetMask := maMask;
  382. end;
  383.  
  384. procedure WildMatcher.SimplifyMask;
  385.   {-Used internally to simplify mask when object instantiated}
  386. var
  387.   MLen : Byte;
  388.   MPos : Word;
  389.   OMask : String;
  390.   OLen : Byte absolute OMask;
  391. begin
  392.   MLen := Length(maMask);
  393.   MPos := 1;
  394.   OLen := 0;
  395.   while MPos <= MLen do begin
  396.     if (MPos = 1) or (maMask[MPos] <> '*') or (maMask[MPos-1] <> '*') then begin
  397.       {Transfer maMask to OMask, skipping repeated asterisks}
  398.       inc(OLen);
  399.       OMask[OLen] := maMask[MPos];
  400.       if not maCase then
  401.         OMask[OLen] := UpCase(OMask[OLen]);
  402.     end;
  403.     inc(MPos);
  404.   end;
  405.   maMask := OMask;
  406. end;
  407.  
  408. procedure ScanDir(Dir : PathStr);
  409.   {-Scan one directory}
  410. var
  411.   FRec : SearchRec;
  412.  
  413.   procedure WriteStatus;
  414.   begin
  415.     Write(StdErr, Dir);
  416.   end;
  417.  
  418.   procedure ClearStatus;
  419.   begin
  420.     Write(StdErr, ^M, CharStr(' ', Length(Dir)), ^M);
  421.   end;
  422.  
  423. begin
  424.   WriteStatus;
  425.   FindFirst(AddBackSlash(Dir)+'*.*', FileAttr, FRec);
  426.   while DosError = 0 do begin
  427.     if (FRec.Attr and VolumeID) <> 0 then
  428.       {do nothing for volume labels}
  429.     else if (FRec.Attr and Directory <> 0) then begin
  430.       {a directory, look deeper}
  431.       if (FRec.Name <> '.') and (FRec.Name <> '..') then begin
  432.         ClearStatus;
  433.         ScanDir(AddBackSlash(Dir)+FRec.Name);
  434.         WriteStatus;
  435.       end;
  436.     end else if DefaultMask or FileMask.Matches(FRec.Name) then
  437.       {a matching file, add it to FileTree}
  438.       FileNames.FileInsert(FRec.Name, FRec.Time, FRec.Size, Dir);
  439.     FindNext(FRec);
  440.   end;
  441.   ClearStatus;
  442. end;
  443.  
  444. procedure ScanDrive(DriveLet : Char);
  445.   {-Scan one drive for duplicate files. DriveLet assumed to be valid}
  446. begin
  447.   ScanDir(DriveLet+':\');
  448. end;
  449.  
  450. function IsOption(var Param : Word) : Boolean;
  451.   {-Return True if ParamStr(Param) is an option, and evaluate it if so}
  452. var
  453.   Arg : String[127];
  454. begin
  455.   IsOption := False;
  456.   Arg := ParamStr(Param);
  457.   case Arg[1] of
  458.     '/', '-' :
  459.       if Length(Arg) <> 2 then
  460.         Abort('Invalid option: '+Arg)
  461.       else
  462.         case UpCase(Arg[2]) of
  463.           'S' :
  464.              if Param = ParamCount then
  465.                Abort('Missing parameter after: '+Arg)
  466.              else begin
  467.                inc(Param);
  468.                Arg := ParamStr(Param);
  469.                {Validate mask}
  470.                if (Length(Arg) > 12) or
  471.                   (JustFileName(Arg) <> Arg) or
  472.                   (Pos('.', Arg) > 9) then
  473.                  Abort('Invalid file mask: '+Arg);
  474.                FileMask.Init(StUpcase(Arg), True);
  475.                DefaultMask := (FileMask.GetMask = '*.*');
  476.                IsOption := True;
  477.              end;
  478.           '0'..'9' :
  479.              begin
  480.                MinDups := Byte(Arg[2])-Byte('0');
  481.                IsOption := True;
  482.              end;
  483.         else
  484.           Abort('Invalid option: '+Arg);
  485.         end;
  486.   end;
  487. end;
  488.  
  489. function IsValidDrive(DriveName : String) : Boolean;
  490.   {-Return true if DriveName specifies a valid drive}
  491. begin
  492.   IsValidDrive := False;
  493.   case Length(DriveName) of
  494.     1 : {OK so far};
  495.     2 : {Assure second character is a colon}
  496.      if DriveName[2] <> ':' then
  497.        Exit;
  498.   else
  499.     Exit;
  500.   end;
  501.   IsValidDrive := ValidDrive(Upcase(DriveName[1]));
  502. end;
  503.  
  504. procedure ValidateDrives;
  505.   {-Assure the requested drives are valid}
  506. var
  507.   Param : Word;
  508. begin
  509.   Param := 1;
  510.   while Param <= ParamCount do begin
  511.     if not IsOption(Param) then
  512.       if not IsValidDrive(ParamStr(Param)) then
  513.         Abort('Invalid drive: '+ParamStr(Param));
  514.     inc(Param);
  515.   end;
  516. end;
  517.  
  518. procedure ScanDrives;
  519.   {-Scan the requested drives for duplicate files}
  520. var
  521.   Param : Word;
  522.   DriveLet : String[1];
  523. begin
  524.   Param := 1;
  525.   while Param <= ParamCount do begin
  526.     if not IsOption(Param) then begin
  527.       DriveLet := Copy(ParamStr(Param), 1, 1); {Minimize stack usage}
  528.       ScanDrive(UpCase(DriveLet[1]));
  529.     end;
  530.     inc(Param);
  531.   end;
  532. end;
  533.  
  534. begin
  535.   {StdErr will be used for messages and status}
  536.   if not OpenStdDev(StdErr, StdErrHandle) then
  537.     Halt;
  538.  
  539.   {Write copyright and help message}
  540.   WriteCopyRight;
  541.   if ParamCount = 0 then
  542.     WriteHelp;
  543.  
  544.   {The FileNames tree object will store the filenames and their locations}
  545.   FileNames.Init;
  546.  
  547.   {Scan all files by default}
  548.   DefaultMask := True;
  549.  
  550.   {Validate the requested drives before scanning anything}
  551.   ValidateDrives;
  552.  
  553.   {Scan the requested drives}
  554.   ScanDrives;
  555.  
  556.   {Dump the output}
  557.   FileNames.DumpDups;
  558.  
  559.   Close(StdErr);
  560. end.
  561.