Take the 2-minute tour ×
Server Fault is a question and answer site for professional system and network administrators. It's 100% free, no registration required.

Does anyone know how to search through thousands of subdirectories for all directories that contain only 1 file and not more than 1 file?

Any suggestion on what tool to use or an easy code snippet?

share|improve this question

6 Answers 6

up vote 10 down vote accepted

In PowerShell, this is one way you could do it:

PS> Get-ChildItem -recurse | `
     Where {$_.PSIsContainer -and `
           @(Get-ChildItem $_.Fullname | Where {!$_.PSIsContainer}).Length -eq 1}

The $_.PSIsContainer returns true for dirs and false for files. The @() syntax ensures the result of the expression is an array. If its length is 1 then there's only one file in that dir. This example also makes use of a nested pipeline e.g. Get-ChildItem $_.Fullname | Where {...} within the first Where scriptblock.

share|improve this answer
    
I tried this and it works well. –  Bratch Oct 14 '09 at 19:55
    
yep, that was easy. it works (minus the backtick marks) in my Powershell ISE IDE. thanks much! –  djangofan Oct 15 '09 at 21:17
    
The backtick is the line continuation char and come in handy on the console host (PowerShell prompt). –  user19468 Oct 16 '09 at 1:38

Here is a Perl solution (tested on Windows):

#!perl

use strict;
use warnings;

use File::Find;
use File::Slurp;
use File::Spec::Functions qw(catfile canonpath rel2abs);

my ($top) = @ARGV;
die "Provide top directory\n" unless defined($top) and length $top;

find(\&wanted, $top);

sub wanted {
    my $name = $File::Find::name;
    return unless -d $name;
    return unless 1 == grep { -f catfile($name, $_) } read_dir $name;
    print canonpath(rel2abs $name), "\n";
}

Output:

C:\Temp> f .
C:\Temp\1
C:\Temp\chrome_9999
C:\Temp\CR_3E.tmp
share|improve this answer

If this was on Linux I would be tempted to use a command like this.

find . -type 'f' -printf '%h\n' | sort | uniq -c

The find command will print out the directory name of all the files. Which we then run through sort, and then use the -c option of uniq to give us the number of files per directory. Once you have the count per directory it should be easy enough to just grep out the directories with a value of 1.

If you would prefer to perform actions on the directories while keeping it on one line, you can pipe the results through awk to xargs. For example, to delete each folder:

find . -type 'f' -printf '%h\n' | sort | uniq -c | awk '{ if ($1 == "1") printf "%s%c",$2,0 }' | xargs -0 -I {} rm -rf {}

This prints out each directory with a value of 1 to a null terminated string, which can then be taken as arguments to xargs. You use a null terminated string so that spaces will be handled as expected. In xargs, the {} characters will be replaced by each passed argument.

share|improve this answer
    
I'm confused. Isn't the question tagged "powershell"? –  Dennis Williamson Oct 14 '09 at 19:02
    
I always like seeing 'Well, under a real OS with real tools I would do xxx' answers. –  MikeyB Oct 14 '09 at 19:35
1  
@Dennis, yes it is, but it is also tagged perl, and it doesn't include a requirement for a specific OS. It seemed to me that the OP was looking for any available solution. I figured one that included installing GNU tools may be acceptable, or at least useful to someone else in the future. –  Zoredache Oct 14 '09 at 21:37

I think you could craft something in a few lines using

File::Find

Something like this.

#!/usr/bin/perl
use File::Find;
my $base_dir = '/';
find( 
  sub {
    # do stuff on each file here.
    $filename = $File::Find::name;
    $dir = $File::Find::dir;
  }, $base_dir );
);

EDIT: I really like Zoredache's find method better, but you did tag this as perl.

share|improve this answer
    
This answer isn't really complete because the OP wants to find only those directories which have exactly one file in them. –  John Gardeniers Oct 15 '09 at 2:54

Now if you want to do something with those folders.


$RootFolder = "c:\myfolder"
$FoldersWithOnlyOneFile = Get-ChildItem $RootFolder -Recurse | `
    Where {$_.PSIsContainer -and @( Get-ChildItem $_.Fullname | Where {!$_.PSIsContainer}).Length -eq 1 `
                            -and @( Get-ChildItem $_.Fullname | Where {$_.PSIsContainer}).Length -eq 0 }


Foreach($folder in $FoldersWithOnlyOneFile)
{
    $Folder.FullName
   Get-ChildItem $Folder.FullName
}

share|improve this answer

The solution with:

sub wanted {
    my $name = $File::Find::name;
    return unless -d $name;
    return unless 1 == grep { -f catfile($name, $_) } read_dir $name;
    print canonpath(rel2abs $name), "\n";
}

Needlessly reads each directory to count items within it, then reads it again when actually descending it (as part of the File::Find framework).

A simpler solution is just to descend, charging the presence of each file to the directory that contains it:

my %count = 0;
...
sub wanted {
  return unless -f;
  $count{$File::Find::dir}++;
}

my @one_file_dirs = sort grep { $count{$_} == 1 } keys %count;
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.