Loop through folders/files and replace word - powershell

Trying to loop through a folders/subfolders that contains multiple html files to find a specific string/word.
Once the powershell script finds it - it will list which files contains the matching words.
Maybe a prompt for the user to replace the word or just set a variable such as $replaceword = 'test2'
$match = "THIS IS A TEST"
#$replacement = Read-Host "Please enter a solution name"
$files = Get-ChildItem -path C:\Users\testfolder -filter *THIS IS A TEST* -Recurse
$files
Sort-Object -Descending -Property { $_.FullName }
#Rename-Item -newname { $_.name -replace $match, $replacement } -force
$files = Get-ChildItem -path C:\Users\testfolder -include *.html -Recurse
foreach($file in $files)
{
((Get-Content $file.fullname) -creplace $match)
read-host -prompt "Done! Press any key to close."
}

You can do something like the following, which will replace matching content of .html files in the C:\Users\testfolder directory structure:
$matchString = 'THIS IS A TEST'
$replaceString = 'New String'
$files = Get-ChildItem -Path 'C:\Users\testfolder' -filter *.html -Recurse
foreach ($file in $files) {
$matchFound = $false
$output = switch -regex -file $file.fullname {
$matchString { $_ -replace $matchString,$replaceString
$matchFound = $true
}
default { $_ }
}
if ($matchFound) {
$output | Set-Content $file.fullname
}
}
If you really want an interactive prompt to replace the string, you can move the $replaceString to a different place within the script logic and add the Read-Host command.
Explanation:
The top of the script contains $matchString (the case-sensitive string you want to match) and $replaceString (the replacement string).
Get-ChildItem uses -Filter to find all files that end with the HTML extension. The -Recurse switch is used to search all subdirectories from the path.
The switch statement is used in favor of Get-Content because it typically performs faster. We only want to overwrite a file when a match is found. $matchFound begins as $false and will become $true when a match is found.
-creplace is used here since your source code used it. -creplace performs a case-sensitive regex match and replace.
Interactive Alternative:
For an interactive element, you can ask the user to enter the replacement string each time a match is found. This simply moves the $replaceString assignment inside of the switch statement and lists the file that contains the match.
$matchString = 'THIS IS A TEST'
$files = Get-ChildItem -Path 'C:\Users\testfolder' -filter *.html -Recurse
foreach ($file in $files) {
$matchFound = $false
$output = switch -regex -file $file.fullname {
$matchString { $replaceString = Read-Host -Prompt "$($file.fullname) contains '$matchString'. Enter your replacement string"
$_ -replace $matchString,$replaceString
$matchFound = $true
}
default { $_ }
}
if ($matchFound) {
$output | Set-Content $file.fullname
}
}

What about something like this, it's a simpler example:
$files = Get-ChildItem -Path c:\path\here -Recurse
$matchString = 'THIS IS A TEST'
$replacementString = 'replace'
foreach ($i in $files){
#Check its a file
if (!$i.PSIsContainer){
#Check the name contains the $matchString
if ($i.Name -match $matchString){
Set-Content -Path $i.FullName -Value $replacementString -Confirm:$true
}
}
}
Let me know if I've misunderstood what you want :)

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

Fix script to allow a scanning of all fields for every row to find if that field contains a target folder name

This is an deepening extension to solve a previously question
Since the input csv file is inconsistent, I cannot use that in this way.
The folder to move to is not always in the same column, so any code trying to use that as input will hit the problem of the value not corresponding to the folder I want to move to. It simply reads garbage ("spam") where it expects the folder name.
The only way to do it is by examining all fields for every row to find if that field contains a target folder name you can use. That means a LOT of Test-Path lines
This is the incriminated code part
Foreach ($fileName in $_.Group.FileName) {
$ValidFileName = $filename -replace $invalid
$targetFile = Join-Path -Path $TargetFolder -ChildPath $fileName
Error is explicitly telling it to move those folders when I iterate through the FileName column. Foreach ($fileName in $_.Group.FileName) {.. this is reading: (1959 10) Showcase Presents n 22,(1959 12) Showcase Presents n 23,alfa, da definire.
My request: Since that examination of all fields for every row is required so I ask for a code editing of this script. Also if this means a LOT of Test-Path lines I suppose that there is no alternative.
However this script below don't create folders and move anything so my request is to try to fix it
$csvpath = 'C:\temp\temp.csv'
$invalid = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join ''))
$sourcePath = 'C:\temp\'
Import-Csv C:\temp\temp.csv -Header Title,FileName,Link -Delimiter ';' |
Group-Object Title |
Foreach {
# I prefer to add a trailing slash to folder names
$TargetFolder = Join-Path -Path $sourcePath -ChildPath (($_.Name -replace $invalid)+'\')
# We don't have to create the new folders, because -Force will create them for us
Foreach ($fileName in $_.Group.FileName) {
$ValidFileName = $filename -replace $invalid
$targetFile = Join-Path -Path $TargetFolder -ChildPath $fileName
# Write your values to the console - Make sure the folder is what it should be
Write-Output "Moving '$targetFile' to '$TargetFolder'"
Move-Item $targetFile $TargetFolder -Force -WhatIf
}
}
I was hesitant to even comment on this since you're just not taking any of our advise on all the previoous posts, and just repeating what we say in a new question. We are here to help with your code, and not write it for you but, learning isn't a crime so I will just assume you're just unfamiliar with powershell in general.
$csvpath = 'C:\tomp\temp.csv'
#$invalid = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join ''))
$sourcePath = 'C:\tomp\'
Import-Csv $csvpath -Header Title,FileName,Link -Delimiter ',' |
Group-Object Title |
ForEach-Object `
-Begin {
$FoldersLocation = Get-ChildItem -Path C:\tomp -Directory
$Source_Folder = ($FoldersLocation.Parent.FullName | Select-Object -Unique)
#Create array list to hold both columns
[System.Collections.ArrayList]$CombinedRows = #()
} `
-Process {
# Create folder if it doesn't exist
$Destination = Join-Path -Path $Source_Folder -ChildPath $_.Name
if ((Test-Path -Path $Destination) -eq $false) {
"Created: $Destination"
#$null = New-Item -Path ($FoldersLocation.Parent.FullName | Select -Unique) -Name $_.Name -ItemType Directory
}
#region Combine two columns
Foreach ($fileName in $_.Group.FileName) {
$null = $CombinedRows.Add($fileName)
}
Foreach ($link in $_.Group.Link) {
$null = $CombinedRows.Add($link)
}
#endregion
} `
-End {
Foreach ($name in $FoldersLocation) {
if ($name.Name -in $CombinedRows) {
if ($name.Name -match "Showcase"){
# No need for output when supplying the -verbose switch
"Moving: '$($Name.FullName)' to 'C:\tomp\Lanterna Verde - Le Prime Storie'"
# Move-Item -Path $Name.FullName -Destination "C:\tomp\Lanterna Verde - Le Prime Storie"
}
else {
# No need for output when supplying the -verbose switch
"Moving: '$($Name.FullName)]' to 'C:\tomp\Batman DC'"
# Move-Item -Path $Name.FullName -Destination "C:\tomp\Batman DC"
}
}
}
}
All it comes down to is control of flow logic. If a certain condition is met, then do this, or do something else if its not. Added very few inline comments as most of it pretty self explanatory.
I recommend reading up on some powershell instead of hoping others could do everything for you.
Powershell

How to parse through folders and files using PowerShell?

I am trying to construct a script that moves through specific folders and the log files in it, and filters the error codes. After that it passes them into a new file.
I'm not really sure how to do that with for loops so I'll leave my code bellow.
If someone could tell me what I'm doing wrong, that would be greatly appreciated.
$file_name = Read-Host -Prompt 'Name of the new file: '
$path = 'C:\Users\user\Power\log_script\logs'
Add-Type -AssemblyName System.IO.Compression.FileSystem
function Unzip
{
param([string]$zipfile, [string]$outpath)
[System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)
}
if ([System.IO.File]::Exists($path)) {
Remove-Item $path
Unzip 'C:\Users\user\Power\log_script\logs.zip' 'C:\Users\user\Power\log_script'
} else {
Unzip 'C:\Users\user\Power\log_script\logs.zip' 'C:\Users\user\Power\log_script'
}
$folder = Get-ChildItem -Path 'C:\Users\user\Power\log_script\logs\LogFiles'
$files = foreach($logfolder in $folder) {
$content = foreach($line in $files) {
if ($line -match '([ ][4-5][0-5][0-9][ ])') {
echo $line
}
}
}
$content | Out-File $file_name -Force -Encoding ascii
Inside the LogFiles folder are three more folders each containing log files.
Thanks
Expanding on a comment above about recursing the folder structure, and then actually retrieving the content of the files, you could try something line this:
$allFiles = Get-ChildItem -Path 'C:\Users\user\Power\log_script\logs\LogFiles' -Recurse
# iterate the files
$allFiles | ForEach-Object {
# iterate the content of each file, line by line
Get-Content $_ | ForEach-Object {
if ($_ -match '([ ][4-5][0-5][0-9][ ])') {
echo $_
}
}
}
It looks like your inner loop is of a collection ($files) that doesn't yet exist. You assign $files to the output of a ForEach(...) loop then try to nest another loop of $files inside it. Of course at this point $files isn't available to be looped.
Regardless, the issue is you are never reading the content of your log files. Even if you managed to loop through the output of Get-ChildItem, you need to look at each line to perform the match.
Obviously I cannot completely test this, but I see a few issues and have rewritten as below:
$file_name = Read-Host -Prompt 'Name of the new file'
$path = 'C:\Users\user\Power\log_script\logs'
$Pattern = '([ ][4-5][0-5][0-9][ ])'
if ( [System.IO.File]::Exists( $path ) ) { Remove-Item $path }
Expand-Archive 'C:\Users\user\Power\log_script\logs.zip' 'C:\Users\user\Power\log_script'
Select-String -Path 'C:\Users\user\Power\log_script\logs\LogFiles\*' -Pattern $Pattern |
Select-Object -ExpandProperty line |
Out-File $file_name -Force -Encoding ascii
Note: Select-String cannot recurse on its own.
I'm not sure you need to write your own UnZip function. PowerShell has the Expand-Archive cmdlet which can at least match the functionality thus far:
Expand-Archive -Path <SourceZipPath> -DestinationPath <DestinationFolder>
Note: The -Force parameter allows it to over write the destination files if they are already present. which may be a substitute for testing if the file exists and deleting if it does.
If you are going to test for the file that section of code can be simplified as:
if ( [System.IO.File]::Exists( $path ) ) { Remove-Item $path }
Unzip 'C:\Users\user\Power\log_script\logs.zip' 'C:\Users\user\Power\log_script'
This is because you were going to run the UnZip command regardless...
Note: You could also use Test-Path for this.
Also there are enumerable ways to get the matching lines, here are a couple of extra samples:
Get-ChildItem -Path 'C:\Users\user\Power\log_script\logs\LogFiles' |
ForEach-Object{
( Get-Content $_.FullName ) -match $Pattern
# Using match in this way will echo the lines that matched from each run of
# Get-Content. If nothing matched nothing will output on that iteration.
} |
Out-File $file_name -Force -Encoding ascii
This approach will read the entire file into an array before running the match on it. For large files it may pose a memory issue, however it enabled the clever use of -match.
OR:
Get-ChildItem -Path 'C:\Users\user\Power\log_script\logs\LogFiles' |
Get-Content |
ForEach-Object{ If( $_ -match $Pattern ) { $_ } } |
Out-File $file_name -Force -Encoding ascii
Note: You don't need the alias echo or its real cmdlet Write-Output
UPDATE: After fuzzing around a bit and trying different things I finally got it to work.
I'll include the code below just for demonstration purposes.
Thanks everyone
$start = Get-Date
"`n$start`n"
$file_name = Read-Host -Prompt 'Name of the new file: '
Out-File $file_name -Force -Encoding ascii
Expand-Archive -Path 'C:\Users\User\Power\log_script\logs.zip' -Force
$i = 1
$folders = Get-ChildItem -Path 'C:\Users\User\Power\log_script\logs\logs\LogFiles' -Name -Recurse -Include *.log
foreach($item in $folders) {
$files = 'C:\Users\User\Power\log_script\logs\logs\LogFiles\' + $item
foreach($file in $files){
$content = Get-Content $file
Write-Progress -Activity "Filtering..." -Status "File $i of $($folders.Count)" -PercentComplete (($i / $folders.Count) * 100)
$i++
$output = foreach($line in $content) {
if ($line -match '([ ][4-5][0-5][0-9][ ])') {
Add-Content -Path $file_name -Value $line
}
}
}
}
$end = Get-Date
$time = [int]($end - $start).TotalSeconds
Write-Output ("Runtime: " + $time + " Seconds" -join ' ')

How do I use where condition in powershell

Here what I want to do is get the list of the folders that have files which has the value of ErrorCode > 0.
This is what I have done till now.
$fileNames = Get-ChildItem -Path $scriptPath -Recurse -Include *.data
$FoldersToRename = #() #initialize as array
foreach ($file in $fileNames) {
If (Get-Content $file | %{$_ -match '"ErrorCode": 0'})
{
echo "matched"
}
Now I have .data file which are being searched by this program. It contains an object with a value of "ErrorCode":value. I want to perform some operations only if that value is greater than zero.
How do I solve this?
One way to do it is like this:
Get-ChildItem -Path $scriptPath -Filter *.data |
ForEach-Object {
if((Get-Content -Path $_.FullName -Raw) -match '"ErrorCode": [1-9]\d*') {
"Matched"
}
}

Replace string content between quotes in file

I have made this loop to iterate some files that I want to replace content in.
The content that I want to replace is a string which can look something like this: foo="1".
What I need your help with is how to find the string (regexp I guess) and update the file with a new value, like 2 for example.
$files = Get-ChildItem -Recurse -Filter "*.config"
foreach ($file in $files)
{
Get-Content $file.FullName
}
Read-Host
From the sample code in the question, I assume you're attempting to update a .NET configuration file (ie. a web.config or app.config file).
Given that these files are really XML files, you may want to treat them as such:
$files = Get-ChildItem -Recurse -Filter "*.config"
foreach ($file in $files)
{
# Create an XmlDocument object from the file
$configXml = [xml](Get-Content $file.FullName)
# Find all the nodes in the document
$xmlNodes = $configXml.SelectNodes('//*')
# Keep track of whether we make changes or not
$changeCount = 0
foreach($node in $xmlNodes)
{
# Check if node has a "foo" attribute
if($node.HasAttribute('foo'))
{
# Set 2 as the value
$node.SetAttribute('foo',2)
$changeCount++
}
}
if($changeCount)
{
# At least one node was updated, save to file
$configXml.Save($file.FullName)
}
}
You can try regex. Ex.
$files = Get-ChildItem -Recurse -Filter "*.config"
$find = 'foo=".*?"'
$replace = 'foo="4"'
foreach ($file in $files)
{
Get-Content $file.FullName |
Foreach-Object { $_ -replace $find, $replace } |
Set-Content $file.Fullname
}
}
Read-Host
Or to only modify files that match:
$files = Get-ChildItem -Recurse -Filter "*.config"
$find = 'foo=".*?"'
$replace = 'foo="4"'
foreach ($file in $files)
{
$content = Get-Content $file.FullName
if(Select-String -InputObject $content -Pattern $find -Quiet) {
$content |
Foreach-Object { $_ -replace $find, $replace } |
Set-Content $file.Fullname
}
}
Read-Host
This should answer your question
$files = Get-ChildItem -Recurse -Filter "*.config"
foreach ($file in $files)
{
$fileContent = Get-Content $file.FullName
$newContent = $fileContent -replace 'foo="1"', 'foo="2"'
Set-Content $file.FullName $newContent
}
Further reading here: Use PowerShell to Replace Text in Strings