I have a script which recursively modifies the "creation date" and the"modified date" of folders to match the oldest and newest files in folders (image files).
After that I want to add the creation date of each folder as a prefix to its name. I have written the code that creates the desired name but I don't know how to rename the folders.
$colFolder = Get-ChildItem -Recurse "." |
Where-Object {$_.mode -match "d"} |
Sort-Object Fullname -Descending
$VerbosePreference = "Continue"
foreach ($strFolder In $colFolder) {
Trap [Exception] {
Write-Verbose "TRAPPED:"
Write-Verbose $_.Exception.Message;
Write-Verbose $strFolder.FullName
Continue
}
$Path = $strFolder.FullName
$Folder = Get-Item $Path
# Get Newest file in folder
$strOldestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -First 1).LastWriteTime
$strNewestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -Last 1).LastWriteTime
# Change the date to match the newest file if it doesn't already
$Folder.CreationTime = $strOldestTime
$Folder.LastWriteTime = $strNewestTime
# add the date as the prefix of the folder (in which the files are)
$date = $strOldestTime.ToString('yyyy-MM-dd')
$foldername = $Folder.Name.split("\")[-1]
Write-Verbose "$date- $foldername"
#now rename the folder to above name
}
Renaming folders can be done as #onupdatecascade described in his answer.
Rename-Item -Path $Folder.FullName -NewName $newname
If you have at least PowerShell v3 (which I highly recommend) you can use the parameter -LiteralPath for the current folder path to avoid issues with special characters:
Rename-Item -LiteralPath $Folder.FullName -NewName $newname
With that said, there are a couple improvements to your code I'd like to suggest.
Get-ChildItem -Recurse ".":
Get-ChildItem uses the current working directory by default, so "." can be omitted.
Where-Object {$_.mode -match "d"}:
The objects you get from Get-ChildItem have a boolean property IsContainer that indicates whether you have a folder or not. No need to use a regex match on the mode. Also, in PowerShell v3 and newer you can omit this step entirely, because the Get-ChildItem cmdlet has an additional parameter -Directory that lists only folders.
Misused hungarian notation (pt.1):
foreach ($strFolder In $colFolder) {
The loop variable $strFolder does not contain strings, but DirectoryInfo objects. This is a prime example for why I consider hungarian notation a pointless waste of space.
Avoid unnecessary steps.
$Path = $strFolder.FullName
$Folder = Get-Item $Path
$strFolder already contains a DirectoryInfo object. The above two statements just convert it to a path string and back to a DirectoryInfo object. Simply make $Folder your loop variable (see above).
Avoid duplicate code (particularly duplicate disk operations):
$strOldestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -First 1).LastWriteTime
$strNewestTime = (Get-ChildItem $Path | Sort-Object LastWriteTime | Select-Object -Last 1).LastWriteTime
Instead of listing and sorting the child items multiple times run the statement just once and collect the result in a variable:
$items = Get-ChildItem $Path | Sort-Object LastWriteTime
$strOldestTime = ($items | Select-Object -First 1).LastWriteTime
$strNewestTime = ($items | Select-Object -Last 1).LastWriteTime
Also, I would simply expand the LastWriteTime property instead of running Select-Object in a subexpression:
$strOldestTime = $items | Select-Object -First 1 -Expand LastWriteTime
$strNewestTime = $items | Select-Object -Last 1 -Expand LastWriteTime
Misused hungarian notation (pt.2):
$strOldestTime and $strNewestTime contain DateTime objects, not strings.
$foldername = $Folder.Name.split("\")[-1]:
The Name property already contains the folder name without path, so there's nothing to split off.
Construct the new folder name from the date and the current name by using the format operator:
$newname = '{0:yyyy-MM-dd} - {1}' -f $oldestTime, $Folder.Name
If there's a possibility that you'll run the script multiple times you may want to remove an existing leading date from the folder name so that the dates won't "stack":
$Folder.Name -replace '^\d{4}-\d{2}-\d{2} - '
Simplified code:
$colFolder = Get-ChildItem -Recurse |
Where-Object { $_.PSIsContainer } |
Sort-Object Fullname -Descending
foreach ($folder in $colFolder) {
$items = Get-ChildItem $folder.FullName | Sort-Object LastWriteTime
$folder.CreationTime = $items | Select-Object -First 1 -Expand LastWriteTime
$folder.LastWriteTime = $items | Select-Object -Last 1 -Expand LastWriteTime
$newname = '{0:yyyy-MM-dd} - {1}' -f $folder.CreationTime, ($folder.Name -replace '^\d{4}-\d{2}-\d{2} - ')
Rename-Item -Path $folder.FullName -NewName $newname
}
or (PowerShell v3 and newer):
$colFolder = Get-ChildItem -Recurse -Directory |
Sort-Object Fullname -Descending
foreach ($folder in $colFolder) {
$items = Get-ChildItem $folder.FullName | Sort-Object LastWriteTime
$folder.CreationTime = $items | Select-Object -First 1 -Expand LastWriteTime
$folder.LastWriteTime = $items | Select-Object -Last 1 -Expand LastWriteTime
$newname = '{0:yyyy-MM-dd} - {1}' -f $folder.CreationTime, ($folder.Name -replace '^\d{4}-\d{2}-\d{2} - ')
Rename-Item -LiteralPath $folder.FullName -NewName $newname
}
Have you tried
Rename-Item oldname newname
It works for simple examples, but could break with folders nested in other folders, and recursion, because you might rename a folder before the script is done with it.
To manage that you can do them in order by length (paths to children being by definition longer than the paths of the parents). Good example here: How do I recursively rename folders with Powershell?
Related
My company recently moved to outlook365. We are entirely VDI based so our user profiles are stored on a single server. As a result our users all now have 2+ .ost files taking up storage space on the server. I'd like to write a script to find and delete the extraneous .ost files. In addition I'd like to schedule the script to run on a monthly basis to clean up any orphaned .ost's that occur for any other reason.
I've tried a few different solutions but can't seem to find the right syntax to identify just the oldest/original .ost in each subdirectory, all attempts have identified the oldest file from the whole directory or all .ost files in the directory.
$Path = "<path>"
$SubFolders = dir $Path -Recurse | Where-Object {$_.PSIsContainer} | ForEach-Object -Process {$_.FullName}
ForEach ($Folder in $SubFolders)
{
$FullFileName = dir $Folder | Where-Object {!$_.PSIsContainer} | Sort-Object {$_.LastWriteTime} -Descending | Select-Object -First 1
}
Inside of your loop, you could use the following to list the .ost file that has the oldest LastWriteTime value. Just add the -Descending flag to Sort-Object to list the newest file.
$FullFileName = foreach ($folder in $Subfolders) {
$Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property LastWriteTime |
Select-Object -Property FullName -First 1
}
$FullFileName
If there is only one .ost file found in the $folder path, it will still find that file. So you will need logic to not delete when there is only one file. This does not guarantee it is the oldest file. You probably want a combination of the oldest CreationTime and newest LastWriteTime. The following will list the oldest .ost file based on CreationTime.
$FullFileName = foreach ($folder in $Subfolders) {
Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property CreationTime |
Select-Object -Property FullName -First 1
}
$FullFileName
Another issue is setting the $FullFileName variable inside of the foreach loop. This means it will be overwritten through each loop iteration. Therefore, if you retrieve the value after the loop completes, it will only have the last value found. Setting the variable to be the result of the foreach loop output will create an array with multiple values.
To only output an OST file path when there are multiple OST files, you can do something like the following:
$FullFileName = foreach ($folder in $Subfolders) {
$files = Get-ChildItem -Path $folder -Recurse -File -Filter "*.ost" |
Sort-Object -Property LastWriteTime -Descending
if ($files.count -ge 2) {
$files | Select-Object -Property FullName -First 1
}
$FullFileName
This one liner should do the job, keeping the ost file with the newest LastWriteTime
gci -Path $Path -directory | where {(gci -Path $_\*.ost).count -gt 1}|%{gci -Path $_\*.cmd|Sort-Object LastWriteTime -Descending|Select-Object -Skip 1|Remove-Item -WhatIf}
Longer variant follows.
$Path = '<path>'
$Ext = '*.ost'
Get-ChildItem -Path $Path -directory -Recurse |
Where-Object {(Get-ChildItem -Path "$_\$Ext" -File -EA 0).Count -gt 1} |
ForEach-Object {
Get-ChildItem -Path "$_\$Ext" -File -EA 0| Sort-Object LastWriteTime -Descending |
Select-Object -Skip 1 | Remove-Item -WhatIf
}
The first two lines evaluate folders with more than one .ost file
The next lines iterates those folders and sort them descending by LastWriteTime, skips the first (newest) and pipes the other to Remove-Item with the -WhatIf parameter to only show what would be deleted while testing.
You can of course also move them to a backup location instead.
I am trying to filter folders based off of the count for the number of files in each folder.
I have been able to list the folder name and the value if there is a value greater than 1. I am trying to exclude the folders that may contain no items.
The amount of items changes daily.
$Date2 = Get-Date -Format "yyyy-MM-dd"
$Date2Str = '{0:yyyy-MM-dd}' -f $Date2
$startFolder = "U:\test"
#Returns the Count of files in each queue
$colItems = (Get-ChildItem $startFolder -recurse | Where-Object
{$_.PSIsContainer -eq $True} | Sort-Object)
if($colItems -ine $null){
foreach ($i in $colItems)
{
$subFolderItems = (Get-ChildItem $i.FullName | Where-Object
($_.CreationTime -lt $Date2Str -and $_.Name -like "*.tif"))
$i.Name + " -- " -f ($subFolderItems.Count) |Format-Table
#{Expression={$colItems -ge 1}}
I expect the output of $colItems to be the subFolder name, and the count, excluding any subFolder with a Count less than 1 or equals to 0.
The actual return is the list of subfolder Names and counts with all subFolders including those with a count equal to 0.
If I've got your explanation right you're looking for something like this:
$startFolder = 'U:\test'
Get-ChildItem -Path $startFolder -Directory |
Select-Object -Property Name, #{Name = 'FileCount'; Expression = { (Get-ChildItem -Path $_.FullName -File).count}}
That lists all subfolders of your $startFolder together with their file count.
BTW: This code expects at least Powershell version 3.
... and of course you can pipe this now to a Where-Object and output only the folders with more than one file in it ...
Get-ChildItem -Path $startFolder -Directory |
Select-Object -Property Name, #{Name = 'FileCount'; Expression = { (Get-ChildItem -Path $_.FullName -File).count } } |
Where-Object -Property FileCount -GT -Value 1
I am trying to use powershell to produce a list of folder names and how many files are in each folder.
I have this script
$dir = "C:\Users\folder"
Get-ChildItem $dir -Recurse -Directory | ForEach-Object{
[pscustomobject]#{
Folder = $_.FullName
Count = #(Get-ChildItem -Path $_.Fullname -File).Count
}
} | Select-Object Folder,Count
Which lists the file count, but it puts the full path (i.e. C:\Users\name\Desktop\1\2\-movi...). Is there any way to just display the last folder ("movies") as well as save the result to a .txt file?
Thank you
Instead of $_.FullName, use $_.Name to only get the directory name.
Your Select-Object call is redundant - it is effectively a no-op.
While it's easy to send the results to a .txt file with >, for instance, it's better to use a more structured format for later programmatic processing.
In the simplest form, that means outputting to a CSV file via Export-Csv; generally, however, the most faithful way of serializing objects to a file is to use Export-CliXml.
Using Export-Csv for serialization:
$dir = 'C:\Users\folder'
Get-ChildItem -LiteralPath $dir -Recurse -Directory | ForEach-Object {
[pscustomobject] #{
Folder = $_.Name
Count = #(Get-ChildItem -LiteralPath $_.Fullname -File).Count
}
} | Export-Csv -NoTypeInformation results.csv
Note that you could streamline your command by replacing the ForEach-Object call with a Select-Object call that uses a calculated property:
$dir = 'C:\Users\folder'
Get-ChildItem -LiteralPath $dir -Recurse -Directory |
Select-Object Name,
#{ n='Count'; e={#(Get-ChildItem -LiteralPath $_.Fullname -File).Count} } |
Export-Csv -NoTypeInformation results.csv
You mean something like this...
Clear-Host
Get-ChildItem -Path 'd:\temp' -Recurse -Directory |
Select-Object Name,FullName,
#{Name='FileCount';Expression = {(Get-ChildItem -Path $_.FullName -File -Recurse| Measure-Object).Count}} `
| Format-Table -AutoSize
# Results
Name FullName FileCount
---- -------- ---------
abcpath0 D:\temp\abcpath0 5
abcpath1 D:\temp\abcpath1 5
abcpath2 D:\temp\abcpath2 5
Duplicates D:\temp\Duplicates 12677
EmptyFolder D:\temp\EmptyFolder 0
NewFiles D:\temp\NewFiles 4
PngFiles D:\temp\PngFiles 4
results D:\temp\results 905
...
We use software called Revit, files are saved as such: filename.rvt
Each time a user edits a file, Revit takes it upon itself to save the old file in the format filename.xxxx.rvt (where xxx is a number).
Over time when files are edited hundreds of times, we have many unnecessary files on the file server.
I am writing a script to:
Locate and folders containing Revit backup files
Delete all but the most recently modified 5 revit backup files
I have tried two approaches below
$searchpath = "e:\"
# Find a unique list of directories that contains a revit backup file (*.*.rvt)
$a = Get-ChildItem -Path $searchpath -Include *.*.rvt -Recurse | Select-object Directory -expandproperty FullName | Get-Unique -AsString
# For each folder that contains a single revit backup file (*.*.rvt)...
# - Sort by modified time
# - Select all except first 5
$a | Get-ChildItem -Include *.*.rvt | Sort-Object LastWriteTime -descending | select-object -skip 5 -property Directory,Name,CreationTime,LastWriteTime | Out-GridView -Title "Old Backups" -PassThru
The issue with this approach is that it only "skips" the first 5 files in the entire search result, not 5 in each folder.
Then I went about it using a loop, and this gets nowhere:
$searchpath = "e:\"
# Find a unique list of directories that contains a revit backup file (*.*.rvt)
$a = Get-ChildItem -Path $searchpath -Include *.*.rvt -Recurse | Select Directory | Get-Unique -AsString
# For each folder that contains a single revit backup file (*.*.rvt)...
# - Sort by modified time
# - Select all except first 5
$a | foreach {
$b += Get-ChildItem -Path $_.Directory.FullName -Include *.*.rvt | Sort-Object LastWriteTime -descending | select-object -skip 5 -property Directory,Name,CreationTime,LastWriteTime
}
$b | Out-GridView -Title "Old Backups" -PassThru
Any thoughts on the correct approach and whats going wrong?
try this:
get-childitem -file -recurse | group Directory | where Count -gt 5 | %{
$_.Group | Sort LastWriteTime -descending | select -skip 5 Directory,Name,CreationTime,LastWriteTime
} | Out-GridView -Title "Old Backups"
If you want delete you can do it (remove what if)
gci -file -recurse | group Directory | where Count -gt 5 | %{
$_.Group | Sort LastWriteTime -descending | select -skip 5 | remove-item -WhatIf
}
The key to do what you seek is to use the Group-Object cmdlet.
In your case, the group you want to create is a group containing all items in the same folder. This will give you something like this:
From there, you can perform actions on each group, such as selecting all the files while skipping the first 5 of each folders and deleting the remaining.
See this simple minimalist example:
$Path = 'C:\__TMP\1'
$Items = Get-ChildItem -Path "$path\*.rvt" -Recurse | Group-Object -Property PsparentPath
Foreach ($ItemsGroup in $Items) {
$SortedFiles = $ItemsGroup.Group | sort LastWriteTime -Descending
$SortedFiles | Select-Object -Skip 5 | % {Write-host "Deleting $($_.FullName)"; Remove-Item $_.FullName}
}
Try something like this:
$searchpath = "E:\"
$number = 5
$directories = Get-ChildItem -Path $searchpath -Include *.*.rvt -Recurse | Where-Object {$_.PsIsContainer}
foreach ($dir in $directories)
{
$files = Get-ChildItem -Path $dir.FullName | Where-Object {-not $_.PsIsContainer}
if ($files.Count -gt $number)
{
$files | Sort-Object CreationTime | Select-Object -First ($files.Count - $number) | Remove-Item -Force
}
}
Change the placeholders accordingly. I just gave you the logical approach.
An alternative solution that doesn't require grouping first and instead processes each directory separately:
& { Get-Item $path; Get-ChildItem -Directory -Recurse $path } | # get all dirs.
ForEach-Object { # for each dir.
Get-ChildItem -File $_.FullName/*.*.rvt | # get backup files in dir.
Sort-Object -Descending LastWriteTime | # sort by last-write time, newest first
Select-Object -Skip 5 | # skip the 5 newest
Remove-Item -Force -WhatIf # delete
}
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
I'm having ongoing trouble with a script I've written that is (supposed) to do the following.
I have one folder with a number of csv files, and I want to copy the latest file with the company name into another folder, and rename it.
It is in the current format:
21Feb17070051_CompanyName_Sent21022017
I want it in the following format:
CompanyName21022017
So I have the following powershell script to do this:
## Declare variables ##
$DateStamp = get-date -uformat "%Y%m%d"
$csv_dest = "C:\Dest"
$csv_path = "C:\Location"
## Copy latest Company CSV file ##
get-childitem -path $csv_path -Filter "*Company*.csv" |
where-object { -not $_.PSIsContainer } |
sort-object -Property $_.CreationTime |
select-object -last 1 |
copy-item -Destination $csv_dest
## Rename the file that has been moved ##
get-childitem -path $csv_dest -Filter "*Company*.csv" |
where-object { -not $_.PSIsContainer } |
sort-object -Property $_.CreationTime |
select-object -last 1 | rename-item $file -NewName {"Company" + $DateStamp + ".csv"}
The file seems to copy ok, but the rename fails -
Rename-Item : Cannot bind argument to parameter 'Path' because it is null.
At C:\Powershell Scripts\MoveCompanyFiles.ps1:20 char:41
+ select-object -last 1 | rename-item $file -NewName {"CompanyName" + $DateSt ...
I think it is something to do with the order in which powershell works, or the fact it can't see the .csv in the $file variable. There are other files (text files, batch files) in the destination, in case that affects things.
Any help in where I'm going wrong would be appreciated.
As wOxxOm answered, you need to remove $file from Rename-Item as it is not defined and the cmdlet already receives the inputobject through the pipeline.
I would also suggest that you combine the two operations by passing through the fileinfo-object for the copied file to Rename-Item. Ex:
## Declare variables ##
$DateStamp = get-date -uformat "%Y%m%d"
$csv_dest = "C:\Dest"
$csv_path = "C:\Location"
## Copy and rename latest Company CSV file ##
Get-ChildItem -Path $csv_path -Filter "*Company*.csv" |
Where-Object { -not $_.PSIsContainer } |
Sort-Object -Property CreationTime |
Select-Object -Last 1 |
Copy-Item -Destination $csv_dest -PassThru |
Rename-Item -NewName {"Company" + $DateStamp + ".csv"}
You can rename and copy in a single command. Just use Copy-Item Command and give new path and name as -Destination parameter value. It will copy and rename the file. You can find an example below.
$source_path = "c:\devops\test"
$destination_path = "c:\devops\test\"
$file_name_pattern = "*.nupkg"
get-childitem -path $source_path -Filter $file_name_pattern |
Copy-Item -Destination { $destination_path + $_.Name.Split("-")[0] + ".nupkg"}