Archive: How to RMDir empty directories recursively?


How to RMDir empty directories recursively?
  I tried already everything I can think of but none method works as expected.

I am searching (RecFind) the whole start menu folders for links to my application on uninstall. If a link is found it is being deleted - so far, so good.
Now I want to delete its folder/directory too if it is empty and so on (recursion).

But no matter what I try I only get the first parent deleted. The upper directory on the next loop won't be deleted because it is detected as non-empty (e.g. by ${DirState}) though it is empty.

I read about problems using Locate or Recfind because they might not close their last handle correctly. So I use ${WordFind} to get the parent directory but the directory is not removed nonetheless.

Any ideas or hints?

Gunther


your problem might be that you're checking from the trunk down to the leaves.. when it needs to work from the leaves up. Might something like this be what you need?
http://nsis.sourceforge.net/Recursiv...nt_directories
( that code looks odd, but might give you some ideas )


Animaether,

Originally posted by Animaether
your problem might be that you're checking from the trunk down to the leaves.. when it needs to work from the leaves up.
No, I do not.
As I wrote
Now I want to delete its folder/directory too if it is empty and so on (recursion).
...
But no matter what I try I only get the first parent deleted. The upper directory on the next loop ...
I am already going from the leaves up to the trunk. ;)
And I am quite familiar with recursion (from other programming and script languages).

Might something like this be what you need?
http://nsis.sourceforge.net/Recursiv...nt_directories
( that code looks odd, but might give you some ideas )
I agree - very odd code ... :rolleyes:

Please have a look at the small test script below
Name "Recursively RMDir test"


>OutFile "RMDir_rec.exe"

>RequestExecutionLevel admin

>!include WordFunc.nsh

ShowInstDetails show

>Function .onInit
SetShellVarContext all
StrCpy$0 "$SMPROGRAMS\Test_root\Test_sub_1\Test_sub_2\Test_sub_3"
CreateDirectory "$0"
Sleep 5000
FunctionEnd

Section
loop:
ClearErrors
RMDir "$0"
Sleep 100
IfErrors end
${WordFind} "$0" "\" "E-2{*" $0
IfErrors 0 loop
end:
SectionEnd
>
This shows the basic concept I am actually using and it works as expected. But it won't (always) do so if comment out the Sleep command in the section!

Therefor my conclusion is that the script is faster (too fast) than the file system.

But how to determine/ detect if the file system is ready before trying to remove the next dir?
Always use an ugly Sleep command (and with which value)?

And the other problem (already described in several threads here in the forum) with unclosed handles (e.g. from Locate or RecFind) also (still) exists.

I achieved to get my function to work with a tremendous effort (writing the paths to a temp file, using sleep commands etc.) instead of using a simple loop like in the above script.

BTW: Would be a nice to have function if you could use "RMDIR /r /ifempty" which will do a recursive removal of empty dirs.

Gunther

You can always change the (arbitrary) sleep to a....


} 

And something in there to bail after 10 seconds or so to prevent getting stuck if the folder was simply not deleteable despite no error being set out of RMDir. But I'm sure you knew this already.

I'm not familiar with any issues of the Locate macro except where the size specification may be used (as that's what opens a file handle, in order to seek to the end, get the new position, and thus determine the file's size).

Originally posted by Animaether
You can always change the (arbitrary) sleep to a....
${DoWhile} ${FileExists} "<folder>"

Sleep 10
>${Loop}
And something in there to bail after 10 seconds or so to prevent getting stuck if the folder was simply not deleteable despite no error being set out of RMDir. But I'm sure you knew this already.
Yes, but always good to read some other/ additional concepts. :up:
Or maybe I can do another "useless/ needless" operation instead of the loop just to asure that the directory is recognized as empty (of course only if it is) after the file deletion?
Any ideas? Maybe opening and closing a file in $TEMP?

I'm not familiar with any issues of the Locate macro except where the size specification may be used (as that's what opens a file handle, in order to seek to the end, get the new position, and thus determine the file's size).
To be honest, I did not investigate this any further, so my first statement could be wrong.

Gunther

Well, my guess is that if the directory is not actually removed at the time RMDir returns successfully and NSIS moves on to the next command, that you're dealing with either some manner of caching or queueing (perhaps from the drive itself). Performing some other IO -might- force the directory removal to be committed, but then again it might not.. seems to me the safest way is to actually check whether the folder has been deleted, and only then carry on.


Originally posted by Animaether
Well, my guess is that if the directory is not actually removed at the time RMDir returns successfully and NSIS moves on to the next command,
Ah wait please ...,
RMDir does not return successfully. It raises an error though the directory is empty! Same with ${DirState} (returns 1 = not empty).
So to me it seems that there has to be a "certain" time between the deletion of the last dir/file in a directory before this directory is being recognized as empty

that you're dealing with either some manner of caching or queueing (perhaps from the drive itself). Performing some other IO -might- force the directory removal to be committed, but then again it might not..
There is at least a chance of "might not ...". :D
And in general I prefer code that works 100% as expected ...! :p

seems to me the safest way is to actually check whether the folder has been deleted, and only then carry on.
I agree. Still the question of how long to wait before one can be sure that the folder is either empty and can be deleted or it is not?

Gunther

well, that's why I suggested the loop.. it's a bit more cpu and i/o intensive (checking the dir's state every-sleep-interval, but at least you won't have to put a blanket '1 second' on there and waste time on most ops, or have it be too short if whatever's causing the discrepancy actually takes longer than 1 second to resolve


OK, I'll give it a try and report back.

Thanks,
Gunther


Here is another (test) version. In about one hundred runs it never failed.


Name "Recursively RMDir test"


>OutFile "RMDir_rec.exe"

>RequestExecutionLevel admin

>!include LogicLib.nsh
>!include WordFunc.nsh

ShowInstDetails show

>Function .onInit
SetShellVarContext all
StrCpy$0 "$SMPROGRAMS\Test_root\Test_sub_1.1"
CreateDirectory "$0"
StrCpy $0 "$SMPROGRAMS\Test_root\Test_sub_1\Test_sub_2\Test_sub_3"
CreateDirectory "$0"
Sleep 5000
FunctionEnd

>Function isEmptyDir
# Stack -> # Stack: <directory>
Exch $0 # Stack: $0
Push $1 # Stack: $1, $0
FindFirst $0 $1 "$0\*.*"
strcmp $1 "." 0 _notempty
FindNext$0 $1
strcmp$1 ".." 0 _notempty
ClearErrors
FindNext$0 $1
IfErrors 0 _notempty
FindClose$0
Pop$1 # Stack: $0
StrCpy $0 1
Exch$0 # Stack: 1 (true)
goto _end
_notempty:
FindClose $0
Pop$1 # Stack: $0
StrCpy $0 0
Exch$0 # Stack: 0 (false)
_end:
>FunctionEnd

Section
Push$0
Push$1
Push$2
loop:
StrCpy $2 0
ClearErrors
RMDir "$0"
IfErrors end
DetailPrint "before Sleep command"
Sleep 100
${DoWhile} ${FileExists} "$0"
IntOp $2 $2 + 1
Push$0
Call isEmptyDir
Pop$1
DetailPrint$2
IntCmp$2 100 end
${Loop}
DetailPrint "before StrCmp command"
StrCmp $1 0 end
${WordFind} "$0" "\" "E-2{*" $0
IfErrors 0 loop
end:
Pop $2
Pop $1
Pop $0
SectionEnd
>
For the archive: The most important command is the 'Sleep 100' command. Without a sleep command the function fails at various points. The value for Sleep might be less but 100 seemed to me being a good compromise.

I now try to imbed it in my real script and see if it works, too. ;)

Gunther