{ ZNDEL version 2.1 - Public Domain / Freeware Exclusive-Delete utility ( originally 'ZIP-NOT-DEL' ? ) E. de Neve CompuServe ID: 100121,1070 Version 2.1 November 1, 1994 New in version 2.1 - fixed bug in redirection detection - confirmation prompt will now bypass redirection Version 2.0 August 17, 1994 New in version 2.0 : - recognizes 12 of the most common archive format extensions - full DIR-style wildcard support - confirmation asked before deleting - no confirmation needed in assigned working directories - realistic limits & safety checks for maximum number of files - switch to override prompting, useful in batch files Version 1.0 (Original) Written Sept. 21, 1991 by G. Palmer } {$A+,B-,D-,E-,F-,G-,I-,L-,N-,O-,P-,Q-,R-,S-,T-,V-,X+,Y-} {* compiler switches *} Program Zndel2; Uses Dos, Crt; Type FullNameStr = String [12]; Const Assume_Yes: Boolean = False; Maxdelete = 2000; Maxsave = 32; Maxworkdirs = 32; MetaBufSize = 4000; { I/O buffer used when patching .exe file } ConfigStart: String [5] = '(CFG<'; { mark start of config area } Nr_workdirs: Byte = 0; Workdirs: Array [1..MaxWorkDirs] Of FullNameStr = ('', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''); ConfigEnd: String [5] = '>CFG)'; { end of config area } MaxArchExt = 12; ArchExt: Array [1..MaxArchExt] Of String [3] = ( 'ZIP', 'ARJ', 'LZH', 'ARC', 'LIM', 'UC2', 'PAK', 'SQZ', 'HAP', 'SDN', 'ZOO', 'SIT' ); VaLetSet: Set Of Char = [ '#'..')', '!', '@', '^', '~', '_', '{', '}', '-', '0'..'9', 'A'..'Z', 'a'..'z']; { set of valid letters that make up an unambiguous file/dir name } Var CH: Char; I: Word; Afile: File; NormOut: Text; Nr_Names_To_Save: Word; Nr_Files_To_Delete: Word; Nr_Files_Found: Word; Nr_Files_to_Protect: Word; TempStr, UpStr: FullNameStr; Files_To_Delete : Array [1..maxdelete] Of FullNameStr; Names_To_Save: Array [1..maxsave] Of FullNameStr; Search_Record: SearchRec; MetaBuffer: Array [0..MetaBufSize] Of Byte; Procedure Show_Info; Begin WriteLn; WriteLn ('Deletes all files in the current directory, except:'); WriteLn (' Files listed on the command line, DIR-style wildcards allowed.'); WriteLn (' Archived files ( ZIP, LZH, ARC, ARJ, LIM, ZOO etc. )'); WriteLn (' Hidden, System and ReadOnly files.'); WriteLn; WriteLn ('Usage: ZNDEL [/Y] [filespec (filespec) ] delete all but filespecs & archives'); WriteLn (' ÀÄÄ> assume YES on all prompting'); WriteLn (' ZNDEL /S show current settings'); WriteLn (' ZNDEL /? show this help text'); WriteLn; WriteLn (' ZNDEL /W [workdir (workdir) ] assign working directories '); WriteLn; WriteLn (' ZNDEL will always ask for confirmation before deleting files,'); WriteLn (' unless the current directory is one of the assigned working dirs.'); Halt; End; Procedure WildExpand (Var inname: String); Var workname: String [12]; {name} Havecard: Boolean; S, D, P: Byte; {counters source,destin,point} ic: Char; Procedure PartExpand; Begin If HaveCard Then IC := '?' Else If (S > Byte (inname [0] ) ) Or (P > 0) Then IC := ' ' Else Begin IC := UpCase (inname [S] ); If IC = '*' Then Begin Havecard := True; IC := '?'; End Else If IC = '.' Then Begin P := S; IC := ' '; End; Inc (S); End; {real ic digest} Workname [D] := IC; Inc (D); End; Begin S := 1; { source } D := 1; { destin } P := 0; { point-pos } workname [0] := #12; workname [9] := '.'; HaveCard := (Inname [0] > #0) And (inname [1] = '.'); While (Byte (inname [0] ) > S) And (Inname [S] = ' ') Do Inc (S); { 'remove' front spaces... } Repeat {copy into name8} PartExpand; Until D = 9; S := 1; {FIND any point if it exists..} While (P = 0) And (S <= Byte (Inname [0] ) ) Do Begin If inname [S] = '.' Then P := S Else Inc (S); End; Havecard := ( (P = 0) And (Inname [0] > #0) ) Or (Inname [0] = #1); S := P; {on point } P := 0; Inc (S); {both get over point} Inc (D); PartExpand; {ext 3 chars} PartExpand; PartExpand; Inname := WorkName; End; Function MatchWild (Var WW1, SS2: String): Boolean; {count on BOTH being expanded..} Var CC: Byte; Begin {loop both strings, if wild has non-? char that doesnt match SS2 char, OR SS2 char has ? that doesn't match SPACE, then exit} matchwild := False; For CC := 1 To 12 Do If WW1 [CC] <> SS2 [CC] Then Begin If ( (ww1 [cc] = ' ') And (ss2 [cc] <> '?') ) Or ( (WW1 [CC] <> '?') ) Then Exit; End; Matchwild := True; End; Function SameName (Wild, Sample: String): Boolean; Begin { Note: WILD must be an already expanded 13-character wildcard string} Wildexpand (Sample); Samename := matchwild (Wild, Sample); End; Procedure Show_Config; Begin Write ('Assigned working directories: '); If Nr_Workdirs = 0 Then WriteLn ('None.'); For I := 1 To Nr_Workdirs Do Write (Workdirs [I], ' '); WriteLn; Halt; End; Function ValidDirName (Var Workstring: String): Boolean; Var I: Byte; NumPoints: Byte; PointStart: Byte; ExtSize: Byte; NameSize: Byte; Begin PointStart := 0; For I := 1 To Length (WorkString) Do Begin If (Workstring [i] = '.') And (Pointstart = 0) Then Begin {point digest} If I > 1 Then PointStart := I Else Begin ValidDirName := False; Exit; End; {too many points, or starts with point..} End {no point - then must be valid filename letter} Else If Not (Workstring [i] In VaLetSet) Then Begin ValidDirName := False; Exit; End; End; {finally, check if the extension OR filename are not too big: } If ( (Pointstart = 0) And (Length (Workstring) > 8) ) Or ( Pointstart > 9) Or ( ( Pointstart > 1) And (Length (WorkString) > (Pointstart + 3) ) ) Then ValidDirname := False Else ValidDirName := True; End; Procedure UpcaseString (Var Workstring: String); Var I: Byte; Begin For I := 1 To Length (WorkString) Do WorkString [i] := UpCase (WorkString [i] ); End; Function FindLocation (Var Infile: File; Sample: String): LongInt; { universal 'binary file' search routine, works with files } { of any length, even if much larger than 64Kb } { searches a file for sample string using the 'Metabuffer' } { assumes the file INFILE was already open for reading } Var I: LongInt; J: Word; Location: LongInt; BytesRead: Word; SearchIndex: LongInt; Begin SearchIndex := 0; FindLocation := 0; If Length (Sample) = 0 Then Exit; Repeat Seek (InFile, Searchindex); BlockRead (InFile, Metabuffer, SizeOf (Metabuffer), BytesRead); If BytesRead < Length (Sample) Then Exit; {file or buffer too small..} For I := 0 To (BytesRead - Length (Sample) ) Do If MetaBuffer [i] = Byte (Sample [1] ) Then Begin J := 1; While (J < Length (Sample) ) And ( Metabuffer [I + J] = Byte (Sample [J + 1] ) ) Do Inc (J); If J = Length (Sample) Then Begin FindLocation := SearchIndex + I; Exit; End; End; If BytesRead < SizeOf (Metabuffer) Then Exit; { at end of file} SearchIndex := SearchIndex + BytesRead - Length (Sample) + 1; { This ensures overlap between consecutive buffer reads; because of this overlap, the whole procedure will still work even in the extreme case when Sizeof(Metabuffer)=Length(Sample) !!! } Until False; End; Procedure Config_Workdirs; Var BytesRead, BytesWritten: Word; {dummy args for Blockread/write} PatchAddr1, PatchAddr2: Word; I, J: Word; NewDirs: Word; ParamString: String; Begin { put supplied working dir names into array } NewDirs := ParamCount; { First parameter was /W } Nr_Workdirs := 0; { disregard old settings } For i := 2 To NewDirs Do { expand & add to SAVE specs list } If (Nr_workdirs < MaxWorkDirs) Then { check for max nr of dirs } Begin ParamString := ParamStr (i); UpcaseString (ParamString); If ParamString [1] = '/' Then Show_Info; { wrong place for option } If ValidDirName (ParamString) Then Begin Inc (Nr_Workdirs); If Paramstring [Byte (Paramstring [0] ) ] = '.' {get rid of ugly points at end} Then Dec (Byte (paramstring [0] ) ); WorkDirs [Nr_Workdirs] := ParamString; End; End; { Find 'home' directory, find ZNDEL.EXE (or whatever our name was) find out where to insert the new workdirs data structure, then copy them to it. } Assign (Afile, ParamStr (0) ); { it's ME ! } FileMode := 2; { default, read and write possible } Reset (Afile, 1); { open, counting will be done in BYTES } If IOResult <> 0 Then Begin WriteLn ('Configuration failed - file not found.'); WriteLn; Halt; End; PatchAddr1 := FindLocation (Afile, Configstart); PatchAddr2 := FindLocation (Afile, ConfigEnd); If IOResult <> 0 Then Begin WriteLn ('Configuration failed - error reading file.'); WriteLn; Halt; End; If (PatchAddr1 = 0) Or (PatchAddr2 = 0) Or ( (PatchAddr2 - PatchAddr1) <> (Ofs (ConfigEnd) - Ofs (ConfigStart) ) ) Then Begin WriteLn ('Error - incompatible structure in: ', ParamStr (0) ); WriteLn; Halt; End; { Now seek to config area in file and copy our own data to it.. } { The area to patch starts just after 'configstart' at Nr_Workdirs} Seek (Afile, PatchAddr1 + Length (ConfigStart) ); BlockWrite (Afile, Nr_Workdirs, ( SizeOf (Nr_Workdirs) + SizeOf (Workdirs) ), BytesWritten); Close (Afile); If IOResult <> 0 Then WriteLn (' Error trying to update options.') Else Begin WriteLn ('New settings written to ', ParamStr (0) ); Show_Config; End; Halt; End; Procedure Get_Command_Line_Args; Var I: Word; ParamString : String; Nr_Params, DigestParam: Byte; Begin Nr_Params := ParamCount; If Nr_Params = 0 Then Exit; DigestParam := 1; ParamString := ParamStr (1); UpcaseString (ParamString); If ParamString = '/W' Then Config_Workdirs; If ParamString = '/S' Then Show_Config; If ParamString = '/Y' Then Begin Assume_Yes := True; Inc (DigestParam); End; If ParamString = '/?' Then Show_Info; { no valid options so interpret the rest as filespecs of files to be saved } Nr_Names_to_save := 0; For i := DigestParam To Nr_Params Do { expand & add to SAVE specs list } If Nr_Names_to_Save < MaxSave Then { check for max nr of names } Begin ParamString := ParamStr (i); If ParamString [1] = '/' Then Show_Info; { wrong place for option } WildExpand (ParamString); Inc (Nr_Names_to_Save); Names_to_Save [Nr_Names_to_save] := ParamString; End; End; Procedure Check_If_Protected (Curr_file: String); Var I: Integer; Begin Inc (Nr_Files_Found); Inc (Nr_Files_to_Protect); { start and assume it's protected } If ( (Search_Record.Attr And ReadOnly) = ReadOnly) Then Exit; { Protected } For I := 1 To MaxArchExt Do { does it have a known extension? } If Pos ('.' + ArchExt [I] , Curr_file) > 1 Then Exit; For I := 1 To Nr_Names_to_Save Do { was it on cmd line? } If SameName (Names_to_Save [i], Curr_File) Then Exit; Dec (Nr_Files_to_Protect); { not protected after all } Inc (Nr_Files_To_Delete); Files_To_Delete [ Nr_Files_to_Delete ] := Curr_File; { add to delete list } End; Function InWorkDir: Boolean; Var ThisDir: String; T: Word; Begin InWorkDir := True; { Test if we are in a working dir, or any one of its subdirs.... } GetDir (0, thisdir); Thisdir := Thisdir + '\'; For T := 1 To Nr_WorkDirs Do Begin If Pos ('\' + WorkDirs [T] + '\', Thisdir) > 0 Then Exit; End; InWorkDir := False; End; Procedure SayPrott; { ask for confirmation } Begin WriteLn (NormOut,'! WARNING - this is not a known working directory.'); Write (NormOut,'Are you sure (Y/N)? '); While KeyPressed Do CH := ReadKey; CH := ReadKey; WriteLn (NormOut,CH); If UpCase (CH) <> 'Y' Then Halt; End; Procedure Bye; Begin WriteLn; WriteLn (NormOut,'ZNDEL 2.1 aborted. Some files not deleted.'); Halt; End; Function Redirected: Boolean; { detect if user wants redirectable output } Assembler; Asm MOV AX, 04400h { query device info } MOV BX, 1 { for device STDOUT } INT 021h XOR AX, AX TEST DL, 1 shl 7 { bit 7 clear: redirected to file } JZ @redirred TEST DL, 1 shl 1 { bit 1 set: device is standard output } JNZ @standard @redirred: INC AX { true if redirected } @standard: End; Begin AssignCrt(NormOut); { save default mode of screen output to CRT } Rewrite(NormOut); { open for writing } If Redirected Then Begin { In Borland/Turbo Pascal, using CRT bypasses DOS so the } { output is not redirectable. Here we reroute the output } { to the official DOS STDOUT device again, but only when } { the user wanted to redirect the output. } Assign (Output, ''); { Put pascal output back on real STDOUT.. } Rewrite (Output); { Open for writing } End; While KeyPressed Do CH := ReadKey; WriteLn ('ZNDEL 2.1 Exclusive Delete utility by G. Palmer and E. de Neve Freeware'); Get_Command_Line_Args; If (Not InWorkDir) Then If (Not Assume_Yes) Then SayPrott; Nr_Files_Found := 0; Nr_Files_to_Protect := 0; Nr_Files_to_Delete := 0; { Reading directory .. } FindFirst ('*.*', Archive, Search_Record); If (DosError = 0) Then Check_if_protected (Search_Record.Name); While (DosError = 0) And (Nr_Files_to_Delete < Maxdelete) Do Begin FindNext (Search_Record); If (DosError = 0) Then Check_if_protected (Search_Record.Name); If KeyPressed Then bye; { chance to cancel } End; { Deleting files .. } If (Nr_Files_to_Delete > 0 ) Then Begin If KeyPressed Then bye; { chance to cancel } For I := 1 To Nr_Files_To_Delete Do Begin If KeyPressed Then bye; { chance to cancel } Assign (Afile, Files_To_Delete [I] ); Erase (Afile); End; End; WriteLn; WriteLn (' Files found: ', Nr_Files_found); WriteLn ('Protected files: ', Nr_Files_to_Protect); WriteLn (' Files deleted: ', Nr_Files_to_Delete); End. { -------------------------------------------------------------------------} ZNDEL version 2.1 - Public Domain / Freeware Exclusive-Delete utility ( originally 'Zip-Not-DEL' ? ) This program deletes all the files in the current directory except archives, files specified on the command line, and files marked as system, hidden, or read-only. Very convenient for cleaning up after de-archiving, e.g in working- and download directories. Usage: ZNDEL [/Y] [filespec (filespec) ] delete all but filespecs & archives ÀÄÄ> assume YES on all prompting (useful in batch files) ZNDEL /S show current workdir assignments ZNDEL /W [workdir (workdir) ] assign working directories ZNDEL /? show the help text Examples: delete all but the assembler sources ZNDEL *.asm to keep prog1.pas, prog2.txt etc. ZNDEL prog? combined effect of above examples ZNDEL prog? *.asm the same, without prompting ZNDEL /Y prog? *.asm Configuration: You can configure ZNDEL to work automatically without prompting for quick cleanups in specific directories. Make sure to specify only simple directory names, do not include drive ID's or subdirectories, for example: ZNDEL /W download stuff temp This makes ZNDEL recognize these directories or any of their subdirectories as special working directories, in which ZNDEL will never ask for confirmation. The commands C:\DOWNLOAD\GAME> ZNDEL and C:\COMMPROG\STUFF\MISC> ZNDEL will both work without confirmation because GAME is a subdirectory of DOWNLOAD, and MISC is a subdir of STUFF. Tech notes & details: The included source file ZNDEL.PAS was tested and compiled using Borland Pascal 7.0. For configuration, the .EXE file itself is modified, which will not work when it is compressed by an executable compressor like LZEXE or PKLITE. Configuration will work OK when ZNDEL.EXE has been renamed. Because wildcards in ZNDEL are used to specify files to save rather than files to delete, the DIR wildcard convention, which is much more flexible than the DEL wildcard (= internal MS-DOS) convention, is simulated with all its details and quirks. For example, in DIR style, "." and "*" both mean "*.*", and "progname" means "progname.*". All output can be suppressed or redirected, e.g. by redirecting to the NUL device, as in ZNDEL /Y > NUL Pascal programmers may find some of the code useful for their own programs, especially the redirection routines, the self-modification trick including a "binary file search" routine (which works on files of unlimited size) and the wildcard evaluation. Use the code any way you like. Legal stuff: There is no warranty of this software's suitability for any purpose, nor any acceptance of liability, express or implied. By using this free software, you agree to this. Version history: Version 2.1 November 1, 1994 New in version 2.1 - fixed bug in redirection detection - confirmation prompt will now bypass redirection Version 2.0 August 17, 1994 New in version 2.0 : - recognizes 12 of the most common archive format extensions - full DIR-style wildcard support - confirmation asked before deleting - no confirmation needed in assigned working directories - realistic limits & safety checks for maximum number of files - switch to override prompting, useful in batch files Version 1.0 (Original) Written Sept. 21, 1991 by G. Palmer Original: Written Sept 21, 1991 by G.Palmer