Powershell - Select-String Advice needed - powershell

My goal is to create a PS script that does the following:
a) scans a source directory and produces a list of files that have a matching pattern that I pass in found inside the files
b) take the list of those files and Copy-Item to move and then archive it.
I have this process working for when I "-Filter" on a filename, but can't seem to get my script to work when using "Select-String -pattern". When it gets to the "$FileNames = #($Files | %{$_.Path.Substring($Source.Length)})" part of the code, it says file does not exist as its passing in the #{Path} code?
View of error
If ({$PatternIdentifier -ne "" -and $FileIdentifier -eq "" -and $FileExtension -ne ""})
{
$Files = get-childitem $Source -Filter $FileExtension | Select-String -pattern $PatternIdentifier -SimpleMatch |Select Path
}
$FileNames = #($Files | %{$_.Path.Substring($Source.Length)})
if($Files.Count -ne 0)
{
if ((test-path $ArchiveDestination) -eq 0)
{
New-Item -ItemType Directory -Force -Path $ArchiveDestination
}
foreach ($File in $Files)
{
Copy-Item $File -Destination $DestinationFolder
Copy-Item $File -Destination $ArchiveDestination
$count++
}
if($error.length -lt 0)
{
Write-Host ("Copied {0} files!!" -f $count)
$answer = $FilesTotalCount -$count
}
}

Get the value from the property:
|Select -expand path

Related

Find similarly-named files, and if present, remove the files without a specific string using PowerShell

In a directory, there are files with the following filenames:
ExampleFile.mp3
ExampleFile_pn.mp3
ExampleFile2.mp3
ExampleFile2_pn.mp3
ExampleFile3.mp3
I want to iterate through the directory, and IF there is a filename that contains the string '_pn.mp3', I want to test if there is a similarly named file without the '_pn.mp3' in the same directory. If that file exists, I want to remove it.
In the above example, I'd want to remove:
ExampleFile.mp3
ExampleFile2.mp3
and I'd want to keep ExampleFile3.mp3
Here's what I have so far:
$pattern = "_pn.mp3"
$files = Get-ChildItem -Path '$path' | Where-Object {! $_.PSIsContainer}
Foreach ($file in $files) {
If($file.Name -match $pattern){
# filename with _pn.mp3 exists
Write-Host $file.Name
# search in the current directory for the same filename without _pn
<# If(Test-Path $currentdir $filename without _pn.mp3) {
Remove-Item -Force}
#>
}
enter code here
You could use Group-Object to group all files by their BaseName (with the pattern removed), and then loop over the groups where there are more than one file. The result of grouping the files and filtering by count would look like this:
$files | Group-Object { $_.BaseName.Replace($pattern,'') } |
Where-Object Count -GT 1
Count Name Group
----- ---- -----
2 ExampleFile {ExampleFile.mp3, ExampleFile_pn.mp3}
2 ExampleFile2 {ExampleFile2.mp3, ExampleFile2_pn.mp3}
Then if we loop over these groups we can search for the files that do not end with the $pattern:
#'
ExampleFile.mp3
ExampleFile_pn.mp3
ExampleFile2.mp3
ExampleFile2_pn.mp3
ExampleFile3.mp3
'# -split '\r?\n' -as [System.IO.FileInfo[]] | Set-Variable files
$pattern = "_pn"
$files | Group-Object { $_.BaseName.Replace($pattern,'') } |
Where-Object Count -GT 1 | ForEach-Object {
$_.Group.Where({-not $_.BaseName.Endswith($pattern)})
}
This is how your code would look like, remove the -WhatIf switch if you consider the code is doing what you wanted.
$pattern = "_pn.mp3"
$files = Get-ChildItem -Path -Filter *.mp3 -File
$files | Group-Object { $_.BaseName.Replace($pattern,'') } |
Where-Object Count -GT 1 | ForEach-Object {
$toRemove = $_.Group.Where({-not $_.BaseName.Endswith($pattern)})
Remove-Item $toRemove -WhatIf
}
I think you can get by here by adding file names into a hash map as you go. If you encounter a file with the ending you are interested in, check if a similar file name was added. If so, remove both the file and the similar match.
$ending = "_pn.mp3"
$files = Get-ChildItem -Path $path -File | Where-Object { ! $_.PSIsContainer }
$hash = #{}
Foreach ($file in $files) {
# Check if file has an ending we are interested in
If ($file.Name.EndsWith($ending)) {
$similar = $file.Name.Split($ending)[0] + ".mp3"
# Check if we have seen the similar file in the hashmap
If ($hash.Contains($similar)) {
Write-Host $file.Name
Write-Host $similar
Remove-Item -Force $file
Remove-Item -Force $hash[$similar]
# Remove similar from hashmap as it is removed and no longer of interest
$hash.Remove($similar)
}
}
else {
# Add entry for file name and reference to the file
$hash.Add($file.Name, $file)
}
}
Just get a list of the files with the _pn then process against the rest.
$pattern = "*_pn.mp3"
$files = Get-ChildItem -Path "$path" -File -filter "$pattern"
Foreach ($file in $files) {
$TestFN = $file.name -replace("_pn","")
If (Test-Path -Path $(Join-Path -Path $Path -ChildPath $TestFN)) {
$file | Remove-Item -force
}
} #End Foreach

Post-order Traverse in PowerShell recurse

Get-ChildItem -Recurse in powershell currently traverse a directory in level order fashion. Is there any way to traverse a directory in post-order way in Powershell?
I am trying to delete files which are older than certain times. and after deleting files, if subfolder is empty, delete that folder too. Right now am doing this.
$path = 'D:\Files'
Get-ChildItem -Path $path -Recurse | Where-Object {
(($_.LastWriteTime -lt (Get-Date).AddDays(-30)) -and ($_ -is [system.io.fileinfo]) )
} | Remove-Item
Get-ChildItem -Path $path -Recurse | Where-Object {
($_ -is [System.IO.DirectoryInfo]) -and $_.CreationTime -lt (Get-Date).AddDays(-30) -and ((Get-ChildItem $_.FullName).Count -eq 0)
} | Remove-Item -Force
But I want to do it in a single command. Not as two different command.
You could reverse the order of the items returned by Get-ChildItem with [Array]::Reverse
Full script:
$items = Get-ChildItem 'D:\Files' -Recurse
[Array]::Reverse($items)
$date = (Get-Date).AddDays(-30)
foreach ($item in $items) {
if ($item.PSIsContainer) {
if ($item.CreationTime -lt $date -and (Get-ChildItem $item.FullName).Count -eq 0) {
Remove-Item $item.FullName
}
}
elseif ($item.LastWriteTime -lt $date) {
Remove-Item $item.FullName
}
}
I couldn't get the post order working properly with GCI, someone claimed it should, but it wasn't traversing depth first. Below is a naive implementation of the classic post order algorithm using the push directory and pop directory commands. Put your "actions" where Write-Host is.
function PostOrder($d){
pushd $d
$folders = Get-ChildItem .\ -Directory -Force
foreach ($folder in $folders){
PostOrder($folder)
}
popd
Write-Host $d.FullName
}
PostOrder("C:\myFolder")

How to use Test-Path including a regex

I would like to check if the file Test.txt exists in a specific directory (folder name with 16-digits).
I tried for example following command:
Test-Path "C:\Users\<USERNAME>\Desktop\Test\([0-9]{16})\Test.txt"
Following command cannot be used in my situation.
Test-Path "C:\Users\<USERNAME>\Desktop\Test\*\Test.txt"
Thank you!
This would be one way of doing that:
# This is for PowerShell version 3.0 and up. If you are using an older version, use
# Get-ChildItem -Path "C:\Users\<USERNAME>\Desktop\Test" | Where-Object { $_.PSIsContainer -and $_.Name -match '\d{16}' }
Get-ChildItem -Path "C:\Users\<USERNAME>\Desktop\Test" -Directory | Where-Object { $_.Name -match '\d{16}' } |
ForEach-Object {
$fileName = Join-Path -Path $_.FullName -ChildPath 'Test.txt'
if (Test-Path -Path $fileName -PathType Leaf) {
Write-Host "$fileName exists"
}
else {
Write-Host "$fileName does not exist"
}
}
Test-Path will return $True/$False, to achieve something similar you can do:
((Get-ChildItem "C:\Users\<USERNAME>\Desktop\Test\*\Test.txt" |
where FullName -match "\\Test\\\d{16}\\Test.txt$").Count -gt 0)
But that will not reveal which folders matched.

Moves files into new subfolder for each year/month

I am trying to move all files under each subfolder into another folder with subfoldername_mmm_yyyy name.
Below codes only move all files in all sub folders into one folder with the name subfoldername_subfoldername_mmm_yyyy.
I know my for each loops are incorrect but I don't know how to fix them, would someone please help?
$curr_date = Get-Date
$folder_path = "C:\Logs\"
$file_type = "C:\Logs\*.log*"
$destination = "C:\Archive\"
# delete tmp files if existed
$deletefile = Get-ChildItem $destination -recurse -include *.7z.tmp -force | remove-item
# set min age of files
$max_days = "-1"
# determine how far back we go based on current date
$zip_date = $curr_date.AddDays($max_days)
Get-ChildItem -Path $folder_path | Where-Object { $_.Attributes -eq "Directory" } | foreach {
# obtain all subfolders
$servername = Get-ChildItem -Path $folder_path | Where-Object { $_.Attributes -eq "Directory" }
# move all files into servername_mmm_yyyy folder
Get-ChildItem $file_type -Recurse | Where-Object { ($_.LastWriteTime -lt $zip_date) -and ($_.psIsContainer -eq $false)}| foreach {
$x = $_.LastWriteTime.ToShortDateString()
$month_year = Get-Date $x -Format MMM_yyyy
$file_destination = ($destination) + ($servername) + "_" + ($month_year)
if (test-path $file_destination) {
move-item $_.fullname $file_destination
}
else {
new-item -ItemType directory -Path $file_destination
move-item $_.fullname $file_destination
}
}
}
Change:
Get-ChildItem $file_type -Recurse | Where-Object { ($_.LastWriteTime -lt $zip_date) -and ($_.psIsContainer -eq $false)} ...
to
Get-ChildItem $file_type -Recurse | Where-Object { ($_.LastWriteTime -lt $zip_date) -and ($_.psIsContainer -eq $false) -and $_.Name.EndsWith(".log")}
and that should get you just the files you want to target
I figured out my problem. Here is how I do it.
I just need to obtain the folder name of each file and build the destination path. Therefore, I only need one foreach loop.
# move all files into servername_mmm_yyyy folder
Get-ChildItem $file_path -Recurse | Where-Object { ($_.LastWriteTime -lt $zip_date) -and ($_.psIsContainer -eq $false)}| foreach {
$x = $_.LastWriteTime.ToShortDateString()
$month_year = Get-Date $x -Format MMM_yyyy
$servername = $_.Directory.Name
$file_destination = ($destination)+($servername)+"_"+($month_year)
if (test-path $file_destination){
copy-item $_.fullname $file_destination
}
else {
new-item -ItemType directory -Path $file_destination
copy-item $_.fullname $file_destination
}
}

Copy everything except files on the list

I am trying to copy all files recursively from a:\ to b:\, except those whose metadata is present in a:\list.txt. The list.txt pattern is LastWriteTimeYYYY-MM-DD HH:MM:SS,size,.fileextension, for example:
2001-01-31 23:59:59,12345,.doc
2001-01-31 23:59:59,12345,.txt
2001-01-31 23:59:00,456,.csv
...so any and all files, anywhere in the a:\ dir tree, matching these metadata should not be copied.
I seem to be having trouble with the Where-Object in order to exclude the items on the list.txt, but copy everything else:
$Source = "C:\a"
$Target = "C:\b"
$List = Import-Csv list.txt -Header LastWriteTime,Size,Name
$Hash = #{}
ForEach ($Row in $List){
$Key = ("{0},{1},.{2}" -F $Row.LastWriteTime,$Row.Size,$Row.Name.Split('.')[-1].ToLower())
IF (!($Hash[$Key])) {$Hash.Add($Key,$Row.Name)}
}
$Hash | Format-Table -Auto
Get-Childitem -Path $Source -Recurse -File | Where-Object {$Hash -eq $Hash[$Key]}| ForEach-Object {$Key = ("{0},{1},{2}" -F ($_.LastWriteTime).ToString('yyyy-MM-dd HH:mm:ss'),$_.Length,$_.Extension.ToLower())
#$Key
If ($Hash[$Key]){
$Destination = $_.FullName -Replace "^$([RegEx]::Escape($Source))","$Target"
If (!(Test-Path (Split-Path $Destination))){MD (Split-Path $Destination)|Out-Null}
$_ | Copy-Item -Destination $Destination
}
}
I propose you a simplification of your code :
$Source = "C:\a\"
$Target = "C:\b\"
New-Item -ItemType Directory $Target -Force | Out-Null
$List = Import-Csv list.txt -Header LastWriteTime,Length,Extension
Get-Childitem $Source -Recurse -File | %{
$File=$_
$exist=$List | where {$_.LastWriteTime -eq $File.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss') -and $_.Length -eq $File.Length -and $_.Extension -eq $File.Extension} | select -first 1
if ($exist -ne $null) {continue}
New-Item -ItemType Directory $File.DirectoryName.Replace($Source, $Target) -Force | Out-Null
Copy-Item $File.FullName $File.FullName.Replace($Source, $Target) -Force
}