PowerShell Replace number in string - regex

I'm not such a regex expert and frankly I'm trying to avoid it whenever I can.
I would like to create a new $String where the number within the string is updated with +1. This number can be one or two digits and will always be between 2 brackets.
From:
$String = "\\Server\c$\share_1\share2_\Taget2[1] - 2014-07-29.log"
To:
$String = "\\Server\c$\share_1\share2_\Taget2[2] - 2014-07-29.log"
Thank you for your help.

If you want to avoid the regex:
$String = "\\Server\c$\share_1\share2_\Taget2[1] - 2014-07-29.log"
$parts = $string.Split('[]')
$Newstring = '{0}[{1}]{2}' -f $parts[0],(1 + $parts[1]),$parts[2]
$Newstring
\\Server\c$\share_1\share2_\Taget2[2] - 2014-07-29.log

Another option is using the Replace() method of the Regex class with a scriptblock (code taken from this answer by Roman Kuzmin):
$callback = {
$v = [int]$args[0].Groups[1].Value
$args[0] -replace $v,++$v
}
$filename = "\\Server\c$\share_1\share2_\Taget2[1] - 2014-07-29.log"
$re = [Regex]"\[(\d+)\]"
$re.Replace($filename, $callback)
Existing files could be handled like this:
...
$re = [Regex]"\[(\d+)\]"
while (Test-Path -LiteralPath $filename) {
$filename = $re.Replace($filename, $callback)
}
Note that you must use Test-Path with the parameter -LiteralPath here, because your filename contains square brackets, which would otherwise be interpreted as wildcard characters.

With regex:
$s = "\\Server\c$\share_1\share2_\Taget2[1] - 2014-07-29.log"
$s -replace "(?<=\[)(\d+)","bla"
Result:
\\Server\c$\share_1\share2_\Taget2[bla] - 2014-07-29.log
So you can do something like this:
$s = "\\Server\c$\share_1\share2_\Taget2[1] - 2014-07-29.log"
$s -match "(?<=\[)(\d+)" | Out-Null ## find regex matches
$newNumber = [int]$matches[0] + 1
$s -replace "(?<=\[)(\d+)",$newNumber
Result:
\\Server\c$\share_1\share2_\Taget2[2] - 2014-07-29.log

Related

Powershell: Replace only fist occurence of a line/string in entire file

I have following beggining of a Powershell script in which I would like to replace the values of variables for different enviroment.
$SomeVar1 = "C:\path\to\file\a"
$SomeVar1 = "C:\path\to\file\a" # Copy for test - Should not be rewriten
$SomeVar2 = "C:\path\to\file\b"
# Note $SomeVar1 = "C:\path\to\file\a" - Should not be rewriten
When I run the rewrite script, the result should look like this:
$SomeVar1 = "F:\different\path\to\file\a"
$SomeVar1 = "C:\path\to\file\a" # Copy for test - Should not be rewrite
$SomeVar2 = "F:\different\path\to\file\b"
# Note $SomeVar1 = "C:\path\to\file\a" - Should not be rewriten
Current script that does(n't) rewrite:
$arr = #(
[PSCustomObject]#{Regex = '$SomeVar1 = "'; Replace = '$SomeVar1 = "F:\different\path\to\file\a"'}
[PSCustomObject]#{Regex = '$SomeVar2 = "'; Replace = '$SomeVar1 = "F:\different\path\to\file\b"'}
)
for ($i = 0; $i -lt $arr.Length; $i++) {
$ArrRegex = [Regex]::Escape($arr[$i].Regex)
$ArrReplace = $arr[$i].Replace
# Get full line for replacement
$Line = Get-Content $Workfile | Select-String $ArrRegex | Select-Object -First 1 -ExpandProperty Line
# Rewrite part
$Line = [Regex]::Escape($Line)
$Content = Get-Content $Workfile
$Content -replace "^$Line",$ArrReplace | Set-Content $Workfile
}
This replaces all the occurences in file on the start of the line (and I need only the 1st one) and doest not replace the one in Note which is okay.
Then I found this Powershell: Replace last occurence of a line in a file which does the exact oposite of what I need, only rewrites the last occurence of the string and it does it in the Note aswell and I would somehow like to change it to do the opposite - 1st occurence, line begining (Wont target the Note)
Code in my case looks like this:
# Rewrite part
$Line = [Regex]::Escape($Line)
$Content = Get-Content $Workfile -Raw
$Line = "(?s)(.*)$Line"
$ArrReplace = "`$1$ArrReplace"
$Content -replace $Line,$ArrReplace | Set-Content $Workfile
Do you have any recommendations on how to archive my goal, or is there a more sothisticated way to replace variables for powershell scripts like this?
Thanks in advance.
So I finally figured it out, I had to add Select-String "^$ArrRegex" during $Line creation to exclude any string that were on on line beggining and then use this Regex to do the job: ^(?s)(.*?\n)$Line
In my case it does the following: Only selects 1st occurnece on the beggining of the line and replaces it. It ignores everything else and when re-run, does not rewrite others. The copies of vars will not really exist in final version and will be set once like $Var1 = "Value" and never changed during script, but I wanted to be sure that I won't replace something by mistake.
The final replacing part does look like this:
for ($i = 0; $i -lt $arr.Length; $i++) {
$ArrRegex = [Regex]::Escape($arr[$i].Regex)
$ArrReplace = $arr[$i].Replace
$Line = Get-Content $Workfile | Select-String "^$ArrRegex" | Select-Object -First 1 -ExpandProperty Line
$Line = [Regex]::Escape($Line)
$Line = "^(?s)(.*?\n)$Line"
$ArrReplace = "`$1$ArrReplace"
$Content -replace $Line, $ArrReplace | Set-Content $Workfile
}
You could possibly use flag variables like below to only do the first replacement for each of your regex patterns.
$Altered = Get-Content -Path $Workfile |
Foreach-Object {
if(-not $a) { #If replacement hasn't been done, replace
$_ = $_ -replace 'YOUR_REGEX1','YOUR_REPLACEMENT1'
if($_ -match 'YOUR_REPLACEMENT1') { $a = 'replacement done' } #Set Flag
}
if(-not $b) { #If replacement hasn't been done, replace
$_ = $_ -replace 'YOUR_REGEX2','YOUR_REPLACEMENT2'
if($_ -match 'YOUR_REPLACEMENT2') { $b = 'replacement done' } #Set Flag
}
$_ # Pipe back to $Altered
}
$Altered | Set-Content -Path $WorkFile
Just reverse the RegEx, if that is what you are after:
Clear-Host
#'
abc
abc
abc
'# -replace '^(.*?)\babc\b', '$1HelloWorld'
# Results
<#
HelloWorld
abc
abc
#>

Regex replace using a substring of the regex result value

I've been reading a ton of material and thought I had found my solution but no luck. I need to find apostrophes contained in a name and then replace them with a double. I am loading a file to an array and then looping through that, looking for the apostrophes. The catch is that each row can have several apostrophes so that's why it's not a simple find and replace.
Here is a sample of the file:
create(xxxxxxx)using(xxxxxxx)name('O'Doe, John')
replace(xxxxxxx)instdata('ab 1234 ')
create(xxxxxxx)using(xxxxxxx)name('Doe, O'Jane')
replace(xxxxxxx)instdata('ab 5678 ')
There are other lines inbetween but they don't contain apostrophes.
Here is what I have so far:
$Pattern = "[A-Z]'[A-Z]"
$user = gc C:\Temp\mfnewuser.ins
for ($i = 0; $i -lt $user.count; $i++) {
if ($user[$i] -match $Pattern) {
$user[$i] = [regex]::replace($strText, $Pattern.substring(2,1), "''")
$user | out-file C:\Temp\mfnewuser.ins
}
}
I'm looking for a capital letter, followed by an apostrophe, followed by another capital. Because of the other commas, I can't just do a global replace. I know my pattern matching is working but I can't seem to manipulate it with the substring. The substring looks at $Pattern as a string instead of the result of a regex. If I can save the regex result to a variable, that would be great. I think then the replace would be easy.
Tried this as well but no luck either:
$Pattern = "[A-Z]'[A-Z]"
$NewPattern = "[A-Z]''[A-Z]"
$f = Get-Content C:\Temp\mfnewuser.ins
$f = $f -replace $Pattern, $NewPattern
$f | out-file C:\Temp\mfnewuser.ins
I may be approaching this all wrong and there is an easier way but I haven't seen anything yet.
EDIT:
Based on Bill_Stewarts example below, I've got this to work on the First Name but not yet the Last Name:
$Pattern = "[A-Z]'[A-Z]"
$user = gc C:\Temp\mfnewuser.ins
for ($i = 0; $i -lt $user.count; $i++) {
if ($user[$i] -match $Pattern) {
$user[$i] = $user[$i] -replace "(.*[A-Z])'([A-Z]+.*)", "`$1''`$2"
$user | out-file C:\Temp\mfnewuser.ins
}
}
Perhaps something like this?
get-content "test.txt" | foreach-object {
$_ -replace "([A-Z])'([A-Z])", "`$1''`$2"
}
Regular expressions can be grouped using ( ) and the -replace operator supports substring replacement ($1 and $2).
Replace your line, with the following.
$user[$i] = $user[$i] -replace "([A-Z])'([A-Z])", "`$1`''`$2"
Or try one of the following. This should suffice.
get-content "mfnewuser.ins" | foreach-object {
$_ -replace "([A-Z])'([A-Z])", "`$1`''`$2"
} | set-content "mfnewuser.ins"
...
get-content "mfnewuser.ins" | foreach-object {
$_ -replace "([a-zA-Z', ]+)'([a-zA-Z', ]+)", "`$1`''`$2"
} | set-content "mfnewuser.ins"

How to remove the last two occurrences of "-" character of a string?

I have an array of strings that are formatted as such:
Ado-trastuzumab emtansine(HER2)02-22-2013
I would like to remove the last two "-" symbols only (from the date part of the original string) so that the name of the drug (Ado-trastuzumab emtansine) is not altered. Right now my regex removes all "-" symbols:
foreach my $string (#array) {
$string =~ tr/-//d;
}
I would like the output to instead be the following:
Ado-trastuzumab emtansine(HER2)02222013
Thanks for the help!
You can use substr as an lvalue to only apply the transliteration to a particular part of your string:
substr($string, -10, 10) =~ tr/-//d;
In this case, on the last 10 letters of the string.
foreach my $string (#array) {
$string =~ s/(\d{2})-(\d{2})-(\d{4})$/$1$2$3/;
}
To do what you say literally - remove the last two hyphens from a string - you could write this
$string =~ s/-([^-]*)-([^-]*)\z/$1$2/;
But in this case you could simply remove all hyphens that follow a digit:
$string =~ s/\d\K-//g;
If nothing should be done when there's only one -:
$s =~ s/-([^-]*)-([^-]*)\z/$1$2/;
$s = reverse($s);
$s =~ s/^([^-]*)-([^-]*)-/$1$2/;
$s = reverse($s);
$s = reverse( reverse($s) =~ s/^([^-]*)-([^-]*)-/$1$2/r ); # 5.14+
All these work even if there is only one -:
$s =~ s/-([^-]*)(?:-([^-]*))?\z/$1$2/;
$s =~ s/-([^-]*)\z/$1/ for 1..2;
$s =~ s/^.*\K-//s for 1..2;
$s = reverse($s);
$s =~ s/-// for 1..2;
$s = reverse($s);
$s = reverse($s);
$s =~ s/^([^-]*)-(?:([^-]*)-)?/$1$2/;
$s = reverse($s);
$s = reverse( reverse($s) =~ s/^([^-]*)-(?:([^-]*)-)?/$1$2/r ); # 5.14+
For long strings, the reverse solutions should be much faster. For the short strings, go for readability.

PowerShell regex to pick out elapsed time

I am new to powershell scripting and am not so good with Regex...
I want to create a regular expression that will pick out time from the following text file...
gfskf dakdshadk daksdkdahkd daksdhasdkh () zadf sflh f.d / sd lhlfhlj f 12hrs:10mins:05sec fsfsf fsfjhsfjh
I want to get the hours so 12 and mins as 10 and seconds as 5.
$hour= [regex]::match($line,$hour_regex).Groups[1].Value
$mins= [regex]::match($line,$mins_regex).Groups[1].Value
$sec= [regex]::match($line,$sec_regex).Groups[1].Value
So essentially I need three regular expressions to extract the relevant data from the file.
Thanks in advance!
Use one regex:
$r = '(\d+)hrs:(\d+)mins:(\d+)sec'
$i = 'hlfhlj f 12hrs:10mins:05sec fsfsf f'
$result = [regex]::match($i,$r)
$hour = $result.Groups[1].Value
$mins = $result.Groups[2].Value
$sec = $result.Groups[3].Value
Just for fun:
$a = "gfskf dakdshadk daksdkdahkd daksdhasdkh () zadf sflh f.d / sd lhlfhlj f 12hrs:10mins:05sec fsfsf fsfjhsfjh"
$hour,$min, $sec = $a -split '(\d\d)' | ? { $_ -match '\d\d' }
Since nobody mentioned it yet, you can also use the -match operator. The submatches can be accessed via the $matches hashtable:
Get-Content "C:\path\to\your.txt" | ? {
$_ -match '(\d+)hrs:(\d+)mins:(\d+)sec'
} | % {
$h = $matches[1]
$m = $matches[2]
$s = $matches[3]
"{0}:{1}:{2}" -f ($h, $m, $s)
}
Use regex capture groups. "(\d+)hrs:(\d+)mins:(\d+)sec" will capture the numbers into groups that can be accessed. Save the results in a MatchCollection to check the results. Like so,
$line = "sflh f.d / sd lhlfhlj f 12hrs:10mins:05sec fsfsf"
PS C:\> $mc = [regex]::match($line, "(\d+)hrs:(\d+)mins:(\d+)sec")
PS C:\> $mc.Groups[1].value
12
PS C:\> $mc.Groups[2].value
10
PS C:\> $mc.Groups[3].value
05
You can try :
$b= [regex]".* (\d*)hrs:(\d*)mins:(\d*)sec.*"
$hours=$b.Match($a).groups[1].value
$minutes=$b.Match($a).groups[2].value
$seconds=$b.Match($a).groups[3].value
if like me you are not at ease with regex you can use split function :
PS>$t="gfskf dakdshadk daksdkdahkd daksdhasdkh () zadf sflh f.d / sd lhlfhlj f 12hrs:10mins:05sec fsfsf fsfjhsfjh"
PS>$slices=$t.split(#(" ",":"))
$hours=($slices[12]).substring(0,2)
$mins=($slices[13]).substring(0,2)
$secs=($slices[14]).substring(0,2)
PS> $s -replace '^.+ (\d+hrs:\d+mins:\d+sec).*$','$1' -split '\D+' | where {$_}
12
10
05

Replace only up to N matches on a line

In Perl, how to write a regular expression that replaces only up to N matches per string?
I.e., I'm looking for a middle ground between s/aa/bb/; and s/aa/bb/g;. I want to allow multiple substitutions, but only up to N times.
I can think of three reliable ways. The first is to replace everything after the Nth match with itself.
my $max = 5;
$s =~ s/(aa)/ $max-- > 0 ? 'bb' : $1 /eg;
That's not very efficient if there are far more than N matches. For that, we need to move the loop out of the regex engine. The next two methods are ways of doing that.
my $max = 5;
my $out = '';
$out .= $1 . 'bb' while $max-- && $in =~ /\G(.*?)aa/gcs;
$out .= $1 if $in =~ /\G(.*)/gcs;
And this time, in-place:
my $max = 5;
my $replace = 'bb';
while ($max-- && $s =~ s/\G.*?\Kaa/$replace/s) {
pos($s) = $-[0] + length($replace);
}
You might be tempted to do something like
my $max = 5;
$s =~ s/aa/bb/ for 1..$max;
but that approach will fail for other patterns and/or replacement expressions.
my $max = 5;
$s =~ s/aa/ba/ for 1..$max; # XXX Turns 'aaaaaaaa'
# into 'bbbbbaaa'
# instead of 'babababa'
And of course, starting from the beginning of the string every time could be expensive.
What you want is not posible in regular expressions. But you can put the replacement in a for-loop:
my $i;
my $aa = 'aaaaaaaaaaaaaaaaaaaa';
for ($i=0;$i<4;$i++) {
$aa =~ s/aa/bb/;
}
print "$aa\n";
result:
bbbbbbbbaaaaaaaaaaaa
You can use the /e flag which evaluates the right side as an expression:
my $n = 3;
$string =~ s/(aa)/$n-- > 0 ? "bb" : $1/ge;
Here's a solution using the /e modifier, with which you can use
perl code to generate the replacement string:
my $count = 0;
$string =~ s{ $pattern }
{
$count++;
if ($count < $limit ) {
$replace;
} else {
$&; # faking a no-op, replacing with the original match.
}
}xeg;
With perl 5.10 or later you can drop the $& (which has weird
performance complications) and use ${^MATCH} via the /p modifier
$string =~ s{ $pattern }
{
$count++;
if ($count < $limit ) {
$replace;
} else {
${^MATCH};
}
}xegp;
It's too bad you can't just do this, but you can't:
last if $count >= $limit;