Script Request: Clean folders with only a single file

I'm not sure if this is the proper place to request that someone with more programming skill than I have try to write a script (I haven't done any programming since about 1988 in Pascal...). Anyway, my problem is as follows: I frequently have nested folders, many of which have just a single file in them. What I'd like is a script that will start from a given folder and look through all subfolders (and sub-subfolders, etc.). When the script finds a subfolder with just a single file, that file would be moved up one level (but obviously not above the folder from which the script was begun). Ideally, the script would also delete empty folders that it finds. Thus:

\Music
\Music\The Beatles
\Music\The Beatles\Rubber Soul
\Music\The Beatles\Rubber Soul\Album Art\folder.jpg

In the example above, presume that folder.jpg is the only file in the folder \Music\The Beatles\Rubber Soul\Album Art. I'd like the script to move that file up from the \Album Art folder to the \Rubber Soul folder and then delete the \Album Art folder (because it's now empty). And I'd like the script to search all of the folders under the \Music folder.

Is this doable? Well, I presume it's doable, so let me rephrase: Is this doable, easily?

Thanks in advance for any help (or completed scripts...).

This should be doable via VBScript. Not sure how easy it would be.

Moving files "up" should be possible with a rename, iirc.
And finding empty folders can be done with the find tool, so maybe there is no need for scripting at all?

I recently made some scripts that are similar. Though it does not do exactly what you want, it should get you started. I might add it as a feature to the CleanItemFolder function.
Link to scripts Folder clean up and unrar scripts.

There is a CollapseFolder Function that will move the contents of the given folder to the parent and then remove the folder. You would need to call this function for all folders that have only one file.
I use it to collapse folders in an 'ItemFolder' with a specific name, like CD1, CD2, DVD1 and so on.

My scripts assume that the folder is one of two types, a List folder (I.E. Movies) or an item folder (I.E. a folder in a list folder). Depending on the type of folder will decide what actions will be executed.

Thanks, wowbagger. I'll take a look at those scripts. Any idea how to query whether a folder has only a single file?

FSUtil.ReadDir in a script will let you enumerate the contents of a directory.

I came across this code snipped (which I have no idea how to use...) that looked like it might be useful:

function count_files($path) {

// (Ensure that the path contains an ending slash)

$file_count = 0;

$dir_handle = opendir($path);

if (!$dir_handle) return -1;

while ($file = readdir($dir_handle)) {

    if ($file == '.' || $file == '..') continue;

    if (is_dir($path . $file)){      
        $file_count += count_files($path . $file . DIRECTORY_SEPARATOR);
    }
    else {
        $file_count++; // increase file count
    }
}

closedir($dir_handle);

return $file_count;

}

Found at: jonasjohn.de/snippets/php/co ... ursive.htm

Without looking up the URL, I bet this is PHP code and of limited use, as long as you don't add ActivePHP or PHP.exe to your windows installation. But still, PHP as an add-on to the windows-scripting-host has not been used yet with DO, if I'm correct. So I'm not sure if this works as expected.

What you try to accomplish can be done with jscript alone (DO11 optional here, but makes life much easier), there's no further installation required.

Oh, well. It was worth trying, right? Unfortunately, I have no idea how to use jscript (or anything else, really) to try to create the script I'm looking for. Perhaps playing around with wowbagger's script will give me some ideas, but after looking at it quickly, I don't have high hopes that I'll be able to make heads or tails of it...

Here is something for you to work with. It is quickly put together, has no errorhandling and is not tested to full extend, but:
It moved up singular files for me and even deleted the resulting empty directorys.
Take it as it is, maybe someone is going to polish it up, I give no warranty on what it does for you! o)

You need to copy the code below to the clipboard, enter customize mode in DO and paste clipboard onto any toolbar (a button will appear).
Advice: Run this on test-files and dummy-directories only!

<?xml version="1.0"?> <button backcol="none" display="both" textcol="none"> <label>MoveSingleFilesUp</label> <icon1>#newcommand</icon1> <function type="script"> <instruction>@script jscript</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function OnClick(data){</instruction> <instruction> DOpus.ClearOutput();</instruction> <instruction> data.func.command.ClearFiles();</instruction> <instruction> //get enumerator and count of selected folders or complete list of folders</instruction> <instruction> var folders = GetFolders(data);</instruction> <instruction> while (!folders.enumerator.atEnd() ) {</instruction> <instruction> var folder = folders.enumerator.item(); folders.enumerator.moveNext();</instruction> <instruction> dout(&quot;Folder [&quot;+folder.name+&quot;]..&quot;);</instruction> <instruction> MoveSingleFilesUp( folder.path+&quot;\\&quot;+folder.name);</instruction> <instruction> }</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function ReadFolder( path, recurse, exception){</instruction> <instruction> if (recurse == undefined) recurse=false;</instruction> <instruction> if (exception == undefined) exception=true;</instruction> <instruction> var fEnum = DOpus.FSUtil.ReadDir( path, true);</instruction> <instruction> if (fEnum.error!=0){</instruction> <instruction> var error = &quot;ReadFolder(): Error reading folder [&quot;+name+&quot;], code [&quot;+fEnum.error+&quot;].&quot;;</instruction> <instruction> if (exception) throw error;</instruction> <instruction> dout(error);</instruction> <instruction> }</instruction> <instruction> return fEnum;</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function MoveSingleFilesUp( folderPath){</instruction> <instruction> var fEnum = ReadFolder( folderPath, true, true);</instruction> <instruction> var fileCount = 0;</instruction> <instruction> while (!fEnum.complete &amp;&amp; (fItem = fEnum.Next())){</instruction> <instruction> if (fItem.is_dir)</instruction> <instruction> MoveSingleFilesUp(fItem);</instruction> <instruction> else</instruction> <instruction> fileCount++;</instruction> <instruction> }</instruction> <instruction> if (fileCount==1){</instruction> <instruction> var parentFolder = GetPathLastFolder(new String(folderPath)).parentPath;</instruction> <instruction> if (parentFolder!=&quot;&quot;){</instruction> <instruction> dout(&quot;Moving up: &quot; + fItem.name + &quot; from [&quot;+folderPath+&quot;] to [&quot;+parentFolder+&quot;]&quot;);</instruction> <instruction> var cmd= &apos;COPY MOVE FILE &quot;&apos;+folderPath+&apos;\\&apos;+fItem.Name+&apos;&quot; TO &quot;&apos;+parentFolder+&apos;&quot;&apos;;</instruction> <instruction> DOpus.NewCommand.RunCommand(cmd);</instruction> <instruction> var cmd= &apos;DELETE &quot;&apos;+folderPath+&apos;&quot;&apos;;</instruction> <instruction> DOpus.NewCommand.RunCommand(cmd);</instruction> <instruction> }</instruction> <instruction> }</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function GetFolders(data){</instruction> <instruction> if (data.func.sourcetab.selected_dirs.count)</instruction> <instruction> var dirs = data.func.sourcetab.selected_dirs;</instruction> <instruction> else</instruction> <instruction> var dirs = data.func.sourcetab.dirs;</instruction> <instruction> return { enumerator:new Enumerator(dirs), count:dirs.count};</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function GetPathLastFolder(mypath) {</instruction> <instruction> mypath = mypath.replace(/(^(\s|\/|\\)+)|((\s|\/|\\)+$)/g, &quot;&quot;);</instruction> <instruction> var lastIndex = mypath.lastIndexOf(&apos;\\&apos;);</instruction> <instruction> if (lastIndex == -1 || (lastIndex + 1) == mypath.length) return {</instruction> <instruction> folder: mypath,</instruction> <instruction> parentPath: &quot;&quot;</instruction> <instruction> };</instruction> <instruction> return {</instruction> <instruction> folder: mypath.substring(lastIndex + 1),</instruction> <instruction> parentPath: mypath.substring(0, lastIndex)</instruction> <instruction> };</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function dout(text){</instruction> <instruction> DOpus.Output(text);</instruction> <instruction>}</instruction> </function> </button>

Thanks, tbone! As soon as I can get some free time, I'll give this a test. I appreciate the help!

@tbone - it doesn't appear that the code recursively moves single files up the tree. Try it on a data set like:

[code]$ tree Foo
Foo
├── dirA
│ └── dirD
│ ├── dirF
│ │ ├── file2.txt
│ │ └── file3.txt
│ └── dirG
│ └── dirI
│ └── dirJ
│ └── dirK
│ └── file1.txt
├── dirB
│ └── file5.txt
└── dirC
└── dirE
└── dirH
├── file6.txt
└── file7.txt

11 directories, 6 files[/code]

It should end up like:

[code]$ tree Foo
Foo
├── dirA
│ └── dirD
│ ├── dirF
│ │ ├── file2.txt
│ │ └── file3.txt
│ └── file1.txt
├── dirC
│ └── dirE
│ └── dirH
│ ├── file6.txt
│ └── file7.txt
└── file5.txt

6 directories, 6 files[/code]

I have perl code that does this if you want it.

The pascal background you have, even if some years have past, should get you going quite easily, I think.
There's nothing new in JScript compared to Pascal as far as loops and conditional logic is concerned, I assume that at least. o)

How you get things done with JScript can be found easy on google or stackoverflow or any admin-related site on the net.
You might want to add "-javascript" to your google searches (notice the "-"), as google gives to much javascript related content when "jscript" is part of the query.

For handling files and folders without using DO, you can look for "WScript.FilesystemObject" especially, it is a module for handling - you guess it - files and folders in scripting.
As Leo mentioned, DO has some helper objects too. "FSUtil" for handling/reading files and folders, "Path" for basic operations on paths and DOpus.NewCommand to get standard raw commands going (for copy/move/rename etc.).

If you wanna try some basic JScript first, see "cscript.exe" command to run JScript files on the commandline independently (from DO e.g.). The internet again, is your best friend on getting started with that, lots of tutorials and ready to use code out there! o)

@MrC
That is true, I obviously did not understand how it should work. Sorry for that. o)

Try this one..

<?xml version="1.0"?> <button backcol="none" display="both" textcol="none"> <label>MoveSingleFilesUp</label> <icon1>#newcommand</icon1> <function type="script"> <instruction>@script jscript</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function OnClick(data){</instruction> <instruction> DOpus.ClearOutput();</instruction> <instruction> data.func.command.ClearFiles();</instruction> <instruction> //get enumerator and count of selected folders or complete list of folders</instruction> <instruction> var folders = GetFolders(data);</instruction> <instruction> while (!folders.enumerator.atEnd() ) {</instruction> <instruction> var folder = folders.enumerator.item(); folders.enumerator.moveNext();</instruction> <instruction> dout(&quot;Folder [&quot;+folder.name+&quot;]..&quot;);</instruction> <instruction> while (MoveSingleFilesUp( folder.path+&quot;\\&quot;+folder.name)){};</instruction> <instruction> }</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function ReadFolder( path, recurse, exception){</instruction> <instruction> if (recurse == undefined) recurse=false;</instruction> <instruction> if (exception == undefined) exception=true;</instruction> <instruction> var fEnum = DOpus.FSUtil.ReadDir( path, true);</instruction> <instruction> if (fEnum.error!=0){</instruction> <instruction> //throw &quot;ReadFolder(): Error reading folder [&quot;+path+&quot;], code [&quot;+fEnum.error+&quot;].&quot;;</instruction> <instruction> }</instruction> <instruction> return fEnum;</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function MoveSingleFilesUp( folderPath){</instruction> <instruction> var fEnum = ReadFolder( folderPath, true, true);</instruction> <instruction> var fileCount = 0;</instruction> <instruction> var didSomething = 0;</instruction> <instruction> while (!fEnum.complete &amp;&amp; (fItem = fEnum.Next())){</instruction> <instruction> if (fItem.is_dir)</instruction> <instruction> didSomething += MoveSingleFilesUp(fItem);</instruction> <instruction> else</instruction> <instruction> fileCount++;</instruction> <instruction> }</instruction> <instruction> if (fileCount==1){</instruction> <instruction> var parentFolder = GetPathLastFolder(new String(folderPath)).parentPath;</instruction> <instruction> if (parentFolder!=&quot;&quot;){</instruction> <instruction> dout(&quot;Moving up: &quot; + fItem.name + &quot; from [&quot;+folderPath+&quot;] to [&quot;+parentFolder+&quot;]&quot;);</instruction> <instruction> var cmd= &apos;COPY MOVE FILE &quot;&apos;+folderPath+&apos;\\&apos;+fItem.Name+&apos;&quot; TO &quot;&apos;+parentFolder+&apos;&quot;&apos;;</instruction> <instruction> DOpus.NewCommand.RunCommand(cmd);</instruction> <instruction> var cmd= &apos;DELETE &quot;&apos;+folderPath+&apos;&quot;&apos;;</instruction> <instruction> DOpus.NewCommand.RunCommand(cmd);</instruction> <instruction> didSomething++;</instruction> <instruction> }</instruction> <instruction> }</instruction> <instruction> return didSomething;</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function GetFolders(data){</instruction> <instruction> if (data.func.sourcetab.selected_dirs.count)</instruction> <instruction> var dirs = data.func.sourcetab.selected_dirs;</instruction> <instruction> else</instruction> <instruction> var dirs = data.func.sourcetab.dirs;</instruction> <instruction> return { enumerator:new Enumerator(dirs), count:dirs.count};</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function GetPathLastFolder(mypath) {</instruction> <instruction> mypath = mypath.replace(/(^(\s|\/|\\)+)|((\s|\/|\\)+$)/g, &quot;&quot;);</instruction> <instruction> var lastIndex = mypath.lastIndexOf(&apos;\\&apos;);</instruction> <instruction> if (lastIndex == -1 || (lastIndex + 1) == mypath.length) return {</instruction> <instruction> folder: mypath,</instruction> <instruction> parentPath: &quot;&quot;</instruction> <instruction> };</instruction> <instruction> return {</instruction> <instruction> folder: mypath.substring(lastIndex + 1),</instruction> <instruction> parentPath: mypath.substring(0, lastIndex)</instruction> <instruction> };</instruction> <instruction>}</instruction> <instruction>///////////////////////////////////////////////////////////////////////////////</instruction> <instruction>function dout(text){</instruction> <instruction> DOpus.Output(text);</instruction> <instruction>}</instruction> </function> </button>

Here's an algorithm (it could be better optimized):

use v5.16;
use strict;
use warnings;

use File::Copy;
use File::Spec;
use File::Basename;

my $level = 0;
while (@ARGV) {
    process_dir($ARGV[0], shift);
}

sub process_dir {
    my ($topdir, $process_dir) = @_;

RESET:
    opendir(my $dh, $process_dir) or die "Failed to open directory: $process_dir: $!";
    my @dir_entries;
    while(readdir $dh) {
        next if $_ eq '.' or $_ eq '..';
        my $entry = File::Spec->catdir($process_dir, $_);
        push @dir_entries, $entry;
        if (-d $entry) {
            $level++;
            if (process_dir($topdir, $entry)) {
                closedir $dh;
                @dir_entries = ();
                goto RESET;
            }
            $level--;
        }
    }
    if (@dir_entries == 1 and ! -d $dir_entries[0]) {
        my ($file, $dir) = fileparse($dir_entries[0]);
        $dir =~ s/[\\\/]$//;
        if ($dir ne $topdir) {
            my $parentdir = dirname($dir);
            File::Copy::mv $dir_entries[0], $parentdir  or die "Failed to copy file $dir_entries[0]: $!";
            rmdir $dir                                  or die "Failed to remove directory $dir: $!";
            return 1;
        }
    }
    closedir $dh;
    return 0;
}

Unfortunately, my Pascal programming days ended about ... um ... 27 years ago... Sigh.

I've used PHP with DO9(?), although it is years ago.
At least back then it was very hard to use because if the script had any errors at all it crashed DO.

I played around with tbone's script. It seems to work very well (though I want to test some more before I run it on "real" files & folders). A few quick thoughts:

  1. Right now, the script asks the user to confirm the deletion of each folder. This can become tedious if the script is being run on many folders. Is there a way to turn off the confirmation dialog?
  2. When "promoting" a file, DO asks a user to replace/rename/skip (etc.) if a file already exists. Again, can this be automated (for example, having DO automatically "rename new")?
  3. How do you stop a script during execution?
  4. Perhaps the script should automatically operate on all folders in the source folder instead of requiring the user to select folders and then run.

Ideally, all of this could be put into a dialog of user options when the script is run, but I know that's probably way more involved than tbone or anyone else is interested in!

Again, thanks for all of the help. This is going to really conserve some time and effort.