Powershell overwriting file contents with match instead of editing single line - powershell

I have a text file that contains a string I want to modify.
Example text file contents:
abc=1
def=2
ghi=3
If I run this code:
$file = "c:\test.txt"
$MinX = 100
$MinY = 100
$a = (Get-Content $file) | %{
if($_ -match "def=(\d*)"){
if($Matches[1] -gt $MinX){$_ -replace "$($Matches[1])","$($MinX)" }
}
}
$a
The result is:
def=100
If I omit the greater-than check like so:
$a = (Get-Content $file) | %{
if($_ -match "def=(\d*)"){
$_ -replace "$($Matches[1])","$($MinX)"
}
}
$a
The result is correct:
abc=1
def=100
ghi=3
I don't understand how a simple integer comparison before doing the replace could screw things up so badly, can anyone advise what I'm missing?

The comparison operator -gt will never get you a value of $true because you need to
cast the $matches[1] string value to int first so it compares two integer numbers
2 is never greater than 100.. Change the operator to -lt instead.
Your code outputs only one line, because you forgot to also output unchanged lines that do not match the regex
$file = 'c:\test.txt'
$MinX = 100
$MinY = 100
$a = (Get-Content $file) | ForEach-Object {
if ($_ -match '^def=(\d+)'){
if([int]$matches[1] -lt $MinX){ $_ -replace $matches[1],$MinX }
}
else {
$_
}
}
$a
Or use switch (is also faster than using Get-Content):
$file = 'c:\test.txt'
$MinX = 100
$MinY = 100
$a = switch -Regex -File $file {
'^def=(\d+)' {
if([int]$matches[1] -lt $MinX){ $_ -replace $matches[1],$MinX }
}
default { $_ }
}
$a
Output:
abc=1
def=100
ghi=3

That's because the expression ($Matches[1] -gt $MinX) is a string comparison. In Powershell, the left-hand side of a comparison dictates the comparison type and since that is of type [string], Powershell has to cast/convert the right-hand side of the expression to [string] also. You expression, therefore, is evaluated as ([string]$Matches[1] -gt [string]$MinX).

Related

How do I loop through a string read from a file against a fixed set of characters individually and if they match print the string in powershell

I currently have a foreach loop, that gets content from a small dictionary file (only strings over 3 characters). I am looking to compare each character in the $line against my other characters, in this case "b" "i" "n" "g" "o" so that if all the characters in $line are in bingo, then it prints the word. If not it loops to the next word.
So far I have:
foreach($line in Get-Content Desktop/dict.txt | Sort-Object Length, { $_ })
The bit I can't get (not too familiar with powershell) is this:
if($line.length -gt 3){
if( i in $line == 'b')
if( i in $line == 'i')
if( i in $line == 'n')
if( i in $line == 'g')
if( i in $line == 'o')
write-output $line
}
}
If I understood correctly, if you want to check if $line is contained in bingo you could use the -match for case insensitive and -cmatch for case sensitive operators. See Comparison Operators.
For example:
PS /> 'bingo' -match 'ing'
True
PS /> 'bingo' -match 'bin'
True
PS /> 'bingo' -match 'ngo'
True
The code could look like this:
foreach($line in Get-Content Desktop/dict.txt | Sort-Object Length, { $_ })
{
if($line.length -gt 3 -and 'bingo' -match $line)
{
$line
# you can add break here to stop this loop if the word is found
}
}
Edit
If you want to check if 3 or more characters in bingo (in any order) are contained in $line, there are many ways to do this, this is the approach I would take:
# Insert magic word here
$magicWord = 'bingo'.ToCharArray() -join '|'
foreach($line in Get-Content Desktop/dict.txt | Sort-Object Length, { $_ })
{
# Remove [Text.RegularExpressions.RegexOptions]::IgnoreCase if you want it to be Case Sensitive
$check = [regex]::Matches($line,$magicWord,[Text.RegularExpressions.RegexOptions]::IgnoreCase)
# If 3 or more unique characters were matched
if(($check.Value | Select-Object -Unique).count -ge 3)
{
'Line is: {0} || Characters Matched: {1}' -f $line,-join $check.Value
}
}
Demo
Given the following words:
$test = 'ngob','ibgo','gn','foo','var','boing','ingob','oubingo','asdBINGasdO!'
It would yield:
Line is: ngob || Characters Matched: ngob
Line is: ibgo || Characters Matched: ibgo
Line is: boing || Characters Matched: boing
Line is: ingob || Characters Matched: ingob
Line is: oubingo || Characters Matched: obingo
Line is: asdBINGasdO! || Characters Matched: BINGO
So you want to get back any words that are the same length and have the same characters no matter the order?
$dict = #(
'bingo'
'rambo'
'big'
'gobin'
'bee'
'ebe'
'been'
'ginbo'
)
$word = 'bingo'
$dict |
Where-Object { $_.length -eq $word.Length } |
ForEach-Object {
$dictwordLetters = [System.Collections.Generic.List[char]]::new($_.ToCharArray())
$word.ToCharArray() | ForEach-Object {
$dictwordLetters.Remove($_) | Out-Null
}
if (-not $dictwordLetters.Count) {
$_
}
}
The following will be the output
bingo
gobin
ginbo
By taking parts of both answers I was able to get the result I was after. As I am new to this, not sure how to thank #martin and #santiago for their work.
This was the code that was put together, which was pretty much taking the dictionary file and then rather than a fixed string size made it greater than 3:
$dict = #(Get-Content Desktop/dict.txt | Sort-Object Length, { $_ })
$word = 'bingo'
$dict |
Where-Object { $_.length -gt 2 } |
ForEach-Object {
$dictwordLetters = [System.Collections.Generic.List[char]]::new($_.ToCharArray())
$word.ToCharArray() | ForEach-Object {
$dictwordLetters.Remove($_) | Out-Null
}
if (-not $dictwordLetters.Count) {
$_
}
}
Your assistance was greatly appreciated.
Here's my two cents:
$dict = 'apple', 'brown', 'green', 'cake', 'bin', 'pear', 'big', 'milk', 'bio', 'bong', 'bingo', 'bodings', 'gibson'
# the search term as string
$term = 'bingo'
# merge the unique characters into a regex like '[bingo]+'
$chars = '[{0}]+' -f (($term.ToCharArray() | Select-Object -Unique) -join '')
# loop over the array (lines in the text file)
$dict | ForEach-Object {
# get all matched characters, uniqify and join if there are more matches.
$found = (($_ | Select-String -Pattern $chars -AllMatches).Matches.Value | Select-Object -Unique ) -join '' | Where-Object { $_.Length -ge 3 }
if ($found) {
# outputting an object better explains what is matched in which line
[PsCustomObject]#{
Line = $_
CharactersMatched = $found
}
# of course, you can also simply output the found matching characters
# $found
}
}
Output:
Line CharactersMatched
---- -----------------
brown bon
bin bin
big big
bio bio
bong bong
bingo bingo
bodings boing
gibson gibon
The previous answers all seem overly complicated. If you are trying to match strings then that sounds like a problem that requires a regular expression, and if that is the case then Select-String would be a better option than Get-Content. Below is an example, I am not sure if it is exactly right for your needs but should point you in the right direction:
Select-String 'Desktop/dict.txt' -pattern '^[bingo]{3,}$'

Cycling through multiple variables in for loop

I just started working with Powershell and this is my first script.
I am checking for 3 strings in last 50 lines of a log file. I need to find all three strings and print error message if any one of those is missing. I have written following script but it does not give me the expected results.
(Get-Content C:\foo\bar.log )[-1..-50] | Out-File C:\boom\shiva\log.txt
$PO1 = Get-Content C:\boom\shiva\log.txt | where {$_ -match "<Ping:AD_P01_RCV> ok"}
$PO2 = Get-Content C:\boom\shiva\log.txt | where {$_ -match "<Ping:AD_P02_SND> ok"}
$PO3 = Get-Content C:\boom\shiva\log.txt | where {$_ -match "<Ping:AD_P03_RCV> ok"}
I am satisfied with above piece of code. The problem is with the below. I dont want to use if-else thrice. I am struggling to draft a for loop which can save space and still give me the same result.
if (!$PO1)
{
"PO1 is critical"
}
else
{
"PO1 is OK"
}
if (!$PO2)
{
"PO2 is critical"
}
else
{
"PO2 is OK"
}
if (!$PO3)
{
"PO3 is critical"
}
else
{
"PO3 is OK"
}
Can someone gave me small example of how i can fit these 3 if-else in one for loop.
If you only want to find out that all 3 strings are present this script will also show which one is missing.
(binary encoded in the variable $Cnt)
## Q:\Test\2018\07\13\SO_51323760.ps1
##
$Last50 = Get-Content 'C:\foo\bar.log' | Select-Object -Last 50
$Cnt = 0
if ($Last50 -match "<Ping:AD_P01_RCV> ok"){$Cnt++}
if ($Last50 -match "<Ping:AD_P02_SND> ok"){$Cnt+=2}
if ($Last50 -match "<Ping:AD_P03_RCV> ok"){$Cnt+=4}
if ($cnt -eq 7){
"did find all 3 strings "
} else {
"didn't find all 3 strings ({0})" -f $cnt
}
Variant immediately complaining missing P0(1..3)
$Last50 = Get-Content 'C:\foo\bar.log' | Select-Object -Last 50
if (!($Last50 -match "<Ping:AD_P01_RCV> ok")) {"PO1 is critical"}
if (!($Last50 -match "<Ping:AD_P02_SND> ok")) {"PO2 is critical"}
if (!($Last50 -match "<Ping:AD_P03_RCV> ok")) {"PO3 is critical"}
Sorry I'm a bit slow this monday.
To check in a loop different variables by building the variable name:
1..3| ForEach-Object {
If (!(Get-Variable -name "P0$_").Value){"`$P0$_ is critical"}
}
What you're trying to do is better addressed with a hashtable than with individually named variables.
$data = Get-Content 'C:\boom\shiva\log.txt'
$ht = #{}
1..3 | ForEach-Object {
$key = 'P{0:d2}' -f $_
$str = if ($_ -eq 2) {"${key}_SND"} else {"${key}_RCV"}
$ht[$key] = $data -match "<ing:AD_${str}> ok"
}
$ht.Keys | ForEach-Object {
if ($ht[$_]) {
"${key} found in log."
} else {
"${key} not found in log."
}
}
You can check if all lines were present at least once with something like this:
if (($ht.Values | Where-Object { $_ }).Count -lt 3) {
'Line missing from log.'
}
PSv3 introduced the -Tail (-Last) parameter to Get-Content, which is the most efficient way to extract a fixe number of lines from the end of a file.
You can pipe its output to Select-String, which accepts an array of regex patterns, any of which produces a match (implicit OR logic).
$matchingLines = Get-Content -Tail 50 C:\foo\bar.log |
Select-String '<Ping:AD_P01_RCV> ok', '<Ping:AD_P02_SND> ok', '<Ping:AD_P03_RCV> ok'
if ($matchingLines) { # at least 1 of the regexes matched
$matchingLines.Line # output the matching lines
} else { # nothing matched
Write-Warning "Nothing matched."
}
I finally got below draft that resolved my query to cycle variables through a for loop. I finally had to convert those individual variables to a array. But htis gives me expected result. Basically i need this script to provide input to my Nagios plugin which needs minor modification but its done.
(Get-Content C:\foo\bar.log )[-1..-50] | Out-File C:\boom\shiva\log.txt
$j = 1
$PO = new-object object[] 3
$PO[0] = Get-Content C:\boom\shiva\log.txt | where {$_ -match "<Ping:AD_P01_RCV> ok"}
$PO[1] = Get-Content C:\boom\shiva\log.txt | where {$_ -match "<Ping:AD_P02_SND> ok"}
$PO[2] = Get-Content C:\boom\shiva\log.txt | where {$_ -match "<Ping:AD_P03_RCV> ok"}
foreach( $i in $PO){
if (!$i){
"PO "+$j+" is CRITICAL"}
else{
"PO "+$j+" is OK"}
$j+=1
}
Thank you LotPings, Ansgar and mklement0 for your support and responses. I picked up a few things from your answers.

Loop over array

I need a piece of powershell-code to search and replace a certain string inside a text-file. In my example, I want to replace 23-06-2016' with '24-06-2016'. The script below does this job:
$original_file = 'file.old'
$destination_file = 'file.new'
(Get-Content $original_file) | Foreach-Object {
$_ -replace '23-06-2016', '24-06-2016' `
} | Out-File -encoding default $destination_file
As the search / replace string change I want to loop over an array of dates which might look like this:
$dates = #("23-06-2016","24-06-2016","27-06-2016")
I tried use the
$original_file = 'file.old'
$destination_file = 'file.new'
foreach ($date in $dates) {
(Get-Content $original_file) | Foreach-Object {
$_ -replace 'date', 'date++' `
} | Out-File -encoding default $destination_file
}
In a first step, the date '23-06-2016' should be replaced by '24-06-2016' and in a second step, the date '24-06-2016' should be replaced by '27-06-2016'.
As my script is not working I am seeking for some advice.
You are using $date as your instance variable in your foreach loop but then referencing it as 'date', which is just a string. Even if you used '$date' it would not work because single-quoted strings do not expand variables.
Further, $date is not a number, so date++ would not do anything even it were referenced as a variable $date++. Further still, $var++ returns the original value before incrementing, so you would be referencing the same date (as opposed to the prefix version ++$var).
In a foreach loop, it's not very practical to refer to other elements, in most cases.
Instead, you could use a for loop:
for ($i = 0; $i -lt $dates.Count ; $i++) {
$find = $dates[$i]
$rep = $dates[$i+1]
}
This isn't necessarily the most clear way to do it.
You might be better off with a [hashtable] that uses the date to find as a key, and the replacement date as the value. Sure, you'd be duplicating some dates as value and key, but I think I'd rather have the clarity:
$dates = #{
"23-06-2016" = "24-06-2016"
"24-06-2016" = "27-06-2016"
}
foreach ($pair in $dates.GetEnumerator()) {
(Get-Content $original_file) | Foreach-Object {
$_ -replace $pair.Key, $pair.Value
} | Out-File -encoding default $destination_file
}

Switch strings in a file

I have a string needs to be changed in a file between two values. What I want to do is if I found value A then change to value B, if I found value B then change to value A. there will be a message box popup saying that value has been changed to [xxxxx] then background picture will be also changed accordingly.
$path = c:\work\test.xml
$A = AAAAA
$B = BBBBB
$settings = get-content $path
$settings | % { $_.replace($A, $B) } | set-content $path
I could not figured out how to use IF A then replace with B or IF B then replace A. Also, the code above will delete rest of contents in the file and only save the part that I modified back to the file.
Assuming that $A and $B contain just simple strings rather than regular expressions you could use a switch statement with wildcard matches:
$path = 'c:\work\test.xml'
$A = 'AAAAA'
$B = 'BBBBB'
(Get-Content $path) | % {
switch -wildcard ($_) {
"*$A*" { $_ -replace [regex]::Escape($A), $B }
"*$B*" { $_ -replace [regex]::Escape($B), $A }
default { $_ }
}
} | Set-Content $path
The [regex]::Escape() makes sure that characters having a special meaing in regular expressions are escaped, so the values are replaced as literal strings.
If you're aiming for something a little more advanced, you could use a regular expression replacement with a callback function:
$path = 'c:\work\test.xml'
$A = 'AAAAA'
$B = 'BBBBB'
$rep = #{
$A = $B
$B = $A
}
$callback = { $rep[$args[0].Groups[1].Value] }
$re = [regex]("({0}|{1})" -f [regex]::Escape($A), [regex]::Escape($B))
(Get-Content $path) | % {
$re.Replace($_, $callback)
} | Set-Content $path
This isn't tested extensively, but I think it should work:
path = c:\work\test.xml
$A = 'AAAAA'
$B = 'BBBBB'
[regex]$regex = "$A|$B"
$text =
Get-Content $path |
foreach {
$regex.Replace($text,{if ($args[0].value -eq $A){$B} else {$A}})
}
$text | Set-Content $path
Hard to be sure without knowing exactly what the data looks like.

Using Context in Powershell Select-String

I have a script that searches for a series of strings (stored in a txt file) in the contents of files in a directory. I would like to modify it to also list the text around the string found (these are regular strings, not regex expressions). I played around a lot and it seems like I need to use -Context, but I am not sure how to get the text from that.
Also, the files I am searching may not have linefeeds, so if it could just get the xx characters before and after the search term, that would be better.
Here's what I have so far (I omitted the looping though files parts):
$result = Get-Content $file.FullName | Select-String $control -quiet
If ($result -eq $True)
{
$match = $file.FullName
"Match on string : $control in file : $match" | Out-File $output -Append
Write-host "Match on string : $control in file : $match"
}
If it could write the context, that would be perfect. Seems like I need to use $_Matches, but not sure how.
If $control is just a regular string, can you turn it into a regular expression?
$n = 3
$re = "(.{0,$n})(" + [Regex]::Escape($control) + ")(.{0,$n})"
$result = (Get-Content $file.FullName) -match $re
With this, the $matches hashtable should give you access to the $n characters before and after the match:
if ($result.Length -gt 0) {
echo "Before: $($matches[1])"
echo "After: $($matches[3])"
}
Here is what I have now and it seems to work:
$regex = "[\s\S]{0,$ContextChars}$SearchTerm[\s\S]{0,$ContextChars}"
$results = Get-Content $file.FullName | Select-String -Pattern $regex -AllMatches | % { $_.Matches } | % { $_.Value }
if ($results)
{
foreach($result in $results)
{
$display = $result
"File: $file Match ---$display---"
}
}
The only thing I wish I had but don't know how to get it is the line number the match is found on.