rmwait.pas 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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. {$WARNING TODO: Add information about passing parameters via RMWAIT_OPTS environment variable once implemented}
  243. WriteLn ('To remove a file whose name starts with a ''-'', for example ''-file'',');
  244. WriteLn ('use one of these commands:');
  245. WriteLn (' rm -- -file');
  246. WriteLn (' rm ./-file');
  247. WriteLn;
  248. Halt;
  249. end;
  250. procedure ParError (S: string); inline;
  251. begin
  252. ForceErrorLn (S);
  253. WriteLn;
  254. Syntax;
  255. end;
  256. procedure ProcessFSpec (FN: PathStr);
  257. var
  258. SR: SearchRec;
  259. D, BaseDir: DirStr;
  260. N, BaseName: NameStr;
  261. E: ExtStr;
  262. RemFNDir: boolean;
  263. begin
  264. RemFNDir := false;
  265. {$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)}
  266. {$WARNING Proper behaviour for this target platform has not been checked!}
  267. {$ENDIF}
  268. {$IF NOT DEFINED (MACOS) and NOT DEFINED (AMIGA)}
  269. (* Special case - root directory needs to be treated in a special way. *)
  270. {$IFDEF UNIX}
  271. if (Length (FN) = 1)
  272. {$ELSE UNIX}
  273. {$IF DEFINED (OS2) or DEFINED (WINDOWS) or DEFINED (DPMI)}
  274. if (((Length (FN) = 3) and (FN [2] = DriveSeparator))
  275. or ((Length (FN) = 2) and (FN [1] = DirectorySeparator)))
  276. (* Root of UNC path - nonsense, but changing it to root of current drive would be dangerous. *)
  277. {$ELSE}
  278. {$IFDEF NETWARE}
  279. if (Length (FN) = Pos (DirectorySeparator, FN))
  280. {$ENDIF NETWARE}
  281. {$ENDIF}
  282. and (FN [Length (FN)] = DirectorySeparator) then
  283. {$ENDIF UNIX}
  284. if OptRecursive then
  285. begin
  286. BaseDir := FN;
  287. BaseName := AllFilesMask;
  288. end
  289. else
  290. begin
  291. ErrorLn (FN + ': is a directory');
  292. Exit;
  293. end
  294. else
  295. {$ENDIF}
  296. begin
  297. (* Check if the specification directly corresponds to a directory *)
  298. if FN [Length (FN)] = DirectorySeparator then
  299. Delete (FN, Length (FN), 1);
  300. FSplit (FN, D, N, E);
  301. FindFirst (FN, (AnyFile or Directory) and not VolumeID, SR);
  302. if (DosError = 0) and (SR.Attr and Directory = Directory) and
  303. ((SR.Name = N + E) or
  304. (* Checking equal names is not sufficient with case preserving file systems. *)
  305. (Pos ('?', FN) = 0) and (Pos ('*', FN) = 0)) then
  306. if OptRecursive then
  307. begin
  308. BaseDir := FN;
  309. if BaseDir [Length (BaseDir)] <> DirectorySeparator then
  310. BaseDir := BaseDir + DirectorySeparator;
  311. BaseName := AllFilesMask;
  312. RemFNDir := true;
  313. end
  314. else
  315. if OptDirectories then
  316. RemFNDir := true
  317. else
  318. begin
  319. ErrorLn (FN + ': is a directory');
  320. Exit;
  321. end
  322. else
  323. begin
  324. BaseDir := D;
  325. BaseName := N + E;
  326. end;
  327. FindClose (SR);
  328. end;
  329. FindFirst (BaseDir + BaseName, AnyFile and not Directory and not VolumeID, SR);
  330. while DosError = 0 do
  331. begin
  332. DelFile (BaseDir + SR.Name);
  333. FindNext (SR);
  334. end;
  335. FindClose (SR);
  336. if OptRecursive then
  337. begin
  338. FindFirst (BaseDir + BaseName, (AnyFile or Directory) and not VolumeID, SR);
  339. while DosError = 0 do
  340. begin
  341. if (SR.Attr and Directory > 0) and
  342. ((Length (SR.Name) <> 1) or (SR.Name [1] <> '.')) and
  343. ((Length (SR.Name) <> 2) or (SR.Name [1] <> '.') or (SR.Name [2] <> '.')) and
  344. (not (OptInteractive) or CheckOK ('descend directory ', BaseDir + SR.Name)) then
  345. ProcessFSpec (BaseDir + SR.Name);
  346. FindNext (SR);
  347. end;
  348. FindClose (SR);
  349. end;
  350. if RemFNDir then
  351. DelDir (FN);
  352. end;
  353. procedure NewExit; far;
  354. begin
  355. ExitProc := OldExit;
  356. if (ErrorAddr <> nil) or (ExitCode <> 0) then
  357. begin
  358. ErrorAddr := nil;
  359. case ExitCode of
  360. 202: WriteLn ('Directory tree too deep!!');
  361. 4: WriteLn ('Increase the FILES directive in CONFIG.SYS!!');
  362. 5, 101, 150..152, 154, 156..158, 160..162: WriteLn ('I/O error (',
  363. ExitCode, ')!!');
  364. else
  365. WriteLn ('Internal error (', ExitCode, ')!!');
  366. end;
  367. WriteLn;
  368. end;
  369. end;
  370. procedure AllowSlash (var S: string); inline;
  371. var
  372. I: byte;
  373. begin
  374. if DirectorySeparator <> '/' then
  375. for I := 1 to Length (S) do
  376. begin
  377. if S [I] = '/' then
  378. S [I] := DirectorySeparator;
  379. end;
  380. end;
  381. procedure ProcessOpts (S: string);
  382. var
  383. I: longint;
  384. procedure ParseOptTries; inline;
  385. var
  386. SN: string;
  387. J, N, Err: longint;
  388. begin
  389. J := Succ (I);
  390. while (J <= Length (S)) and (S [J] in ['0'..'9']) do
  391. Inc (J);
  392. if J = Succ (I) then
  393. OptRetries := 3
  394. else
  395. begin
  396. SN := Copy (S, Succ (I), J - I - 1);
  397. Val (SN, N, Err);
  398. if Err <> 0 then
  399. ParError ('invalid value for retry attempts ''' + SN + '''');
  400. OptRetries := N;
  401. I := Pred (J);
  402. if (J < Length (S)) and (S [J] = ',') then
  403. begin
  404. Inc (J);
  405. Inc (I);
  406. while (J <= Length (S)) and (S [J] in ['0'..'9']) do
  407. Inc (J);
  408. if J > Succ (I) then
  409. begin
  410. SN := Copy (S, Succ (I), J - I - 1);
  411. Val (SN, N, Err);
  412. if Err <> 0 then
  413. ParError ('invalid value for retry wait time ''' + SN + '''');
  414. OptWait := N;
  415. I := Pred (J);
  416. end;
  417. end;
  418. end;
  419. end;
  420. begin
  421. if S [2] = '-' then
  422. if Length (S) = 2 then
  423. OptsStop := true
  424. else
  425. begin
  426. Delete (S, 1, 2);
  427. for I := 1 to Length (S) do
  428. S [I] := Upcase (S [I]);
  429. if S = 'HELP' then Syntax;
  430. if S = 'DIRECTORY' then
  431. OptDirectories := true
  432. else if S = 'FORCE' then
  433. OptForce := true
  434. else if S = 'INTERACTIVE' then
  435. OptInteractive := true
  436. else if S = 'RECURSIVE' then
  437. OptRecursive := true
  438. else if S = 'VERBOSE' then
  439. OptVerbose := true
  440. else if S = 'VERSION' then
  441. begin
  442. WriteLn ('rmwait - version 20091101');
  443. Halt;
  444. end
  445. else if Copy (S, 1, 3) = 'TRY' then
  446. begin
  447. I := 3;
  448. ParseOptTries;
  449. if I <> Length (S) then
  450. ParError ('unrecognized option ''' + S + '''');
  451. end
  452. else
  453. ParError ('unrecognized option ''' + S + '''');
  454. end
  455. else
  456. begin
  457. I := 2;
  458. repeat
  459. case Upcase (S [I]) of
  460. 'H', '?': Syntax;
  461. 'D': OptDirectories := true;
  462. 'F': OptForce := true;
  463. 'I': OptInteractive := true;
  464. 'R': OptRecursive := true;
  465. 'V': OptVerbose := true;
  466. 'T': ParseOptTries;
  467. else
  468. ParError ('invalid option -- ' + S [I])
  469. end;
  470. Inc (I);
  471. until (I > Length (S));
  472. end;
  473. end;
  474. var
  475. J, K: longint;
  476. Par: string;
  477. begin
  478. {$IFDEF OS2}
  479. DosCalls.DosError (0);
  480. {$ENDIF}
  481. OldExit := ExitProc;
  482. ExitProc := @NewExit;
  483. J := ParamCount;
  484. if J = 0 then
  485. Syntax
  486. else
  487. begin
  488. {$WARNING TODO: Add support for passing parameters via RMWAIT_OPTS environment variable}
  489. K := 1;
  490. Par := ParamStr (K);
  491. while (K <= J) and (Par [1] = '-') and (Length (Par) > 1) and not OptsStop do
  492. begin
  493. ProcessOpts (Par);
  494. Inc (K);
  495. Par := ParamStr (K);
  496. end;
  497. if K > J then
  498. Syntax
  499. else
  500. repeat
  501. AllowSlash (Par);
  502. Deleted := 0;
  503. ProcessFSpec (FExpand (Par));
  504. if Deleted = 0 then
  505. ErrorLn (ParamStr (K) + ': No such file or directory');
  506. Inc (K);
  507. Par := ParamStr (K);
  508. until K > J;
  509. end;
  510. end.