Powershell Rename files if name contains <string> - powershell

I'm trying to prepend a PatientOrderNumber to the name of files in a directory if the file name includes a unique ID from my CSV file. I've tried the -like and -contains parameters and have not been successful.
here is my code:
$csvPath = "C:\Users\dougadmin28\Desktop\Test\1bs4e.csv"
## Variable to hold path of Files that need to be renamed ##
$filePath = "C:\Users\dougadmin28\Desktop\Node Modify File Name App\pdfs"
$csv = Import-Csv $csvPath #| Select-Object -Property PatientOrderNumber, FileID, DocumentID, LastName, FirstName|Export-Csv 'C:\Users\dougadmin28\Desktop\Node Modify File Name App\documents.csv'
$files = Get-ChildItem $filePath
foreach ($item in $csv){
foreach($file in $files) {
if($file.FullName -contains '*$item.DocumentID*') {
Rename-Item $file.FullName -NewName "$($item.PatientOrderNumber+"_"+($item.FileID)+'_'+($item.DocumentID)+'_'+($item.LastName)+'_'+($item.FirstName)+($file.extension))"
}#End forEach
}#End if
} #End forEach```

tl;dr
Instead of:
$file.FullName -contains '*$item.DocumentID*' # WRONG
use:
$file.FullName -like "*$($item.DocumentID)*"
Note the use of " instead of ' and the $(...) around the property-access expression.
As for what you tried:
$file.FullName -contains '*$item.DocumentID*'
-contains tests array membership of the RHS in the array-valued LHS, it doesn't perform substring matching.
See this answer for the difference between PowerShell's -contains operator and the String.Contains() .NET method.
'...' strings are taken literally (verbatim), so no expansion (interpolation) is performed; "..." (expandable strings) must be used for string interpolation.
See about_Quoting_Rules for documentation about PowerShell's string-literal types.
Inside an expandable string, only simple variable references (e.g., $item) can be embedded as-is; anything more complex, such as an expression that involves property access (e.g. $item.DocumentID), requires enclosing in $(...), the subexpression operator.
See this answer for an overview of string expansion (interpolation) in PowerShell.

Related

PowerShell: Replace string in all .txt files within directory

I am trying to replace every instance of a string within a directory. However my code is not replacing anything.
What I have so far:
Test Folder contains multiple files and folders containing content that I need to change.
The folders contain .txt documents, the .txt documents contain strings like this: Content reference="../../../PartOfPath/EN/EndofPath/Caution.txt" that i need to change into this: Content reference="../../../PartOfPath/FR/EndofPath/Caution.txt"
Before this question comes up, yes it has to be done this way, as there are other similar strings that I don't want to edit. So I cannot just replace all instances of EN with FR.
$DirectoryPath = "C:\TestFolder"
$Parts =#(
#{PartOne="/PartOfPath";PartTwo="EndofPath/Caution.txt"},
#{PartOne="/OtherPartOfPath";PartTwo="EndofPath/Note.txt"},
#{PartOne="/ThirdPartOfPath";PartTwo="OtherEndofPath/Warning.txt"}) | % { New-Object object | Add-Member -NotePropertyMembers $_ -PassThru }
Get-ChildItem $DirectoryPath | ForEach {
foreach($n in $Parts){
[string]$PartOne = $n.PartOne
[string]$PartTwo = $n.PartTwo
$ReplaceThis = "$PartOne/EN/$PartTwo"
$WithThis = "$PartOne/FR/$PartTwo"
(Get-Content $_) | ForEach {$_ -Replace $ReplaceThis, $WithThis} | Set-Content $_
}
}
The code will run and overwrite files, however no edits will have been made.
While troubleshooting I came across this potential cause:
This test worked:
$FilePath = "C:\TestFolder\Test.txt"
$ReplaceThis ="/PartOfPath/EN/Notes/Note.txt"
$WithThis = "/PartOfPath/FR/Notes/Note.txt"
(Get-Content -Path $FilePath) -replace $ReplaceThis, $WithThis | Set-Content $FilePath
But this test did not
$FilePath = "C:\TestFolder\Test.txt"
foreach($n in $Parts){
[string]$PartOne = $n.PartOne
[string]$PartTwo = $n.PartTwo
[string]$ReplaceThis = "$PartOne/EN/$PartTwo"
[string]$WithThis = "$PartOne/FR/$PartTwo"
(Get-Content -Path $FilePath) -replace $ReplaceThis, $WithThis | Set-Content $FilePath
}
If you can help me understand what is wrong here I would greatly appreciate it.
Thanks to #TessellatingHeckler 's comments I revised my code and found this solution:
$DirectoryPath = "C:\TestFolder"
$Parts =#(
#{PartOne="/PartOfPath";PartTwo="EndofPath/Caution.txt"},
#{PartOne="/OtherPartOfPath";PartTwo="EndofPath/Note.txt"},
#{PartOne="/ThirdPartOfPath";PartTwo="OtherEndofPath/Warning.txt"}) | % { New-Object object | Add-Member -NotePropertyMembers $_ -PassThru }
Get-ChildItem $LanguageFolderPath -Filter "*.txt" -Recurse | ForEach {
foreach($n in $Parts){
[string]$PartOne = $n.PartOne
[string]$PartTwo = $n.PartTwo
$ReplaceThis = "$PartOne/EN/$PartTwo"
$WithThis = "$PartOne/FR/$PartTwo"
(Get-Content $_) | ForEach {$_.Replace($ReplaceThis, $WithThis)} | Set-Content $_
}
}
There were two problems:
Replace was not working as I intended, so I had to use .replace instead
The original Get-ChildItem was not returning any values and had to be replaced with the above version.
PowerShell's -replace operator is regex-based and case-insensitive by default:
To perform literal replacements, \-escape metacharacters in the pattern or call [regex]::Escape().
By contrast, the [string] type's .Replace() method performs literal replacement and is case-sensitive, invariably in Windows PowerShell, by default in PowerShell (Core) 7+ (see this answer for more information).
Therefore:
As TessellatingHeckler points out, given that your search strings seem to contain no regex metacharacters (such as . or \) that would require escaping, there is no obvious reason why your original approach didn't work.
Given that you're looking for literal substring replacements, the [string] type's .Replace() is generally the simpler and faster option if case-SENSITIVITY is desired / acceptable (invariably so in Windows PowerShell; as noted, in PowerShell (Core) 7+, you have the option of making .Replace() case-insensitive too).
However, since you need to perform multiple replacements, a more concise, single-pass -replace solution is possible (though whether it actually performs better would have to be tested; if you need case-sensitivity, use -creplace in lieu of -replace):
$oldLang = 'EN'
$newLang = 'FR'
$regex = #(
"(?<prefix>/PartOfPath/)$oldLang(?<suffix>/EndofPath/Caution.txt)",
"(?<prefix>/OtherPartOfPath/)$oldLang(?<suffix>/EndofPath/Note.txt)",
"(?<prefix>/ThirdPartOfPath/)$oldLang(?<suffix>/OtherEndofPath/Warning.txt)"
) -join '|'
Get-ChildItem C:\TestFolder\Test.txt -Filter *.txt -Recurse | ForEach-Object {
($_ |Get-Content -Raw) -replace $regex, "`${prefix}$newLang`${suffix}" |
Set-Content -LiteralPath $_.FullName
}
See this regex101.com page for an explanation of the regex and the ability to experiment with it.
The expression used as the replacement operand, "`${prefix}$newLang`${suffix}", mixes PowerShell's up-front string interpolation ($newLang, which could also be written as ${newLang}) with placeholders referring to the named capture groups (e.g. (?<prefix>...)) in the regex, which only coincidentally use the same notation as PowerShell variables (though enclosing the name in {...} is required; also, here the $ chars. must be `-escaped to prevent PowerShell's string interpolation from interpreting them); see this answer for background information.
Note the use of -Raw with Get-Content, which reads a text file as a whole into memory, as a single, multi-line string. Given that you don't need line-by-line processing in this case, this greatly speeds up the processing of a given file.
As a general tip: you may need to use the -Encoding parameter with Set-Content to ensure the desired character encoding, given that PowerShell never preserves a file's original coding when reading it. By default, you'll get ANSI-encoded files in Windows PowerShell, and BOM-less UTF-8 files in PowerShell (Core) 7+.

Script lists all files that don't contain needed content

I'm trying to find all files in a dir, modified within the last 4 hours, that contain a string. I can't have the output show files that don't contain needed content. How do I change this so it only lists the filename and content found that matches the string, but not files that don't have that string? This is run as a windows shell command. The dir has a growing list of hundreds of files, and currently output looks like this:
File1.txt
File2.txt
File3.txt
... long long list, with none containing the needed string
(powershell "Set-Location -Path "E:\SDKLogs\Logs"; Get-Item *.* | Foreach { $lastupdatetime=$_.LastWriteTime; $nowtime = get-date; if (($nowtime - $lastupdatetime).totalhours -le 4) {Select-String -Path $_.Name -Pattern "'Found = 60.'"| Write-Host "$_.Name Found = 60"; }}")
I tried changing the location of the Write-Host but it's still printing all files.
Update:
I'm currently working on this fix. Hopefully it's what people were alluding to in comments.
$updateTimeRange=(get-date).addhours(-4)
$fileNames = Get-ChildItem -Path "K:\NotFound" -Recurse -Include *.*
foreach ($file in $filenames)
{
#$content = Get-Content $_.FullName
Write-host "$($file.LastWriteTime)"
if($file.LastWriteTime -ge $($updateTimeRange))
{
#Write-Host $file.FullName
if(Select-String -Path $file.FullName -Pattern 'Thread = 60')
{
Write-Host $file.FullName
}
}
}
If I understood you correctly, you just want to display the file name and the matched content? If so, the following will work for you:
$date = (Get-Date).AddHours(-4)
Get-ChildItem -Path 'E:\SDKLogs\Logs' | Where-Object -FilterScript { $date -lt $_.LastWriteTime } |
Select-String -Pattern 'Found = 60.' |
ForEach-Object -Process {
'{0} {1}' -f $_.FileName, $_.Matches.Value
}
Get-Date doesn't need to be in a variable before your call but, it can become computationally expensive running a call to it again and again. Rather, just place it in a variable before your expression and call on the already created value of $date.
Typically, and for best practice, you always want to filter as far left as possible in your command. In this case we swap your if statement for a Where-Object to filter as the objects are passed down the pipeline. Luckily for us, Select-String returns the file name of a match found, and the matched content so we just reference it in our Foreach-Object loop; could also use a calculated property instead.
As for your quoting issues, you may have to double quote or escape the quotes within the PowerShell.exe call for it to run properly.
Edit: swapped the double quotes for single quotes so you can wrap the entire expression in just PowerShell.exe -Command "expression here" without the need of escaping; this works if you're pattern to find doesn't contain single quotes.

How to take a list of partial file names and return a list of the full file names with PowerShell

I’m wondering how to take a list of partial document names and return a list of the full document names with PowerShell.
I have ton of documents to do this with. We have a naming scheme: HG-xx-xx-###
The full naming scheme for the actual files is: HG-xx-xx-###.x.x_File_Name
I have a lot of different lists of file names like so:
HG-SP-HG-001
HG-WI-BE-005
HG-GD-BB-043
I'm trying to have program return a list of the full file names like so:
HG-SP-HG-001.1.6_Example
HG-WI-BE-005.1.0_Example
HG-GD-BB-043.1.1_Example
I've included both methods I've tried. I give it a list or even just one partial file name and I get nothing back.
I've tried two different ways and I'm at the end of my programming and googling capabilities, any ideas?
$myPath = 'P:\'
$_DocList = READ-HOST "Enter list of document ID's"
$_DocList = $_DocList.Split(',').Split(' ')
#Here I'm not sure if I should do it like so:
$output =
ForEach($_Doc in $_DocList)
{
$find = gci $myPath -Recurse |where{$_.name -contains $($_Doc)}
Write-Host "$find"
}
$output | clip
#or like this:
$_DocList | ForEach-Object
{
gci -Path $myPath -Filter $_ -Recurse
$info = Get-ChildItem $_.FullName | Measure-Object
if ($info.Count -ne 0) {
Write-Output "$($_.Name)"
}
} | clip
Doug Maurer's helpful answer shows a solution based on a wildcard pattern based to the -Filter parameter.
Since this parameter only accepts a single pattern, the Get-ChildItem -Recurse call must be called multiple times, in a loop.
However, since you're using -Recurse, you can take advantage of the -Include parameter, which accepts multiple patterns, so you can get away with one Get-ChildItem call.
While for a single Get-ChildItem call -Filter performs better than -Include, a single Get-ChildItem -Include call with an array of pattern is likely to outperform multiple Get-ChildItem -Filter calls, especially with many patterns.
# Sample name prefixes to search for.
$namePrefixes = 'HG-SP-HG-001', 'HG-WI-BE-005', 'HG-GD-BB-043'
# Append '*' to all prefixes to form wildcard patterns: 'HG-SP-HG-001*', ...
$namePatterns = $namePrefixes -replace '$', '*'
# Combine Get-ChildItem -Recurse with -Include and all patterns.
# .Name returns the file name part of all matching files.
$names = (Get-ChildItem $myPath -File -Recurse -Include $namePatterns).Name
Maybe something like this?
$docList = #('HG-SP-HG-*','HG-WI-BE-*','HG-GD-BB-*')
foreach($item in $docList)
{
$check = Get-ChildItem -Filter $item P:\ -File
if($check)
{
$check
}
}
Maybe something like this?
$docList = #('HG-SP-HG','HG-WI-BE','HG-GD-BB')
$docList | Get-ChildItem -File -Filter $_ -Recurse | select Name
When using the filter with partial names you'll need to specify wildcard
$names = 'HG-SP-HG','HG-WI-BE','HG-GD-BB'
$names | Foreach-Object {
Get-ChildItem -File -Filter $_* -Recurse
}
And if you only want the full path back, simply select it.
$names = 'HG-SP-HG','HG-WI-BE','HG-GD-BB'
$names | Foreach-Object {
Get-ChildItem -File -Filter $_* -Recurse
} | Select-Object -ExpandProperty FullName
If you have an established pattern of what the files look like, why not regex it?
# Use these instead to specify a docID
#$docID = "005"
#pattern = "^HG(-\w{2}){2}-$docID"
$pattern = "^HG(-\w{2}){2}-\d{3}"
Get-ChildItem -Path "P:\" -Recurse | ?{$_ -match $pattern}
Granted, there may be more efficient ways to do this, but it should be quick enough for a few thousand files.
EDIT: This is the breakdown of the regex pattern's hieroglyphics.
^ Start at the beginning
HG literal characters "HG"
(-\w{2})
( start of a grouping
- literal "-" character (hyphen)
\w{2}
\w any word character
{2} exactly 2 times
) End of the grouping
{2} exactly 2 times
- literal "-" character (hyphen)
\d any digit 0 through 9
{3} Exactly 3 times

Referencing targeted object in ForEach-Object in Powershell

I am fairly new to Powershell. I am attempting to re-write the names of GPO backup folders to use their friendly name rather than their GUID by referencing the name in each GPO backup's 'gpresult.xml' file that is created as part of the backup. However, I do not understand how I can reference the specific object (in this case, the folder name) that is being read into the ForEach-Object loop in order to read into the file beneath this folder.
function Backup_GPO {
$stamp = Get-Date -UFormat "%m%d"
New-Item -ItemType "directory" -Name $stamp -Path \\dc-16\share\GPO_Backups -Force | out-null # create new folder to specify day backup is made
Backup-GPO -All -Path ("\\dc-16\share\GPO_Backups\" + "$stamp\" )
Get-ChildItem -Path \\dc-16\share\GPO_Backups\$stamp | ForEach-Object {
# I want to reference the current folder here
[xml]$file = Get-Content -Path (folder that is being referenced in for loop)\gpresult.xml
$name = $file.GPO.Name
}
I'm coming from Python, where if I want to reference the object I'm currently iterating on, I can do so very simply -
for object in list:
print(object)
How do you reference the currently in-use object in Powershell's ForEach-Object command?
I'm coming from Python, where if I want to reference the object I'm currently
iterating on, I can do so very simply -
for object in list:
print(object)
The direct equivalent of that in PowerShell is the foreach statement (loop):
$list = 1..3 # create a 3-element array with elements 1, 2, 3
foreach ($object in $list) {
$object # expression output is *implicitly* output
}
Note that you cannot directly use a foreach statement in a PowerShell pipeline.
In a pipeline, you must use the ForEach-Object cmdlet instead, which - somewhat confusingly - can also be referred to as foreach, via an alias - it it is only the parsing mode that distinguishes between the statement and the cmdlet's alias.
You're using the ForEach-Object cmdlet in the pipeline, where different rules apply.
Script blocks ({ ... }) passed to pipeline-processing cmdlets such as ForEach-Object and Where-Object do not have an explicit iteration variable the way that the foreach statement provides.
Instead, by convention, such script blocks see the current pipeline input object as automatic variable $_ - or, more verbosely, as $PSItem.
While the foreach statement and the ForEach-Object cmdlet operate the same on a certain level of abstraction, there's a fundamental difference:
The foreach statement operates on collections collected up front in memory, in full.
The ForEach-Object cmdlet operates on streaming input, object by object, as each object is being received via the pipeline.
This difference amounts to the following trade-off:
Use the foreach statement for better performance, at the expense of memory usage.
Use the ForEach-Object cmdlet for constant memory use and possibly also for the syntactic elegance of a single pipeline, at the expense of performance - however, for very large input sets, this may be the only option (assuming you don't also collect a very large dataset in memory on output).
Inside the ForEach-Object scriptblock, the current item being iterated over is copied to $_:
Get-ChildItem -Filter gpresult.xml |ForEach-Object {
# `$_` is a FileInfo object, `$_.FullName` holds the absolute file system path
[xml]$file = Get-Content -LiteralPath $_.FullName
}
If you want to specify a custom name, you can either specify a -PipelineVariable name:
Get-ChildItem -Filter gpresult.xml -PipelineVariable fileinfo |ForEach-Object {
# `$fileinfo` is now a FileInfo object, `$fileinfo.FullName` holds the absolute file system path
[xml]$file = Get-Content -LiteralPath $fileinfo.FullName
}
or use a foreach loop statement, much like for object in list in python:
foreach($object in Get-ChildItem -Filter gpresult.xml)
{
[xml]$file = Get-Content -LiteralPath $object.FullName
}
Another way...
$dlist = Get-ChildItem -Path "\\dc-16\share\GPO_Backups\$stamp"
foreach ($dir in $dlist) {
# I want to reference the current folder here
[xml]$file = Get-Content -Path (Join-Path -Path $_.FullName -ChildPath 'gpresult.xml')
$name = $file.GPO.Name
}
Here's my solution. It's annoying that $_ doesn't have the full path. $gpath is easier to work with than $_.fullname for joining the two strings together on the next line with get-content. I get a gpreport.xml file when I try backup-gpo. Apparently you can't use relative paths like .\gpo_backups\ with backup-gpo.
mkdir c:\users\js\gpo_backups\
get-gpo -all | where displayname -like '*mygpo*' |
backup-gpo -path c:\users\js\gpo_backups\
Get-ChildItem -Path .\GPO_Backups\ | ForEach-Object {
$gpath = $_.fullname
[xml]$file = Get-Content -Path "$gpath\gpreport.xml"
$file.GPO.Name
}

Iterate & Search for a string in child items

I am trying to build a PowerShell script that iterates through a list of files and searches and removes a match, not having much luck, here is my script
$path = "D:\Test\"
$filter = "*.txt"
$files = Get-ChildItem -path $path -filter $filter
foreach ($item in $files)
{
$search = Get-content -path $path$item
$search| select-string -pattern "T|"
}
At the moment the script is just returning the whole content of the file and not the select string.
Basically each file in the folder will have a trailer record at the end i.e. T|1410 I need to iterate through all the files and delete the last line, some of these files will be 200mb+ can someone guide me please.
I've edited my script and now I am using the following method.
$path = "D:\Test\"
$filter = "*.txt"
$files = Get-ChildItem -path $path -filter $filter
foreach ($item in $files)
{
$search = Get-content $path$item
($search)| ForEach-Object { $_ -replace 'T\|[0-9]*', '' } | Set-Content $path$item
}
I am using Powershell v.2
However, this is adding a new empty line to my end of file as well as leaving the replace empty, how can I avoid this as well as starting the search from the bottom
-pattern "T|"
That pattern matches a "T" or nothing. But there is nothing between every pair of characters in any string. To avoid the usual regular expression handling of | as an alternates separator, use a backslash to match a literal |:
-pattern "T\|"
Alternately, use Select-String's -SimpleMatch switch to stop the argument to -Pattern being treated as a regular expression.
As Richard mentioned, you have to escape the | character.
You could also use the regex::escape function for that:
[regex]::Escape("T|")
Aside from escaping the characters the other option you have available is the -SimpleMatch switch. From TechNet
Uses a simple match rather than a regular expression match. In a simple match, Select-String searches the input for the text in the Pattern parameter. It does not interpret the value of the Pattern parameter as a regular expression statement.
If you don't want to have to worry about escaping the characters and are not using regex this would be the way to go.
$search | select-string -pattern "T|" -SimpleMatch