rmwait.pas 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. {
  2. rmwait - remove (delete) file(s) with optional retries
  3. Copyright (C) 2009 by Tomas Hajny, member of the Free Pascal team
  4. This tool tries to mimic behaviour of GNU rm, but it provides
  5. the additional feature of retries and it also fixes some issues
  6. appearing at least with the Win32 port of version 3.13.
  7. See the file COPYING, included in this distribution,
  8. for details about the copyright.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  12. }
  13. program rmwait;
  14. {$D
  15. Remove (delete) file(s) with optional retries.
  16. }
  17. { $DEFINE DONOTHING}
  18. uses
  19. {$IFDEF GO32V2}
  20. Go32,
  21. {$ENDIF GO32V2}
  22. {$IFDEF OS2}
  23. DosCalls,
  24. {$ENDIF OS2}
  25. {$IFDEF WINDOWS}
  26. Windows,
  27. {$ENDIF WINDOWS}
  28. {$IFDEF UNIX}
  29. BaseUnix,
  30. {$ENDIF UNIX}
  31. Dos;
  32. const
  33. OptDirectories: boolean = false;
  34. OptForce: boolean = false;
  35. OptInteractive: boolean = false;
  36. OptRecursive: boolean = false;
  37. OptVerbose: boolean = false;
  38. OptRetries: longint = 1;
  39. OptWait: longint = 5;
  40. OptsStop: boolean = false;
  41. var
  42. OldExit: pointer;
  43. Deleted: cardinal;
  44. procedure VerbLine (S: string); inline;
  45. begin
  46. if OptVerbose then
  47. WriteLn (S);
  48. end;
  49. procedure ForceErrorLn (S: string); inline;
  50. begin
  51. WriteLn (ParamStr (0), ': ', S);
  52. end;
  53. procedure ErrorLn (S: string); inline;
  54. begin
  55. { if not (OptForce) then}
  56. ForceErrorLn (S);
  57. end;
  58. procedure GenericErrorLn (S: string; N: longint); inline;
  59. begin
  60. if not (OptForce) then
  61. WriteLn (ParamStr (0), ': ', S, ' (', N, ')');
  62. end;
  63. procedure ClearIO; inline;
  64. begin
  65. if IOResult <> 0 then ;
  66. end;
  67. procedure Wait (Seconds: Cardinal);
  68. {$IFDEF GO32v2}
  69. var
  70. R: Registers;
  71. T0, T1, T2: int64;
  72. DayOver: boolean;
  73. begin
  74. (* Sleep is supposed to give up time slice - DOS Idle Interrupt chosen
  75. because it should be supported in all DOS versions. *)
  76. R.AH := $2C;
  77. RealIntr($21, R);
  78. T0 := R.CH * 3600 + R.CL * 60 + R.DH;
  79. T2 := T0 + Seconds;
  80. DayOver := T2 > (24 * 3600);
  81. repeat
  82. Intr ($28, R);
  83. (* R.AH := $2C; - should be preserved. *)
  84. RealIntr($21, R);
  85. T1 := R.CH * 3600 + R.CL * 60 + R.DH;
  86. if DayOver and (T1 < T0) then
  87. Inc (T1, 24 * 3600);
  88. until T1 >= T2;
  89. end;
  90. {$ELSE GO32v2}
  91. {$IFDEF OS2}
  92. begin
  93. DosSleep (Seconds * 1000);
  94. end;
  95. {$ELSE OS2}
  96. {$IFDEF UNIX}
  97. begin
  98. fpSleep (Seconds * 1000);
  99. end;
  100. {$ELSE UNIX}
  101. {$IFDEF WINDOWS}
  102. begin
  103. Sleep (Seconds * 1000);
  104. end;
  105. {$ELSE WINDOWS}
  106. var
  107. T0, T1, T2: int64;
  108. begin
  109. {$WARNING No sleeping is performed with this platform!}
  110. T0 := GetMSCount;
  111. T2 := T0 + Seconds * 1000;
  112. repeat
  113. T1 := GetMSCount;
  114. (* GetMSCount returning lower value than in the first check indicates overflow
  115. and is treated as end of the waiting period due to undefined range. *)
  116. until (T1 >= T2) or (T1 < T0);
  117. end;
  118. {$ENDIF WINDOWS}
  119. {$ENDIF UNIX}
  120. {$ENDIF OS2}
  121. {$ENDIF GO32v2}
  122. procedure ClearAttribs (var F: file); inline;
  123. var
  124. W: word;
  125. begin
  126. {$I-}
  127. GetFAttr (F, W);
  128. if W and (ReadOnly or SysFile) <> 0 then
  129. SetFAttr (F, W and not ReadOnly and not SysFile);
  130. ClearIO;
  131. {$I+}
  132. end;
  133. function StrF (U: cardinal): string; inline;
  134. begin
  135. Str (U, StrF);
  136. end;
  137. function CheckOK (Msg: string; FN: PathStr): boolean;
  138. var
  139. Resp: string;
  140. begin
  141. Write (ParamStr (0), ': ', Msg, '''', FN, '''? ');
  142. ReadLn (Resp);
  143. CheckOK := (Length (Resp) > 0) and (UpCase (Resp [1]) = 'Y');
  144. end;
  145. procedure DelFile (FN: PathStr); inline;
  146. var
  147. F: file;
  148. R, Tries: longint;
  149. begin
  150. VerbLine ('removing ''' + FN + '''');
  151. Inc (Deleted);
  152. if not (OptInteractive) or CheckOK ('remove ', FN) then
  153. begin
  154. Assign (F, FN);
  155. if OptForce then
  156. ClearAttribs (F);
  157. Tries := 1;
  158. repeat
  159. {$I-}
  160. {$IFDEF DONOTHING}
  161. WriteLn ('Debug: ', FN);
  162. {$ELSE DONOTHING}
  163. Erase (F);
  164. {$ENDIF DONOTHING}
  165. R := IOResult;
  166. {$I+}
  167. Inc (Tries);
  168. if (R = 5) and (Tries <= OptRetries) then
  169. Wait (OptWait);
  170. until (R <> 5) or (Tries > OptRetries);
  171. case R of
  172. 0: ;
  173. 2: ErrorLn (FN + ': No such file or directory');
  174. 5: ErrorLn (FN + ': Permission denied');
  175. else
  176. GenericErrorLn (FN + ': Cannot be removed', R);
  177. end;
  178. end;
  179. end;
  180. procedure DelDir (FN: PathStr); inline;
  181. var
  182. F: file;
  183. R, Tries: longint;
  184. begin
  185. VerbLine ('removing ''' + FN + '''');
  186. Inc (Deleted);
  187. if not (OptInteractive) or CheckOK ('remove directory ', FN) then
  188. begin
  189. if OptForce then
  190. begin
  191. Assign (F, FN);
  192. ClearAttribs (F);
  193. end;
  194. Tries := 1;
  195. repeat
  196. {$I-}
  197. {$IFDEF DONOTHING}
  198. WriteLn ('Debug: Directory ', FN);
  199. {$ELSE DONOTHING}
  200. RmDir (FN);
  201. {$ENDIF DONOTHING}
  202. R := IOResult;
  203. {$I+}
  204. Inc (Tries);
  205. if (R = 5) and (Tries <= OptRetries) then
  206. begin
  207. VerbLine ('Removal attempt failed, waiting ' + StrF (OptWait) + ' seconds before trying again...');
  208. Wait (OptWait);
  209. end;
  210. until (R <> 5) or (Tries > OptRetries);
  211. case R of
  212. 0: ;
  213. 5: ErrorLn (FN + ': Permission denied');
  214. else
  215. GenericErrorLn (FN + ': Cannot be removed', R);
  216. end;
  217. end;
  218. end;
  219. procedure Syntax;
  220. begin
  221. WriteLn;
  222. WriteLn ('RMWait - remove (delete) file(s) with optional retries');
  223. WriteLn;
  224. WriteLn ('Syntax:');
  225. WriteLn (ParamStr (0) + ' [<options>...] [<file specifications>...]');
  226. WriteLn;
  227. WriteLn ('<file specifications> may contain wildcards ''*'' and ''?''.');
  228. WriteLn;
  229. WriteLn ('Options:');
  230. WriteLn (' -d, --directory remove directory. even if non-empty');
  231. WriteLn (' -f, --force ignore non-existent files, never prompt');
  232. WriteLn (' -i, --interactive prompt before any removal');
  233. WriteLn (' -r, -R, --recursive remove the contents of directories recursively');
  234. WriteLn (' -v, --verbose explain what is being done');
  235. WriteLn (' --version output version information and exit');
  236. WriteLn (' -h, -?, --help display this help and exit');
  237. WriteLn (' -t[<N>[,<T>]], --try[<N>[,<T>]] in case of errors, retry deleting N times');
  238. WriteLn (' (default 3 times) waiting T seconds between');
  239. WriteLn (' individual attempts (default 5 seconds)');
  240. WriteLn (' -- stop processing of options');
  241. WriteLn;
  242. WriteLn ('To remove a file whose name starts with a ''-'', for example ''-file'',');
  243. WriteLn ('use one of these commands:');
  244. WriteLn (' rm -- -file');
  245. WriteLn (' rm ./-file');
  246. WriteLn;
  247. Halt;
  248. end;
  249. procedure ParError (S: string); inline;
  250. begin
  251. ForceErrorLn (S);
  252. WriteLn;
  253. Syntax;
  254. end;
  255. procedure ProcessFSpec (FN: PathStr);
  256. var
  257. SR: SearchRec;
  258. D, BaseDir: DirStr;
  259. N, BaseName: NameStr;
  260. E: ExtStr;
  261. RemFNDir: boolean;
  262. begin
  263. RemFNDir := false;
  264. {$IF NOT DEFINED (OS2) and NOT DEFINED (WINDOWS) and NOT DEFINED (DPMI) and NOT DEFINED (UNIX) and NOT DEFINED (MACOS) and NOT DEFINED (AMIGA) and NOT DEFINED (NETWARE)}
  265. {$WARNING Proper behaviour for this target platform has not been checked!}
  266. {$ENDIF}
  267. {$IF NOT DEFINED (MACOS) and NOT DEFINED (AMIGA)}
  268. (* Special case - root directory needs to be treated in a special way. *)
  269. {$IFDEF UNIX}
  270. if (Length (FN) = 1)
  271. {$ELSE UNIX}
  272. {$IF DEFINED (OS2) or DEFINED (WINDOWS) or DEFINED (DPMI)}
  273. if (((Length (FN) = 3) and (FN [2] = DriveSeparator))
  274. or ((Length (FN) = 2) and (FN [1] = DirectorySeparator)))
  275. (* Root of UNC path - nonsense, but changing it to root of current drive would be dangerous. *)
  276. {$ELSE}
  277. {$IFDEF NETWARE}
  278. if (Length (FN) = Pos (DirectorySeparator, FN))
  279. {$ENDIF NETWARE}
  280. {$ENDIF}
  281. and (FN [Length (FN)] = DirectorySeparator) then
  282. {$ENDIF UNIX}
  283. if OptRecursive then
  284. begin
  285. BaseDir := FN;
  286. BaseName := AllFilesMask;
  287. end
  288. else
  289. begin
  290. ErrorLn (FN + ': is a directory');
  291. Exit;
  292. end
  293. else
  294. {$ENDIF}
  295. begin
  296. (* Check if the specification directly corresponds to a directory *)
  297. if FN [Length (FN)] = DirectorySeparator then
  298. Delete (FN, Length (FN), 1);
  299. FSplit (FN, D, N, E);
  300. FindFirst (FN, (AnyFile or Directory) and not VolumeID, SR);
  301. if (DosError = 0) and (SR.Attr and Directory = Directory) and
  302. ((SR.Name = N + E) or
  303. (* Checking equal names is not sufficient with case preserving file systems. *)
  304. (Pos ('?', FN) = 0) and (Pos ('*', FN) = 0)) then
  305. if OptRecursive then
  306. begin
  307. BaseDir := FN;
  308. if BaseDir [Length (BaseDir)] <> DirectorySeparator then
  309. BaseDir := BaseDir + DirectorySeparator;
  310. BaseName := AllFilesMask;
  311. RemFNDir := true;
  312. end
  313. else
  314. if OptDirectories then
  315. RemFNDir := true
  316. else
  317. begin
  318. ErrorLn (FN + ': is a directory');
  319. Exit;
  320. end
  321. else
  322. begin
  323. BaseDir := D;
  324. BaseName := N + E;
  325. end;
  326. FindClose (SR);
  327. end;
  328. FindFirst (BaseDir + BaseName, AnyFile and not Directory and not VolumeID, SR);
  329. while DosError = 0 do
  330. begin
  331. DelFile (BaseDir + SR.Name);
  332. FindNext (SR);
  333. end;
  334. FindClose (SR);
  335. if OptRecursive then
  336. begin
  337. FindFirst (BaseDir + BaseName, (AnyFile or Directory) and not VolumeID, SR);
  338. while DosError = 0 do
  339. begin
  340. if (SR.Attr and Directory > 0) and
  341. ((Length (SR.Name) <> 1) or (SR.Name [1] <> '.')) and
  342. ((Length (SR.Name) <> 2) or (SR.Name [1] <> '.') or (SR.Name [2] <> '.')) and
  343. (not (OptInteractive) or CheckOK ('descend directory ', BaseDir + SR.Name)) then
  344. ProcessFSpec (BaseDir + SR.Name);
  345. FindNext (SR);
  346. end;
  347. FindClose (SR);
  348. end;
  349. if RemFNDir then
  350. DelDir (FN);
  351. end;
  352. procedure NewExit; far;
  353. begin
  354. ExitProc := OldExit;
  355. if (ErrorAddr <> nil) or (ExitCode <> 0) then
  356. begin
  357. ErrorAddr := nil;
  358. case ExitCode of
  359. 202: WriteLn ('Directory tree too deep!!');
  360. 4: WriteLn ('Increase the FILES directive in CONFIG.SYS!!');
  361. 5, 101, 150..152, 154, 156..158, 160..162: WriteLn ('I/O error (',
  362. ExitCode, ')!!');
  363. else
  364. WriteLn ('Internal error (', ExitCode, ')!!');
  365. end;
  366. WriteLn;
  367. end;
  368. end;
  369. procedure AllowSlash (var S: string); inline;
  370. var
  371. I: byte;
  372. begin
  373. if DirectorySeparator <> '/' then
  374. for I := 1 to Length (S) do
  375. begin
  376. if S [I] = '/' then
  377. S [I] := DirectorySeparator;
  378. end;
  379. end;
  380. procedure ProcessOpts (S: string);
  381. var
  382. I: longint;
  383. procedure ParseOptTries; inline;
  384. var
  385. SN: string;
  386. J, N, Err: longint;
  387. begin
  388. J := Succ (I);
  389. while (J <= Length (S)) and (S [J] in ['0'..'9']) do
  390. Inc (J);
  391. if J = Succ (I) then
  392. OptRetries := 3
  393. else
  394. begin
  395. SN := Copy (S, Succ (I), J - I - 1);
  396. Val (SN, N, Err);
  397. if Err <> 0 then
  398. ParError ('invalid value for retry attempts ''' + SN + '''');
  399. OptRetries := N;
  400. I := Pred (J);
  401. if (J < Length (S)) and (S [J] = ',') then
  402. begin
  403. Inc (J);
  404. Inc (I);
  405. while (J <= Length (S)) and (S [J] in ['0'..'9']) do
  406. Inc (J);
  407. if J > Succ (I) then
  408. begin
  409. SN := Copy (S, Succ (I), J - I - 1);
  410. Val (SN, N, Err);
  411. if Err <> 0 then
  412. ParError ('invalid value for retry wait time ''' + SN + '''');
  413. OptWait := N;
  414. I := Pred (J);
  415. end;
  416. end;
  417. end;
  418. end;
  419. begin
  420. if S [2] = '-' then
  421. if Length (S) = 2 then
  422. OptsStop := true
  423. else
  424. begin
  425. Delete (S, 1, 2);
  426. for I := 1 to Length (S) do
  427. S [I] := Upcase (S [I]);
  428. if S = 'HELP' then Syntax;
  429. if S = 'DIRECTORY' then
  430. OptDirectories := true
  431. else if S = 'FORCE' then
  432. OptForce := true
  433. else if S = 'INTERACTIVE' then
  434. OptInteractive := true
  435. else if S = 'RECURSIVE' then
  436. OptRecursive := true
  437. else if S = 'VERBOSE' then
  438. OptVerbose := true
  439. else if S = 'VERSION' then
  440. begin
  441. WriteLn ('rmwait - version 20091101');
  442. Halt;
  443. end
  444. else if Copy (S, 1, 3) = 'TRY' then
  445. begin
  446. I := 3;
  447. ParseOptTries;
  448. if I <> Length (S) then
  449. ParError ('unrecognized option ''' + S + '''');
  450. end
  451. else
  452. ParError ('unrecognized option ''' + S + '''');
  453. end
  454. else
  455. begin
  456. I := 2;
  457. repeat
  458. case Upcase (S [I]) of
  459. 'H', '?': Syntax;
  460. 'D': OptDirectories := true;
  461. 'F': OptForce := true;
  462. 'I': OptInteractive := true;
  463. 'R': OptRecursive := true;
  464. 'V': OptVerbose := true;
  465. 'T': ParseOptTries;
  466. else
  467. ParError ('invalid option -- ' + S [I])
  468. end;
  469. Inc (I);
  470. until (I > Length (S));
  471. end;
  472. end;
  473. var
  474. J, K: longint;
  475. Par: string;
  476. begin
  477. {$IFDEF OS2}
  478. DosCalls.DosError (0);
  479. {$ENDIF}
  480. OldExit := ExitProc;
  481. ExitProc := @NewExit;
  482. J := ParamCount;
  483. if J = 0 then
  484. Syntax
  485. else
  486. begin
  487. K := 1;
  488. Par := ParamStr (K);
  489. while (K <= J) and (Par [1] = '-') and (Length (Par) > 1) and not OptsStop do
  490. begin
  491. ProcessOpts (Par);
  492. Inc (K);
  493. Par := ParamStr (K);
  494. end;
  495. if K > J then
  496. Syntax
  497. else
  498. repeat
  499. AllowSlash (Par);
  500. Deleted := 0;
  501. ProcessFSpec (FExpand (Par));
  502. if Deleted = 0 then
  503. ErrorLn (ParamStr (K) + ': No such file or directory');
  504. Inc (K);
  505. Par := ParamStr (K);
  506. until K > J;
  507. end;
  508. end.