After looking into this for a while, I've found a lot of people trying to do the same thing but I haven't yet been able to find a complete answer so I'm hoping someone can help us!
What I would like to do is give 2 directories, A and B, and compare everything inside A to everything inside B and if there are any files or folders in B that are not in A then generate an output file detailing the path to the item that is in B and not A. From Here, Id like to use the list of items and say if any of the items in these paths contain something different to the files in 2 directories (the names will be the same so documents\folder\B.txt contains something different to desktop\folder\B.txt) generate another list showing what is different or showing the file path to items that are different if I cant show the text that is different.
I've looked into doing this with .hash but I'm not sure if that's to best way to go about it? something like:
$SourceDocs = Get-ChildItem –Path C:\Documents1 | foreach {Get-FileHash –Path $_.FullName} $DestDocs = Get-ChildItem –Path C:\Documents2 | foreach {Get-FileHash –Path $_.FullName}
In terms of what I have for the physical comparison, I originally was trying to do a 3-way comparison so the code below isn't accurate but its all I've got as yet.
$folderA = 'C:\Users\USERNAME\Desktop\Folder A'
$folderB = 'C:\Users\USERNAME\Desktop\Folder B'
$folderC = 'C:\Users\USERNAME\Desktop\Folder C'
$AChild = Get-ChildItem $folderA -Recurse
$BChild = Get-ChildItem $folderB -Recurse
$CChild = Get-ChildItem $folderC -Recurse
Compare-Object -ReferenceObject $AChild -DifferenceObject $BChild, $CChildcode
After looking into this further, Ive found out it might be better to use 3.0 rather than 2.0 so I can use -Dir more efficiently.
Any questions, let me know.
Any help greatly appreciated.
This is answer to what you want:
What I would like to do is give 2 directories, A and B, and compare everything inside A to everything inside B and if there are any files or folders in B that are not in A then generate an output file detailing the path to the item that is in B and not A.
$folder1 = "C:\Users\USERNAME\Desktop\Folder A"
$folder2 = "C:\Users\USERNAME\Desktop\Folder B"
# Get all files under $folder1, filter out directories
$firstFolder = Get-ChildItem -Recurse $folder1 | Where-Object { -not $_.PsIsContainer }
$failedCount = 0
$i = 0
$totalCount = $firstFolder.Count
$firstFolder | ForEach-Object {
$i = $i + 1
Write-Progress -Activity "Searching Files" -status "Searching File $i of $totalCount" -percentComplete ($i / $firstFolder.Count * 100)
# Check if the file, from $folder1, exists with the same path under $folder2
If ( Test-Path ( $_.FullName.Replace($folder1, $folder2) ) ) {
# Compare the contents of the two files...
If ( Compare-Object (Get-Content $_.FullName) (Get-Content $_.FullName.Replace($folder1, $folder2) ) ) {
# List the paths of the files containing diffs
$fileSuffix = $_.FullName.TrimStart($folder1)
$failedCount = $failedCount + 1
Write-Host "$fileSuffix is on each server, but does not match"
}
}
else
{
$fileSuffix = $_.FullName.TrimStart($folder1)
$failedCount = $failedCount + 1
Write-Host "$fileSuffix is only in folder 1"
$fileSuffix | Out-File "$env:userprofile\desktop\folder1.txt" -Append
}
}
$secondFolder = Get-ChildItem -Recurse $folder2 | Where-Object { -not $_.PsIsContainer }
$i = 0
$totalCount = $secondFolder.Count
$secondFolder | ForEach-Object {
$i = $i + 1
Write-Progress -Activity "Searching for files only on second folder" -status "Searching File $i of $totalCount" -percentComplete ($i / $secondFolder.Count * 100)
# Check if the file, from $folder2, exists with the same path under $folder1
If (!(Test-Path($_.FullName.Replace($folder2, $folder1))))
{
$fileSuffix = $_.FullName.TrimStart($folder2)
$failedCount = $failedCount + 1
Write-Host "$fileSuffix is only in folder 2"
$fileSuffix | Out-File "$env:userprofile\desktop\folder2.txt" -Append
}
}
Related
I am trying to create directories and subdirectories based on the names of existing files. After that I want to move those files into the according directories. I have already come pretty far, also with the help of
here and here, but I am failing at some point.
Existing Test Files Actually about 5000 files
Folder structure This is how it should look like afterwards
MM0245AK625_G03_701.txt
MM\MM0245\625\G03\MM0245AK625_G03_701.txt
MM0245AK830_G04_701.txt
MM\MM0245\830\G04\MM0245AK830_G04_701.txt
VY0245AK_G03.txt
VY\VY0245\VY0245AK_G03.txt
VY0245AK_G03_701.txt
VY\VY0245\G03\VY0245AK_G03_701.txt
VY0245AK625_G03.txt
VY\VY0245\625\VY0245AK625_G03.txt
VY0245AK625_G03_701.txt
VY\VY0245\625\G03\VY0245AK625_G03_701.txt
VY0345AK625_G03_701.txt
VY\VY0345\625\G03\VY0345AK625_G03_701.txt
Code for creating those files is at the end of this post.
As you can see, the files do match some kind of pattern, but not consistently. I use multiple copies of my code with different 'parameters' to sort each type of filepattern, but there gotta be a more streamline way.
Existing code
$dataPath = "$PSScriptRoot\Test"
#$newDataPath = "$PSScriptRoot\"
Get-ChildItem $dataPath -Filter *.txt | % {
$g1 = $_.BaseName.Substring(0, 2)
$g2 = $_.BaseName.Substring(0, 6)
$g3 = $_.BaseName.Substring(8, 3)
$g4 = $_.BaseName.Substring(12, 3)
$path = "$DataPath\$g1\$g2\$g3\$g4"
if (-not (Test-Path $path)) {
New-Item -ItemType Directory -Path $path
}
Move-Item -Path $_.FullName -Destination $path
}
This code also creates directories in the 3rd $g3 layer for files in "the shorter format", e.g. XX0000AK_G00.txt. This file should however not be moved further than layer $g2. Of course the code above is not capable of doing this, so I tried it with regex below.
This is an alternative idea (not worked out furhter than creating directories), but I failed to continue after
Select-Object -Unique. I am failing to use $Matches[1] in New-Item, because I can only Select-Object -unique the variable $_, not $Matches[1] or even the subdirectory "$($Matches[1])$($Matches[2])". The following code is my attempt.
cd $PSScriptRoot\Test
# Create Folder Layer 1
Get-ChildItem |
% {
$_.BaseName -match "^(\w{2})(\d{4})AN(\d{3})?_(G\d{2})(_\d{3})?$" | Out-Null
$Matches[1]
"$($Matches[1])$($Matches[2])"
} |
Select-Object -Unique |
% {
New-Item -ItemType directory $_
} | Out-Null
I am fairly new to powershell, please don't be too harsh :) I also don't have a programming background, so please excuse the use of incorrect wording.
new-item $dataPath\MM0245AK830_G04_701.txt -ItemType File
new-item $dataPath\VY0245AK_G03.txt -ItemType File
new-item $dataPath\VY0245AK_G03_701.txt -ItemType File
new-item $dataPath\VY0245AK625_G03.txt -ItemType File
new-item $dataPath\VY0245AK625_G03_701.txt -ItemType File
new-item $dataPath\VY0345AK625_G03_701.txt -ItemType File
i am truly bad at complex regex patterns [blush], so this is done with simple string ops, mostly.
what the code does ...
fakes reading in some files
when you have tested this and it works as needed on all your test files, replace the entire #region/#endregion block with a Get-ChildItem call.
iterates thru the collection
splits the BaseName on ak & saves it for later use
checks for a the two short file layouts
checks for 1 _ versus 2
builds the $Dir string for each of those 2 filename layouts
builds the long file name $Dir
uses the previous $Dir stuff to build the $FullDest for each file
shows the various stages for each file
that last section would be replaced with your mkdir & Move-Item commands.
the code ...
#region >>> fake reading in files
# when ready to use the real things, use $Get-ChildItem
$InStuff = #'
MM0245AK625_G03_701.txt
MM0245AK830_G04_701.txt
VY0245AK_G03.txt
VY0245AK_G03_701.txt
VY0245AK625_G03.txt
VY0245AK625_G03_701.txt
VY0345AK625_G03_701.txt
'# -split [System.Environment]::NewLine |
ForEach-Object {
[System.IO.FileInfo]$_
}
#endregion >>> fake reading in files
foreach ($IS_Item in $InStuff)
{
$BNSplit_1 = $IS_Item.BaseName -split 'ak'
if ($BNSplit_1[-1].StartsWith('_'))
{
if (($BNSplit_1[-1] -replace '[^_]').Length -eq 1)
{
$Dir = '{0}\{1}' -f $IS_Item.BaseName.Substring(0, 2),
$IS_Item.BaseName.Substring(0, 6)
}
else
{
$Dir = '{0}\{1}\{2}' -f $IS_Item.BaseName.Substring(0, 2),
$IS_Item.BaseName.Substring(0, 6),
$IS_Item.BaseName.Split('_')[1]
}
}
else
{
$Dir = '{0}\{1}\{2}\{3}' -f $IS_Item.BaseName.Substring(0, 2),
$IS_Item.BaseName.Substring(0, 6),
$BNSplit_1[-1].Split('_')[0],
$BNSplit_1[-1].Split('_')[1]
}
$FullDest = Join-Path -Path $Dir -ChildPath $IS_Item
#region >>> show what was done with each file
# replace this block with your MkDir & Move-Item commands
$IS_Item.Name
$Dir
$FullDest
'depth = {0}' -f ($FullDest.Split('\').Count - 1)
'=' * 20
#endregion >>> show what was done with each file
}
the output ...
MM0245AK625_G03_701.txt
MM\MM0245\625\G03
MM\MM0245\625\G03\MM0245AK625_G03_701.txt
depth = 4
====================
MM0245AK830_G04_701.txt
MM\MM0245\830\G04
MM\MM0245\830\G04\MM0245AK830_G04_701.txt
depth = 4
====================
VY0245AK_G03.txt
VY\VY0245
VY\VY0245\VY0245AK_G03.txt
depth = 2
====================
VY0245AK_G03_701.txt
VY\VY0245\G03
VY\VY0245\G03\VY0245AK_G03_701.txt
depth = 3
====================
VY0245AK625_G03.txt
VY\VY0245\625\G03
VY\VY0245\625\G03\VY0245AK625_G03.txt
depth = 4
====================
VY0245AK625_G03_701.txt
VY\VY0245\625\G03
VY\VY0245\625\G03\VY0245AK625_G03_701.txt
depth = 4
====================
VY0345AK625_G03_701.txt
VY\VY0345\625\G03
VY\VY0345\625\G03\VY0345AK625_G03_701.txt
depth = 4
====================
I would first split each file BaseName on the underscore. Then use a regex to split the first part into several array elements, combine that with a possible second part of the split in order to create the destination folder path for the files.
$DataPath = "$PSScriptRoot\Test"
$files = Get-ChildItem -Path $DataPath -Filter '*_*.txt' -File
foreach ($file in $files) {
$parts = $file.BaseName -split '_'
# regex your way to split the first part into path elements (remove empty items)
$folders = [regex]::Match($parts[0], '(?i)^(.{2})(\d{4})[A-Z]{2}(\d{3})?').Groups[1..3].Value | Where-Object { $_ -match '\S'}
# the second part is a merge with the first part
$folders[1] = $folders[0] + $folders[1]
# if there was a third part after the split on the underscore, add $part[1] (i.e. 'Gxx') to the folders array
if ($parts.Count -gt 2) { $folders += $parts[1] }
# join the array elements with a backslash (i.e. [System.IO.Path]::DirectorySeparatorChar)
# and join all tat to the $DataPath to create the full destination for the file
$target = Join-Path -Path $DataPath -ChildPath ($folders -join '\')
# create the folder if that does not yet exist
$null = New-Item -Path $target -ItemType Directory -Force
# move the file to that (new) directory
$file | Move-Item -Destination $target -WhatIf
}
The -WhatIf switch makes the code not move anything to the new destination, it will only display where the file would go to. Once you are happy with that information, remove -WhatIf and run the code again
After moving your filestructure will look like this:
D:\TEST
+---MM
| \---MM0245
| +---625
| | \---G03
| | MM0245AK625_G03_701.txt
| |
| \---830
| \---G04
| MM0245AK830_G04_701.txt
|
\---VY
+---VY0245
| | VY0245AK_G03.txt
| |
| +---625
| | | VY0245AK625_G03.txt
| | |
| | \---G03
| | VY0245AK625_G03_701.txt
| |
| \---G03
| VY0245AK_G03_701.txt
|
\---VY0345
\---625
\---G03
VY0345AK625_G03_701.txt
I have a file structure like:
RootDir\
ADir
1.xml
2.xml
BDir
1.xml
2.xml
3.xml
CDir
2.xml
3.xml
I need to write script to recursively check if a file exists in more than one directory under RootDir, where the file path is identical from one level below RootDir.
For example RootDir\ADir\1.xml also exists in location RootDir\BDir\1.xml
The code below returns the path of matching entries, however it also continues to match with iteself, despite the -exclude switch
$rootCompare = "C:\Users\XXX\Documents\Compare\RootDir"
$rootItems = get-childitem -Path C:\Users\XXX\Documents\Compare\RootDir -Recurse
foreach ($i in $rootItems){
$dirItems = get-childitem $i -File -Recurse | ForEach-Object {
$fullName = $_.fullName
$baseName = $_.baseName
$baseExt = $_.Extension
Write-Host "fullname="$fullName
if (Test-Path $rootCompare\*\$baseName$baseExt -Exclude $fullName) {
$fileSearch = get-ChildItem -Path $rootCompare\*\$baseName$baseExt -Exclude "*$fullName*"
Write-Host "searching for file: "$fullName
Write-Host "file exists in another location: "$fileSearch
}
}
}
Example return:
fullname= C:\Users\XXX\Documents\Compare\RootDir\CDir\3.xml
searching for file: C:\Users\XXX\Documents\Compare\RootDir\CDir\3.xml
file exists in another location: C:\Users\XXX\Documents\Compare\RootDir\ADir\3.xml
C:\Users\XXX\Documents\Compare\RootDir\CDir\3.xml
1) How can I adapt the exclude condition to prevent it from matching with itself?
2) This version only works at 1 level below RootDir. How should it be adapted to work n levels below RootDir?
For example RootDir\ADir\DDir\1.xml should also match RootDir\BDir\DDir\1.xml but not RootDir\ADir\1.xml
Important point:
The number, name and paths the of directories are dynamic and cannot be hard-coded
The quick and dirty way of how I would do it is to do a single Get-ChildItem -Recurse into a variable. Then I could iterate through that variable twice. Once to get the first list of objects, and a second to compare to the first. Then in the comparison we want to match where the Name is the same, and the FullName doesn't match.
$rootCompare = "C:\Users\XXX\Documents\Compare\RootDir"
$rootItems = get-childitem -Path $rootCompare -Recurse
foreach ($i in $rootItems){
foreach ($j in $rootItems){
if($i.Name -eq $j.Name -and $i.fullName -ne $j.fullName)
{
Write-Host "file" $i.fullName
Write-Host "also exists in another location: "$j.fullName
}
}
}
My problem is, that the string for replacement needs to change according to the folder depth the designated file is located and I don't have a clue how to get that info. I need to work with relative addresses.
I want the script to be run from 2 folder levels above the folder where all the files are that need correcting. So I've set the $path in line 1. That folder suppose to be 'depth 0'. In here, the replacement string needs to be in it's native form -> stylesheet.css.
For files in the folders one level below 'depth 0' the string for replacement needs to be prefixed with ../ once -> ../stylesheet.css.
For files in the folders two level below 'depth 0' the string for replacement needs to be prefixed with ../ twice -> ../../stylesheet.css.
...and so on...
I'm stuck here:
$depth = $file.getDepth($path) #> totally clueless here
I need $depth to contain the number of folders under the root $path.
How can I get this? Here's the rest of my code:
$thisLocation = Get-Location
$path = Join-Path -path $thisLocation -childpath "\Files\depth0"
$match = "findThisInFiles"
$fragment = "stylesheet.css" #> string to be prefixed n times
$prefix = "../" #> prefix n times according to folder depth starting at $path (depth 0 -> don't prefix)
$replace = "" #> this will replace $match in files
$depth = 0
$htmlFiles = Get-ChildItem $path -Filter index*.html -recurse
foreach ($file in $htmlFiles)
{
$depth = $file.getDepth($path) #> totally clueless here
$replace = ""
for ($i=0; $i -lt $depth; $i++){
$replace = $replace + $prefix
}
$replace = $replace + $fragment
(Get-Content $file.PSPath) |
Foreach-Object { $_ -replace $match, $replace } |
Set-Content $file.PSPath
}
Here's a function I've written that uses Split-Path recursively to determine the depth of a path:
Function Get-PathDepth ($Path) {
$Depth = 0
While ($Path) {
Try {
$Parent = $Path | Split-Path -Parent
}
Catch {}
if ($Parent) {
$Depth++
$Path = $Parent
}
else {
Break
}
}
Return $Depth
}
Example usage:
$MyPath = 'C:\Some\Example\Path'
Get-PathDepth -Path $MyPath
Returns 3.
Unfortunately, I had to wrap Split-Path in a Try..Catch because if you pass it the root path then it throws an error. This is unfortunate because it means genuine errors won't cause an exception to occur but can't see a way around this at the moment.
The advantage of working using Split-Path is that you should get a consistent count regardless of whether a trailing \ is used or not.
Here is a way to get the depth in the folder structure for all files in a location. Hope this helps get you in the right direction
New-Item -Path "C:\Logs\Once\Test.txt" -Force
New-Item -Path "C:\Logs\Twice\Folder_In_Twice\Test.txt" -Force
$Files = Get-ChildItem -Path "C:\Logs\" -Recurse -Include *.* | Select-Object FullName
foreach ($File in $Files) {
[System.Collections.ArrayList]$Split_File = $File.FullName -split "\\"
Write-Output ($File.FullName + " -- Depth is " + $Split_File.Count)
}
Output is this just for illustration
C:\Logs\Once\Test.txt -- Depth is 4
C:\Logs\Twice\Folder_In_Twice\Test.txt -- Depth is 5
I need to organize many folders, this folders must have some kind of format in their names. I need to rename them and put them like this:
parent folder 2nd name + "_" + 2 digits + "_" + folder name
This needs to happen with every folder where the .ps1 is executed.
Here are some better explanations, here we have some folders:
00_00_Master folder
+- 00_01_Activities
`- 00_02_Schedules
+- 02_00_dir1
`- 02_01_dir2
As you can see, the subfolders inside 00_02_Schedules start with the 2nd "name".
Etc, with all folders (never files). Now for a better explanation, I added (it will happen a lot) 3 more folders inside 00_02_Schedules. It should be like this (before and after executing .ps1)
Before:
...
`- 00_02 Schedules
+- 02_00_dir1
+- 02_01_dir2
+- dir3
+- 12345dir4
`- 01_01_dir5
After:
...
`- 00_02 Schedules
+- 02_00_dir1
+- 02_01_dir2
+- 02_02_dir3
+- 02_03_dir4
`- 02_04_dir5
I will try to explain a little (I put all the code I had, even when I think some is useless now)
$var1 = 0
$var2 = 0
# It always has to work in the same place, never with a fixed path like
# C:\Users\etc\etc
$arr1 = Get-ChildItem -Path .\ -Directory
$arr2 = New-Object System.Collections.ArrayList
[string]$dir = Get-Location
$ruta = $dir | Split-Path -Leaf
# Did these 2 things because I needed the folder's name, I didn't know how
# to do it better
# I think we can say this following code is useless now, I think I tried to do
# an array of arrays so I could cleave the name
foreach ($Name in $arr1) {
[void]$arr2.Add($Name)
}
foreach ($Directory in $arr1) {
if ($Directory -match '^[0-9].*') {
Rename-Item \$Directory -NewName { [string]($_.Name).Substring(5) }
# This is the part I've being having most problems with. I may say the
# code works...as long a folder's name doesn't start with a number,
# because it enters inside the loop and many error logs pops out. Also
# it renames the folders previously renamed like 00_01_00_01, when it's
# supposed to delete before rename them.
# Also, I think it won't work if the numeric extension isn't equal to 5.
}
}
$arr3 = $ruta -split '_'
$foldername = $arr3[1]
# Works, It splits and give me the second "piece" between the 2 "_"
foreach ($Directory in $arr1) {
Rename-Item "$Directory" -NewName $("$foldername" + "_" + "$var1" + "$var2" + "_" + $Directory.Name)
# I think it should have been: (I put it that way before)
# $("$foldername" + "_" + "$var2" + "$var1" + "_" + $Directory.Name)
# but if I did that way, it renamed them like 10,20,30 instead of 01,02,03 etc.
$var++
if ($var1 = 9) {
$var1 = 0
$var2++
# I suppose is not fancy, but in the result output I can see 2 numbers, not 1
}
}
One more thing, is it possible to transform this (fixed) code to work in all folders in a recursive way? (would save a lot of work and executions).
With this initial situation:
> tree
└───00_00_Master folder
├──00_01_Activities
└──00_02_Schedules
├──01_01_dir5
├──02_00_dir1
├──02_01_dir2
├──12345dir4
└──dir3
This script (adapt the Set-Location to fit your environment):
## Q:\Test\2017\08\18\SO_45750567.ps1
Function Test-DirSchema ($DirName) {
# assume parent is already processed and complies with schema
$ThisDir = GI $DirName
# Get parent's second number for our first
$First = $ThisDir.Parent.Name.Split('_')[1]
If ($ThisDir.Name -Match "^$($First)_\d{2}_.*") {
"{0} : {1} complies with schema" -f $First,$ThisDir.Name
} else {
$Highest = (gci "$($ThisDir.Parent)\$($First)_??_*"|Sort|Select -Last 1).Name
$Second = [int]($Highest -Replace('^\d{2}_(\d{2})_.*','$1'))
$Second = ($Second + 1).ToString('00')
"{0} : {1} does not comply with schema" -F $First,$ThisDir.Name
$NewName = "{0}_{1}_{2}" -F $First, $Second,$ThisDir.Name.TrimStart('0123456789_')
"new: {0}" -F $NewName
$ThisDir | Rename-Item -NewName $NewName
}
"----"
}
Set-Location "A:\00_00_Master folder"
Get-ChildItem "[0-9][0-9]_*" -Rec |
Where-Object {$_.PSIsContainer} |
ForEach-Object {Test-DirSchema $_.FullName}
with this output:
> Q:\Test\2017\08\18\SO_45750567.ps1
02 : 01_01_dir5 does not comply with schema
new: 02_02_dir5
----
02 : 02_00_dir1 complies with schema
----
02 : 02_01_dir2 complies with schema
----
02 : 12345dir4 does not comply with schema
new: 02_03_dir4
----
02 : dir3 does not comply with schema
new: 02_04_dir3
----
Will have this end situation:
> tree
└───00_00_Master folder
├──00_01_Activities
└──00_02_Schedules
├──02_00_dir1
├──02_01_dir2
├──02_02_dir5
├──02_03_dir4
└──02_04_dir3
The order is, due to different sorting, not exactly yours.
If there is no previous number it will start with 01.
Get-ChildItem -Directory -Recurse | ForEach { #Get All Directories
$a = 0 #Reset Indexing to 01
Get-ChildItem $_.FullName -File | ForEach {
$ParentFolderSecond = ($_.Directory.Parent.Name -split '_')[1] #Second part of folder's parent
$Number = $a++ #Number incrementing by 1
$FolderName = $_.Directory.Name #folder's name
"$($ParentFolderSecond)_$($Number.ToString("00"))_$($FolderName)$($_.Extension)" #put this into a rename-item if it works OK.
}
}
Does this match your specs?
it should work recursively as is but I think i'm missing something in the parent folder part.
it won't rename anything, just spit out a list of what the file names 'will' be.
I would probably do something like this:
Get-ChildItem -Recurse -Directory | Where-Object {
# find schedule directories below the current working directory
$_.Name -match '^\d{2}_(\d{2})_Schedules$'
} | ForEach-Object {
$prefix = $matches[1]
$pattern = "^${prefix}_(\d{2})_"
# enumerate subfolders of schedule directory
$folders = Get-ChildItem $_.DirectoryName -Directory
# find highest existing index of folders that already conform to the
# name schema
$lastindex = $folders |
Where-Object { $_.Name -match $pattern } |
ForEach-Object { [int]$matches[1] } |
Sort-Object |
Select-Object -Last 1
$nextindex = if ($lastindex) { $lastindex + 1 } else { 0 }
# rename folders that don't conform to the name schema starting with the
# index after the highest existing index (or 0 if there was none), remove
# digits and underscores from beginning of folder name
$folders | Where-Object {
$_.Name -notmatch $pattern
} | Rename-Item -NewName {
$_.Name -replace '^\d(?:[_\d]*)', "${prefix}_${script:nextindex}_"
${script:nextindex}++
}
}
I have a folder that contains many huge files. I want to split these files in 3 folders. The requirement is to get the count of files in main folder and then equally split those files in 3 child folders.
Example - Main folder has 100 files. When I run the powershell, 3 child folders should be created with 33, 33 and 34 files respectively.
How can we do this using Powershell?
I've tried the following:
$FileCount = (Get-ChildItem C:\MainFolder).count
Get-ChildItem C:\MainFolder -r | Foreach -Begin {$i = $j = 0} -Process {
if ($i++ % $FileCount -eq 0) {
$dest = "C:\Child$j"
md $dest
$j++
}
Move-Item $_ $dest
}
Here is another solution. This one accounts for the sub folders not existing.
# Number of groups to support
$groupCount = 3
$path = "D:\temp\testing"
$files = Get-ChildItem $path -File
For($fileIndex = 0; $fileIndex -lt $files.Count; $fileIndex++){
$targetIndex = $fileIndex % $groupCount
$targetPath = Join-Path $path $targetIndex
If(!(Test-Path $targetPath -PathType Container)){[void](new-item -Path $path -name $targetIndex -Type Directory)}
$files[$fileIndex] | Move-Item -Destination $targetPath -Force
}
If you need to split up the files into a different number of groups the use $groupCount of higher that 3. Can also work logic with a switch that would change $groupCount to something else if the count was greater that 500 for example.
Loop though the files one by one. Using $fileIndex as a tracker we determine the folder 0,1 or 2 in my case that the file will be put into. Then using that value we check to be sure the target folder exists. Yes, this logic could easily be placed outside the loop but if you have file and folder changes while the script is running you could argue it is more resilient.
Ensure the folder exists, if not make it. Then move that one item. Using the modulus operator, like in the other answers, we don't have to worry about how many files are there. Let PowerShell do the math.
This is super quick and dirty, but it does the job.
#Get the collection of files
$files = get-childitem "c:\MainFolder"
#initialize a counter to 0 or 1 depending on if there is a
#remainder after dividing the number of files by 3.
if($files.count % 3 -eq 0){
$counter = 0
} else {
$counter = 1
}
#Iterate through the files
Foreach($file in $files){
#Determine which subdirectory to put the file in
If($counter -lt $files.count / 3){
$d = "Dir1"
} elseif($counter -ge $files.count / 3 * 2){
$d = "Dir3"
} else {
$d = "Dir2"
}
#Create the subdirectory if it doesn't exist
#You could just create the three subdirectories
#before the loop starts and skip this
if(-Not (test-path c:\Child\$d)){
md c:\Child\$d
}
#Move the file and increment the counter
move-item $file.FullName -Destination c:\Child\$d
$counter ++
}
I think it's possible to do without doing the counting and allocating yourself. This solution:
Lists all the files
Adds a counter property which cycles 0,1,2,0,1,2,0,1,2 to each file
groups them into buckets based on the counter
moves each bucket in one command
There's scope for rewriting it in a lot of ways to make it nicer, but this saves doing the math, handling uneven allocations, iterating over the files and moving them one at a time, would easily adjust to different numbers of groups.
$files = (gci -recurse).FullName
$buckets = $files |% {$_ | Add-Member NoteProperty "B" ($i++ % 3) -PassThru} |group B
$buckets.Name |% {
md "c:\temp$_"
Move-Item $buckets[$_].Group "c:\temp$_"
}