Powershell script to search for similar filenames - powershell

I am trying to build a PowerShell script that can search for files with similar names inside of a folder.
The files will always have a similar name template:
filename(C).TIF
filename(M).TIF
filename(Y).TIF
filename(K).TIF
All I need to do is to extract the "filename", check if there are 4 similar ones (C,M,Y,K) and use it as a variable to move those files.
$files = Get-ChildItem -Path "E:\test" -Filter "*.TIF" |
Foreach-Object {$_.BaseName} | Sort-Object -Unique
$files = $files -replace ".{3}$"
$names = (Get-Unique -InputObject $files)
$names
The result looks like this:
jobname
jobname
jobname
jobname
test
test
test
test
But I need to sort by unique and count them, maybe, before action.

But I need to sort by unique and count them, maybe, before action.
You definitely want the Group-Object cmdlet!
As the name suggests, it... groups objects, based on some common property:
$filesByCommonBaseName = Get-ChildItem -Path "E:\test" -Filter "*.TIF" |Group-Object { $_.BaseName -replace '.{3}$' }
Now that you have them grouped correctly, you can start operating on them as such:
foreach($entry in $filesByCommonBaseName){
Write-Host "Operating on files starting with $($entry.Name)"
if($entry.Count -eq 4){
# we found four matches, move them!
$entry.Group |Move-Item -Destination $destinationDir -Force
}
}

Related

Extract the end of every lines from multiple files

I have several txt files distributed in several sub-folders.
This is what a file looks like:
Data file. version 01.10.
1
8
*
DAT\Trep\Typ10
DAT\Trep\Typ12
DAT\Trep\Typ13
what I would like to do is to extract only the part after the last "\" in order to get something like this:
Typ10 FileName.txt Path
Typ12 FileName.txt Path
Typ13 FileName.txt Path
...
I tried the following
Get-ChildItem -Path 'D:\MyData\*.txt' -Recurse | ForEach-Object {Write-Output $_; $data=Get-Content $_}
$data = $data -replace '.*\\'
$data
it works well for a single file but not with several (-recurse).
Being a powershell beginner I can't figure out how to improve my script.
I also tried to add this to get the result shown above in my post, but that doesn't work either.
Select-Object -Property #{Name = 'Result list'; Expression = { $data }}, Filename, Path
Thanks in advance for your kind help
Use Select-String:
Get-ChildItem -Path D:\MyData\*.txt -Recurse -File |
Select-String '^.+\\(.+)' |
ForEach-Object {
[pscustomobect] #{
Result = $_.Matches.Groups[1].Value
FileName = $_.FileName
Path = $_.Path
}
}
As for your desire to exclude certain folders during recursive traversal:
Unfortunately, Get-ChildItem -Exclude only excludes the matching folders themselves, not also their content. There are two relevant feature requests to potentially overcome this limitation in the future:
GitHub issue #4126 asks for path patterns to be supported too in the future.
GitHub issue #15159 proposes a new subtree-exclusion parameter, such as
-ExcludeRecursive.
For now, a different approach with post-filtering based on Where-Object is required, using folder names Folder1 and Folder2 as examples:
Get-ChildItem -Path D:\MyData\*.txt -Recurse |
Where-Object FullName -NotLike *\Folder1\* |
Where-Object FullName -NotLike *\Folder2\* |
Select-String '^.+\\(.+)' |
ForEach-Object {
[pscustomobect] #{
Result = $_.Matches.Groups[1].Value
FileName = $_.FileName
Path = $_.Path
}
}
For a more flexible, cross-platform approach based on regex matching (which is invariably more complex), see the bottom section of this answer.

PowerShell script speed standalone vs within Visual Code

I have a simple PowerShell script that writes to a file a list of recently added files.
$fpath = "\\DS1821\video\movies"
$file1 = "C:\Users\brian.w.williams\Desktop\RecentMovie.txt"
if (Test-Path $file1) {Remove-Item -Path $file1}
Get-ChildItem -Path "$fpath" -File -Recurse -Include "*.eng.srt" |
ForEach-Object {
$movie = $_.BaseName -replace ".eng",""
if ( ($_.LastWriteTime.Month -ge 7) -and ($_.LastWriteTime.Year -ge 2021) ) {
Write-Host $movie " = " $_.LastWriteTime
Write-Output $movie | Out-file $file1 -append;
}
}
The script works fine. But I noticed that the script runs much faster (a couple of minutes) when run within Visual Code (i.e. "Run without debugging"). When I run the script standalone (i.e. "Run with PowerShell") the script can take hours to complete. Why the difference? Is there anything I can do to speed it up? I have tried mapping the network folder, but that made no difference.
There is improvement to be made to help the code speed up.
First of all, using Write-Host and Write-Output to append to a file inside the ForEach loop is very time consuming.
Then you're using parameter -Include on Get-ChildItem, where you really want to use -Filter. A Filter is MUCH faster than Include, because the latter will only filter the filenames afterwards. -Filter however can only work on one single filename pattern, but this is exactly what you are doing here.
The if(..) can also be improved to have it compare only one value (a datetime) instead of two separate properties from LastWriteTime.
Try:
$sourcePath = '\\DS1821\video\movies'
# set a variable to an output file on your desktop
$outputFile = Join-Path -Path ([Environment]::GetFolderPath("Desktop")) -ChildPath 'RecentMovies.txt'
# set the reference date in a variable for faster comparison later
$refdate = (Get-Date -Year 2021 -Month 7 -Day 1).Date
# get the files, filter on the name ending in '*.eng.srt'
# filter more with Where-Object so you'll get only files newer than or equal to the reference date
# output objects with one calculated property ('MovieName') and the LastWriteTime
$movies = Get-ChildItem -Path $sourcePath -File -Recurse -Filter '*.eng.srt' |
Where-Object { $_.LastWriteTime -ge $refdate } |
Select-Object #{Name = 'MovieName'; Expression = {$_.BaseName -replace '\.eng\.srt$', '.srt'}},
LastWriteTime
# now output what you have collected to file.
# Set-Content creates a new file or overwrites an existing file
$movies.MovieName | Set-Content -Path $outputFile
# and to screen
$movies | ForEach-Object { Write-Host ('{0} = {1}' -f $_.MovieName, $_.LastWriteTime.ToString()) }
# or to Out-GridView if you prefer
$movies | Out-GridView -Title 'My Movies'
1. I've changed some variable names to make the code better readable
2. Since -replace uses regex, you have to escape the dots with a backslash. The regex also anchors the string to the end of the BaseName with the dollar sign ($) at the end

Correction in sub folder names by replacing first two characters, if needed

I am using below Powershell script which successfully traverses through all my case folders within the main folder named Test. What it is incapable of doing is to rename each sub folder, if required, as can be seen in current and desired output. Script should first sort the sub folders based on current numbering and then give them proper serial numbers as folder name prefix by replacing undesired serial numbers.
I have hundreds of such cases and their sub folders which need to be renamed properly.
The below output shows two folders named "352" and "451" (take them as order IDs for now) and each of these folders have some sub-folders with a 2 digit prefix in their names. But as you can notice they are not properly serialized.
$Search = Get-ChildItem -Path "C:\Users\User\Desktop\test" -Filter "??-*" -Recurse -Directory | Select-Object -ExpandProperty FullName
$Search | Set-Content -Path 'C:\Users\User\Desktop\result.txt'
Below is my current output:
C:\Users\User\Desktop\test\Case-352\02-Proceedings
C:\Users\User\Desktop\test\Case-352\09-Corporate
C:\Users\User\Desktop\test\Case-352\18-Notices
C:\Users\User\Desktop\test\Case-451\01-Contract
C:\Users\User\Desktop\test\Case-451\03-Application
C:\Users\User\Desktop\test\Case-451\09-Case Study
C:\Users\User\Desktop\test\Case-451\14-Violations
C:\Users\User\Desktop\test\Case-451\21-Verdict
My desired output is as follows:
C:\Users\User\Desktop\test\Case-352\01-Proceedings
C:\Users\User\Desktop\test\Case-352\02-Corporate
C:\Users\User\Desktop\test\Case-352\03-Notices
C:\Users\User\Desktop\test\Case-451\01-Contract
C:\Users\User\Desktop\test\Case-451\02-Application
C:\Users\User\Desktop\test\Case-451\03-Case Study
C:\Users\User\Desktop\test\Case-451\04-Violations
C:\Users\User\Desktop\test\Case-451\05-Verdict
Thank you so much. If my desired functionality can be extended to this script, it will be of great help.
Syed
You can do the following based on what you have posted:
$CurrentParent = $null
$Search = Get-ChildItem -Path "C:\Users\User\Desktop\test" -Filter '??-*' -Recurse -Directory | Where Name -match '^\d\d-\D' | Foreach-Object {
if ($_.Parent.Name -eq $CurrentParent) {
$Increment++
} else {
$CurrentParent = $_.Parent.Name
$Increment = 1
}
$CurrentNumber = "{0:d2}" -f $Increment
Join-Path $_.Parent.FullName ($_.Name -replace '^\d\d',$CurrentNumber)
}
$Search | Set-Content -Path 'C:\Users\User\Desktop\result.txt'
I added Where to filter more granularly beyond what -Filter allows.
-match and -replace both use regex to perform the matching. \d is a digit. \D is a non-digit. ^ matches the position at the beginning of the string.
The string format operator -f is used to maintain the 2-digit requirement. If you happen to reach 3-digit numbers, then 3 digit numbers will be output instead.
You can take this further to perform a rename operation:
$CurrentParent = $null
Get-ChildItem . -Filter '??-*' -Recurse -Directory | Where Name -match '^\d\d-\D' | Foreach-Object {
if ($_.Parent.Name -eq $CurrentParent) {
$Increment++
} else {
$CurrentParent = $_.Parent.Name
$Increment = 1
}
$CurrentNumber = "{0:d2}" -f $Increment
$NewName = $_.Name -replace '^\d\d',$CurrentNumber
$_ | Where Name -ne $NewName | Rename-Item -NewName $NewName -WhatIf
}
$NewName is used to simply check if the new name already exists. If it does, a rename will not happen for that object. Remove the -WhatIf if you are happy with the results.

Get a list of all specific empty subfolders inside employee's folders

Could you please help me with a directory operation?
I have an employee directory and in that directory, there are about 200+ employees subdirectories named by their employee code. And within each employee's subdirectory, there are about 20 subfolders referring to various documents. For example, subfolder named 'Educational Documents'. This 'Educational Documents' subfolder exists in each of these 200+ employee's folders.
I want to output a text or csv file listing all such 'Educational Documents' subfolders out of those 200+ employees which are empty or in other words where scanned PDF files have not been copied as yet. By doing so, I will be able to use that output file as a task list for myself to populate all those empty folders by putting scanned PDF documents for the missing employee data.
I have tried to use DOS commands with /S switch but that does not precisely cater to my needs and therefore I am looking at some Powershell script which could get this done.
My code so far:
$Search = gci -Filter "Educational Documents" -Recurse -Path "D:\Employees" -Directory
Foreach ($path in $Search.fullname)
{
Write-Output $path | Out-File d:\Filelist.txt -append
$file = gci -path $path | select name
$file.name | Out-File d:\filelist.txt -append
Write-Output "------- Next Folder --------------" | Out-File d:\Filelist.txt -append
}
If I understand correctly, you want a file listing of all empty folders called 'Educational Documents'.
To do that, you could make use of the GetFileSystemInfos() method of the DirectoryInfo objects returned by Get-ChildItem like this:
$Search = Get-ChildItem -Path "D:\Employees" -Filter "Educational Documents" -Recurse -Directory |
Where-Object { $_.GetFileSystemInfos().Count -eq 0 } | Select-Object -ExpandProperty FullName
# add '-PassThru' to also output this list on screen
$Search | Set-Content -Path 'D:\Empty_EducationalDocuments_Folders.txt'
Hope that helps
As per your comment, you would like to list both empty folders and folders that do not have a file with the word Graduation in their name, you can edit the above to become
$Search = Get-ChildItem -Path "D:\Employees" -Filter "Educational Documents" -Recurse -Directory |
Where-Object { $_.GetFileSystemInfos().Count -eq 0 -or
$_.GetFiles("*Graduation*", "TopDirectoryOnly").Count -eq 0 } |
Select-Object -ExpandProperty FullName
# add '-PassThru' to also output this list on screen
$Search | Set-Content -Path 'D:\EducationalDocuments_Folders_without_Graduation_File.txt'
You can try this code:
$Search = Get-ChildItem -Recurse -Path "D:\Employees" -Directory
foreach ($path in $Search.fullname)
{
$directoryInfo = Get-ChildItem -Path $path | Measure-Object
if($directoryInfo.count -eq 0)
{
$path | Out-File "D:\Filelist.txt" -append
Write-Output "------- Next Folder --------------" | Out-File "D:\Filelist.txt" -append
}
}
I used some code from this question: Test if folder is empty

How to get folder name thats different on all user profiles

I like to know how to get 5J91Q4CX.C10 to use in a variable.
C:\Users\user\AppData\Local\Apps\2.0\5J91Q4CX.C10
On all user profiles this folder has a different name.
It is always 8 numbers and digits then a . and then 3 digits or numbers.
I need to use this for a powershell script.
Any idea how I can make a variable for this foldername?
Thanks
I'd do something like this:
#Loop through all user profile folders using something like this:
$userFolders = Get-ChildItem -Path "C:\Users\" -Directory -Force -ErrorAction SilentlyContinue |
Where-Object { #('All Users','Default User', 'Public', 'Default') -notcontains $_.Name } |
Select-Object -ExpandProperty Name
# next loop through these folders to find the foldername that can be different for each user
foreach ($userName in $userFolders) {
$folderName = Get-ChildItem -Path "C:\Users\$userName\AppData\Local\Apps\2.0" -Directory -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '[A-Za-z0-9]{8}\.[A-Za-z0-9]{3}' } |
Select-Object -ExpandProperty Name
# do something with this variable
Write-Host "C:\Users\$userName\AppData\Local\Apps\2.0\$folderName"
}
Some RegEx could do the trick:
$str = "C:\Users\user\AppData\Local\Apps\2.0\5J91Q4CX.C10"
$str -match '.*\\(.*)$'
$matches[1] # 5J91Q4CX.C10
.*\\(.*)$ matches all chars after the last dash \ and before the end of the line $
not sure what you are really trying to do... you could do a directory search through the C:\Users to report back on all subfolders and then a Foreach loop to go through each subfolder and create the file wanted in the destination etc, something like:
$FOLDERS = Get-ChildItem C:\Users -Directory
FOREACH ($FOLDER in $FOLDERS) {
#WHATEVER YOU WANT TO DO
}