rmwait.pas 14 KB

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