How do I recursively rename folders with Powershell? - powershell

Recursive renaming files using PS is trivial (variation on example from Mike Ormond's blog):
dir *_t*.gif -recurse
| foreach { move-item -literal $_ $_.Name.Replace("_thumb[1]", "")}
I'm trying to recursively rename a folder structure.
The use case is I'd like to be able to rename a whole VS.NET Solution (e.g. from Foo.Bar to Bar.Foo). To do this there are several steps:
Rename folders (e.g. \Foo.Bar\Foo.Bar.Model => \Bar.Foo\Bar.Foo.Model)
Rename files (e.g. Foo.Bar.Model.csproj => Bar.Foo.Model.csproj)
Find and Replace within files to correct for namespace changes (e.g. 'namespace Foo.Bar' => 'namespace Bar.Foo')
I'm currently working the first step in this process.
I found this posting, which talks about the challenges, and claims a solution but doesn't talk about what that solution is.
I keep running into the recursion wall. If I let PS deal with the recursion using a flag, the parent folder gets renamed before the children, and the script throws an error. If I try to implement the recursion myself, my head get's all achy and things go horribly wrong - for the life of me I cannot get things to start their renames at the tail of the recursion tree.

Here's the solution rbellamy ended up with:
Get-ChildItem $Path -Recurse | %{$_.FullName} |
Sort-Object -Property Length -Descending |
% {
Write-Host $_
$Item = Get-Item $_
$PathRoot = $Item.FullName | Split-Path
$OldName = $Item.FullName | Split-Path -Leaf
$NewName = $OldName -replace $OldText, $NewText
$NewPath = $PathRoot | Join-Path -ChildPath $NewName
if (!$Item.PSIsContainer -and $Extension -contains $Item.Extension) {
(Get-Content $Item) | % {
#Write-Host $_
$_ -replace $OldText, $NewText
} | Set-Content $Item
}
if ($OldName.Contains($OldText)) {
Rename-Item -Path $Item.FullName -NewName $NewPath
}
}

How about this - do a recursive list of the full names, sort it in descending order by the length of the full name, and then run that back through your rename routine.
e.g.
gci <directory> -recurse |
foreach {$_.fullname} |
sort -length -desc

Maybe something in this is useful, here's a snippet that recurses and prepends "pre" to a directory structure
$dirs = Get-ChildItem c:/foldertorecurse -rec | Where-Object {$_.PSIsContainer -eq 1} | sort fullname -descending
foreach ( $dir in $dirs ) { rename-item -path $dir.fullname -newname ("pre" + $dir.name) }

Related

Move-Item doesn't move file

I've tried to rename a csv with powershell and then move it automatically to another folder, when there's no file.
Originally, the csv-name looks like this: import_9999_2020-08-13_132238.csv but the part with 9999 can also include just 2 or 3 digits.
My actual Code looks like:
#Import of path and target-path
$path = "\\network-path\subfolder\subfolder\subfolder\subfolder\subfolder1\"
$target_path = "\\network-path\subfolder\subfolder\subfolder\subfolder\subfolder2\"
#endless loop
$a=$true
while($a -eq $true){
$Files = gci $path
$TargetFiles = gci $target_path
#wait 5 minutes if path is empty
if(($Files).Count -eq 0){
sleep -Seconds 300
}
#if path is filled with one or more files
else {
#if file in target-path is processed (from another program)
if(($TargetFiles).count -eq 0){
#rename and move the latest file
get-childitem -path $path -Filter "import_*.csv"|
where-object { -not $_.PSIsContainer } |
sort-object -Property $_.CreationTime |
select-object -last 1 |
Rename-Item -NewName {($_.Name.Substring(0,($_.Name.Length)-22))+".csv"} |
Move-Item -Destination $target_path +"$($_.Name).csv"
}
sleep -Seconds 20
}
}
It works partly and renames the csv, but it doesn't move it to the target-path. The path is correct, i've copied it from the Windows-Explorer.
Any Ideas, why the program doesn't work completely? Thanks
For better readability, I would split the code where you find the original file, rename it and move it to the target path into several lines.
Also, Move-Item can rename the file aswell, so no need to do a Rename-Item first:
$file = Get-ChildItem -Path $path -Filter "import_*.csv" -File |
Sort-Object -Property $_.LastWriteTime |
Select-Object -Last 1
# create the new name for the file.
# or use regex: $newName = '{0}{1}' -f ($file.BaseName -replace '(_\d{4}-\d{2}-\d{2}_\d+)$'), $file.Extension
$newName = '{0}{1}' -f (($file.BaseName -split '_')[0..1] -join '_'), $file.Extension
# now move the file and rename at the same time
$file | Move-Item -Destination (Join-Path -path $target_path -ChildPath $newName)
I changed CreationTime to LastWriteTime to really get the latest file.
I also added the -File switch to the Get-ChildItem cmdlet, because only for old PowerShell versions you need to use where-object { -not $_.PSIsContainer }
If you want to pass it down the pipeline add -Passthru to the Rename-Item cmdlet
Somefile.txt | Rename-Item -NewName {$_.basename + "abc" + $_.ext} | # nothing in the pipeline
Somefile.txt | Rename-Item -NewName {$_.basename + "abc" + $_.ext} -Passthru | # now the new named fileinfo object is in the pipeline, contained in automatic variable $_

Move Files Containing X in their Names to Folder X

I'm Trying to move files containing "Sxx" in their names to folder named "Sxx".
for example:
file1: S01E12.srt ----> /S01/S01E12.srt
file2: S03E14.jpg ----> /S03/S03E14.jpg
etc.
So i came up with these codes for creating folders using files having "Sxx" in their names and then moving them into the right folders.
For creating folders:
foreach ($name in (Get-ChildItem -File | % {$_.BaseName -replace 'E\d{2}',''}))
{
if ($name -like 'S*') {
New-Item -path "$name" -ItemType Directory
}
}
For moving files:
get-childitem -File | where {$_ -like "S01*"} | move-Item -Destination "S01*"
get-childitem -File | where {$_ -like "S02*"} | move-Item -Destination "S02*"
...
etc.
Any idea how to replace hardcoding method for moving part?
If you have any advice or better code for any part that would be awesome too.
Below is my code. I created two foreach loops to go once through all files and afterwards through all folders. I used a substring to get part of the file names.
$files = Get-ChildItem -File -Path C:\users\Lenovo\Desktop\Test
$dir = Get-ChildItem -Path C:\users\lenovo\Desktop\Test -Directory
foreach ($item in $files) {
foreach ($folder in $dir)
{
if (($item.Name).Substring(0,3) -like $folder.Name)
{
Move-Item -Path $item.FullName -Destination $folder.FullName
}
}
}
I create four files with S01.txt to S04.txt and it was working. To test whether your variable is empty, either you can write the variable with Write-Host or slightly more advanced, you can use the debug mode to see exactly what your variables contain.

Get contents of subfolders containing a string in their name

I want to get all files in subfolders, of the same root folder, that all contain the same string ("foo") in the name of the subfolder(s). Below gives me no error, and no output. I don't know what I'm missing.
Get-ChildItem $rootfolder | where {$_.Attributes -eq 'Directory' -and $_.BaseName -contains 'foo'}) | echo $file
Ultimately, I would like to not just echo their names, but move each file to a target folder.
Thank you.
Here is a solution that includes moving the child files of each folder to a new target folder:
$RootFolder = '.'
$TargetFolder = '.\Test'
Get-ChildItem $RootFolder | Where-Object {$_.PSIsContainer -and $_.BaseName -match 'foo'} |
ForEach-Object { Get-ChildItem $_.FullName |
ForEach-Object { Move-Item $_.FullName $TargetFolder -WhatIf } }
Remove -WhatIf when you are happy it's doing what it should be.
You might need to modify the Get-ChildItem $_.FullName part if you (for example) want to exclude sub-directories of the folders, or if you want to include child items in all subfolders of those paths, but not the folders themselves.
replace
Get-ChildItem $rootfolder | where {$_.Attributes -match 'Directory' -and $_.basename -Match 'foo'}) | echo $file
with
Get-ChildItem $rootfolder | where {($_.Attributes -eq 'Directory') -and ($_.basename -like '*foo*')} | Move-Item $targetPath
your request:
that all contain the same string ("foo")
you have to use the -like comparison operator. Also for exact match I would use -eq (case sensitive version is -ceq) instead of -match since its used for matching substrings and patterns.
workflow:
Gets all the files in directory, sending it through pipe to Where-Object cmdlet where you are filtering based on properties Attributes and Basename. When the filtering is done, its being sent to cmdlet Move-Item.
Adapt the first two vars to your environment.
$rootfolder = 'C:\Test'
$target = 'X:\path\to\whereever'
Get-ChildItem $rootfolder -Filter '*foo*' |
Where {$_.PSiscontainer} |
ForEach-Object {
"Processing folder: {0} " -f $_
Move $_\* -Destination $target
}

Rename if file already exist in powershell

I created ps script to copy only files in the folder structure- recursive
cp $source.Text -Recurse -Container:$false -destination $destination.Text
$dirs = gci $destination.Text -directory -recurse | Where { (gci $_.fullName).count -eq 0 } | select -expandproperty FullName
$dirs | Foreach-Object { Remove-Item $_ }
it is working fine. but the problem i have files in the same names. it is not copying duplicated files. i need to rename if file already exist
source:
folderA--> xxx.txt,yyy.txt,
folderB-->xxx.txt,yyy.txt,zzz.txt,
folderc-->xxx.txt
destination (requirement)
xxx.txt
xxx1.txt
xxx2.txt
yyy.txt
yyy1.txt
zzz.txt
Here a solution where I use the Group-Object cmdlet to group all items by the filename. I then iterate over each group and if the group contains more then one file, I append _$ito it where $i starts by one and gets incremented:
$source = $source.Text
$destination = $destination.Text
Get-ChildItem $source -File -Recurse | Group-Object Name | ForEach-Object {
if ($_.Count -gt 1) { # rename duplicated files
$_.Group | ForEach-Object -Begin {$i = 1} -Process {
$newFileName = $_.Name -replace '(.*)\.(.*)', "`$1_$i.`$2"
$i++
Copy-Item -Path $_.FullName -Destination (Join-Path $destination $newFileName)
}
}
else # the filename is unique, just copy it.
{
$_.Group | Copy-Item -Destination $destination
}
}
Note:
You may change the -File to -Container:$false if your PowerShell version doesn't support it. Also note that the script doesn't look into the destination folder whether a file with the same name already exist.

Recursively count files in subfolders

I am trying to count the files in all subfolders in a directory and display them in a list.
For instance the following dirtree:
TEST
/VOL01
file.txt
file.pic
/VOL02
/VOL0201
file.nu
/VOL020101
file.jpg
file.erp
file.gif
/VOL03
/VOL0301
file.org
Should give as output:
PS> DirX C:\TEST
Directory Count
----------------------------
VOL01 2
VOL02 0
VOL02/VOL0201 1
VOL02/VOL0201/VOL020101 3
VOL03 0
VOL03/VOL0301 1
I started with the following:
Function DirX($directory)
{
foreach ($file in Get-ChildItem $directory -Recurse)
{
Write-Host $file
}
}
Now I have a question: why is my Function not recursing?
Something like this should work:
dir -recurse | ?{ $_.PSIsContainer } | %{ Write-Host $_.FullName (dir $_.FullName | Measure-Object).Count }
dir -recurse lists all files under current directory and pipes (|) the result to
?{ $_.PSIsContainer } which filters directories only then pipes again the resulting list to
%{ Write-Host $_.FullName (dir $_.FullName | Measure-Object).Count } which is a foreach loop that, for each member of the list ($_) displays the full name and the result of the following expression
(dir $_.FullName | Measure-Object).Count which provides a list of files under the $_.FullName path and counts members through Measure-Object
?{ ... } is an alias for Where-Object
%{ ... } is an alias for foreach
Similar to David's solution this will work in Powershell v3.0 and does not uses aliases in case someone is not familiar with them
Get-ChildItem -Directory | ForEach-Object { Write-Host $_.FullName $(Get-ChildItem $_ | Measure-Object).Count}
Answer Supplement
Based on a comment about keeping with your function and loop structure i provide the following. Note: I do not condone this solution as it is ugly and the built in cmdlets handle this very well. However I like to help so here is an update of your script.
Function DirX($directory)
{
$output = #{}
foreach ($singleDirectory in (Get-ChildItem $directory -Recurse -Directory))
{
$count = 0
foreach($singleFile in Get-ChildItem $singleDirectory.FullName)
{
$count++
}
$output.Add($singleDirectory.FullName,$count)
}
$output | Out-String
}
For each $singleDirectory count all files using $count ( which gets reset before the next sub loop ) and output each finding to a hash table. At the end output the hashtable as a string. In your question you looked like you wanted an object output instead of straight text.
Well, the way you are doing it the entire Get-ChildItem cmdlet needs to complete before the foreach loop can begin iterating. Are you sure you're waiting long enough? If you run that against very large directories (like C:) it is going to take a pretty long time.
Edit: saw you asked earlier for a way to make your function do what you are asking, here you go.
Function DirX($directory)
{
foreach ($file in Get-ChildItem $directory -Recurse -Directory )
{
[pscustomobject] #{
'Directory' = $File.FullName
'Count' = (GCI $File.FullName -Recurse).Count
}
}
}
DirX D:\
The foreach loop only get's directories since that is all we care about, then inside of the loop a custom object is created for each iteration with the full path of the folder and the count of the items inside of the folder.
Also, please note that this will only work in PowerShell 3.0 or newer, since the -directory parameter did not exist in 2.0
Get-ChildItem $rootFolder `
-Recurse -Directory |
Select-Object `
FullName, `
#{Name="FileCount";Expression={(Get-ChildItem $_ -File |
Measure-Object).Count }}
My version - slightly cleaner and dumps content to a file
Original - Recursively count files in subfolders
Second Component - Count items in a folder with PowerShell
$FOLDER_ROOT = "F:\"
$OUTPUT_LOCATION = "F:DLS\OUT.txt"
Function DirX($directory)
{
Remove-Item $OUTPUT_LOCATION
foreach ($singleDirectory in (Get-ChildItem $directory -Recurse -Directory))
{
$count = Get-ChildItem $singleDirectory.FullName -File | Measure-Object | %{$_.Count}
$summary = $singleDirectory.FullName+" "+$count+" "+$singleDirectory.LastAccessTime
Add-Content $OUTPUT_LOCATION $summary
}
}
DirX($FOLDER_ROOT)
I modified David Brabant's solution just a bit so I could evaluate the result:
$FileCounter=gci "$BaseDir" -recurse | ?{ $_.PSIsContainer } | %{ (gci "$($_.FullName)" | Measure-Object).Count }
Write-Host "File Count=$FileCounter"
If($FileCounter -gt 0) {
... take some action...
}