Powershell regex to get DC from Distinguished Name - regex

I'm trying to get the first DC that appears for a given Distinguished name as below:
CN=blah1,CN=Computers,DC=blah2,DC=blah3
So in plain English I wish to "replace all strings, up to 'DC=', and return any value from DC= and up to the next ,
I've tried working it out using online calculators but somehow doesn't work.

Have a look at this:
$str = "CN=blah1,CN=Computers,DC=blah2,DC=blah3"
$str -match '^.*?(DC=.*?),'
$Matches[1] # DC=blah2
It finds the first DC=* where * is whatever follows the = until the next comma.

Well to show the second easy method to grep certain parts of your string. Try to use - split:
$string = "CN=blah1,CN=Computers,DC=blah2,DC=blah3"
#Seperate the string at ",DC=" and get the second part
($string -split ',DC=')[1]
Returns: blah2

I think the best (by "best" I mean the "most robust") answer is not to do string parsing or a regex in the first place and use the IADSPathname interface to retrieve the part of the name you're interested in. This is available (albeit initially somewhat complex) in PowerShell via the Pathname COM object. The Pathname object handles all escaping of characters automatically.
Example:
$ADS_SETTYPE_DN = 4
$ADS_DISPLAY_VALUE_ONLY = 2
$Pathname = New-Object -ComObject Pathname
$distinguishedName = "CN=blah1,CN=Computers,DC=blah2,DC=blah3"
# Set the AD path in the object
$Pathname.GetType().InvokeMember("Set", "InvokeMethod", $null, $Pathname, #($distinguishedName,$ADS_SETTYPE_DN))
# Get the number of name elements
$numElements = $Pathname.GetType().InvokeMember("GetNumElements", "InvokeMethod", $null, $Pathname, $null)
# Retrieve the second-to-last name element (outputs "DC=blah2")
$Pathname.GetType().InvokeMember("GetElement", "InvokeMethod", $null, $Pathname, $numElements - 2)
# Set the display type to values only
$Pathname.GetType().InvokeMember("SetDisplayType", "InvokeMethod", $null, $Pathname, $ADS_DISPLAY_VALUE_ONLY)
# Retrieve the second-to-last name element (outputs "blah2")
$Pathname.GetType().InvokeMember("GetElement", "InvokeMethod", $null, $Pathname, $numElements - 2)
Admittedly, the Pathname COM object is not easy to use in PowerShell because you have to call it indirectly. This can be alleviated somewhat by using a "wrapper" function to invoke the object's methods. Example:
$ADS_SETTYPE_DN = 4
$ADS_DISPLAY_VALUE_ONLY = 2
$Pathname = New-Object -ComObject Pathname
function Invoke-Method {
param(
[__ComObject] $object,
[String] $method,
$parameters
)
$output = $object.GetType().InvokeMember($method, "InvokeMethod", $null, $object, $parameters)
if ( $output ) { $output }
}
$distinguishedName = "CN=blah1,CN=Computers,DC=blah2,DC=blah3"
# Set the AD path in the object
Invoke-Method $Pathname "Set" #($distinguishedName,$ADS_SETTYPE_DN)
# Get the number of name elements
$numElements = Invoke-Method $Pathname "GetNumElements"
# Retrieve the second-to-last name element (outputs "DC=blah2")
Invoke-Method $Pathname "GetElement" ($numElements - 2)
# Set the display type to values only
Invoke-Method $Pathname "SetDisplayType" $ADS_DISPLAY_VALUE_ONLY
# Retrieve the second-to-last name element (outputs "blah2")
Invoke-Method $Pathname "GetElement" ($numElements - 2)
For a complete solution, I wrote a PowerShell module called ADName that provides an easy-to-use interface for both the Pathname and the NameTranslate objects.
In the ADName module, the Get-ADName cmdlet is a wrapper for the Pathname object, and the Convert-ADName cmdlet is a wrapper for the NameTranslate object. Example:
# Get elements of name as an array
$nameElements = Get-ADName "CN=blah1,CN=Computers,DC=blah2,DC=blah3" -Split
# Output second-to-last element (e.g., "DC=blah2")
$nameElements[-2]
# Get name elements (values only)
$nameElements = Get-ADName "CN=blah1,CN=Computers,DC=blah2,DC=blah3" -Split -ValuesOnly
# Output second-to-last element (e.g., "blah2")
$nameElements[-2]
I've found that the Get-ADName and Convert-ADName cmdlets are extremely useful in a variety of scenarios. One example:
$name = "CN=blah1,CN=Computers,DC=blah2,DC=blah3"
# Output canonical name of parent path; e.g.: "blah2.blah3/Computers"
$name | Get-ADName -Format Parent | Convert-ADName Canonical

Split is enough for this, i.e:
$s = "CN=blah1,CN=Computers,DC=blah2,DC=blah3"
$s.Split(",")[2].Split("=")[1]
# blah2
Powershell Demo

Related

Colorizing Get-Help output: how to use Regex to select exact string that starts with a hyphen(-) and ends with an alphabet

I'm currently trying to color my PowerShell's Get-Help cmdlet output. I successfully colored the output that shows the name of the cmdlet that I'm trying to use Get-Help on. I've also managed to color the output that shows all the headings of manual page. However, I'm unable to consistently color the output of the options shown on the manual page as you can see below:
#!/usr/bin/env powershell
$GREEN = "$([char]0x1b)[92m"
$RED = "$([char]0x1b)[91m"
$CYAN = "$([char]0x1b)[96m"
$BLUE = "$([char]0x1b)[94m"
$YELLOW = "$([char]0x1b)[93m"
$PURPLE = "$([char]0x1b)[95m"
$RESET = "$([char]0x1b)[0m"
Get-Help #args > man_text.txt
$WORD = $args[0]
cat man_text.txt | `
% {$_ `
-creplace "^[A-Z \d\W]+$", "$GREEN`$0$RESET" `
-creplace "\b$WORD\b", "$YELLOW`$0$RESET" `
-replace "-[a-z]*\b", "$CYAN`$0$RESET" `
}
In other words, I need the regex that matches a string that starts with a "-" and ends with an alphabet.
I would really appreciate if someone could help me with this. Thanks in advance.
This is something you could put into your $Profile file to automatically colorize output of Get-Help. It fixes the problem of colorizing the parameters using RegEx \B-\w+ (see regex101 demo).
# Overrides the original Get-Help command to colorize its output.
Function Get-Help {
[CmdletBinding(DefaultParameterSetName='AllUsersView', HelpUri='https://go.microsoft.com/fwlink/?LinkID=2096483')]
param(
[Parameter(Position=0, ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullOrEmpty()]
[string] ${Name},
[string] ${Path},
[ValidateSet('Alias','Cmdlet','Provider','General','FAQ','Glossary','HelpFile','ScriptCommand','Function','Filter','ExternalScript','All','DefaultHelp','DscResource','Class','Configuration')]
[string[]] ${Category},
[Parameter(ParameterSetName='DetailedView', Mandatory=$true)]
[switch] ${Detailed},
[Parameter(ParameterSetName='AllUsersView')]
[switch] ${Full},
[Parameter(ParameterSetName='Examples', Mandatory=$true)]
[switch] ${Examples},
[Parameter(ParameterSetName='Parameters', Mandatory=$true)]
[string[]] ${Parameter},
[string[]] ${Component},
[string[]] ${Functionality},
[string[]] ${Role},
[Parameter(ParameterSetName='Online', Mandatory=$true)]
[switch] ${Online},
[Parameter(ParameterSetName='ShowWindow', Mandatory=$true)]
[switch] ${ShowWindow}
)
process {
# Call the original Get-Help command by its fully qualified path.
$help = Microsoft.PowerShell.Core\Get-Help #PSBoundParameters
# Define the styles for colorization.
$style = #{
SECTION = $PSStyle.Formatting.FormatAccent
COMMAND = $PSStyle.Foreground.BrightYellow
PARAM = $PSStyle.Foreground.FromRgb(64,200,230)
}
# Escape the command name for use in RegEx
$commandNameEscaped = [regex]::Escape( $help.Name )
# Define a RegEx for doing the formatting. The names of the RegEx groups have to match the keys of the $style hashtable.
$regEx = #(
"(?m)(?<=^[ \t]*)(?<SECTION>[A-Z][A-Z \t\d\W]+$)"
"(?<COMMAND>\b$commandNameEscaped\b)"
"(?<PARAM>\B-\w+)"
) -join '|'
# Format the help object
$help | Out-String | ForEach-Object {
[regex]::Replace( $_, $regEx, {
# Get the RegEx group that has matched.
$matchGroup = $args.Groups.Where{ $_.Success }[ 1 ]
# Use the RegEx group name to select associated style for colorizing the match.
$style[ $matchGroup.Name ] + $matchGroup.Value + $PSStyle.Reset
})
}
}
}
Output:
Remarks:
By defining a function with the same name as an existing command, we effectively override it.
We can call the original command by specifying its fully qualified name, with module prefix like Microsoft.PowerShell.Core\Get-Help. To get the module prefix, type (Get-Command TheCommand).ModuleName.
Using the automatic $PSStyle variable as a handy way to get ANSI escape codes for coloring.
This even works when we call a command with -? parameter, as this calls Get-Help internally.
Demo and explanation of the complete pattern at regex101.
Requires PS 7.2+

Advanced pattern matching in Powershell

Hope you can help me with something. Thanks to #mklement0 I've gotten a great script matching the most basic, initial pattern for words in alphabetical order. However what's missing is a full text search and select.
An example of current script with a small sample of a few words within a Words.txt file:
App
Apple
Apply
Sword
Swords
Word
Words
Becomes:
App
Sword
Word
This is great as it really narrows down to a basic pattern per line! However the result of it going line by line there is still a pattern that can further be narrowed down which is "Word" (capitalization not important) so ideally the output should be:
App
Word
And "Sword" is removed as it falls in more basic pattern prefixed as "Word".
Would you have any suggestion on how to achieve this? Keep in mind this will be a dictionary list of about 250k words, so I would not know what I am looking for ahead of time
CODE (from a related post, handles prefix matching only):
$outFile = [IO.File]::CreateText("C:\Temp\Results.txt") # Output File Location
$prefix = '' # initialize the prefix pattern
foreach ($line in [IO.File]::ReadLines('C:\Temp\Words.txt')) # Input File name.
{
if ($line -like $prefix)
{
continue # same prefix, skip
}
$line # Visual output of new unique prefix
$prefix = "$line*" # Saves new prefix pattern
$outFile.writeline($line) # Output file write to configured location
}
You can try a two-step approach:
Step 1: Find the list of unique prefixes in the alphabetically sorted word list. This is done by reading the lines sequentially, and therefore only requires you to hold the unique prefixes as a whole in memory.
Step 2: Sort the resulting prefixes in order of length and iterate over them, checking in each iteration whether the word at hand is already represented by a substring of it in the result list.
The result list starts out empty, and whenever the word at hand has no substring in the result list, it is appended to the list.
The result list is implemented as a regular expression with alternation (|), to enable matching against all already-found unique words in a single operation.
You'll have to see if the performance is good enough; for best performance, .NET types are used directly as much as possible.
# Read the input file and build the list of unique prefixes, assuming
# alphabetical sorting.
$inFilePath = 'C:\Temp\Words.txt' # Be sure to use a full path.
$uniquePrefixWords =
foreach ($word in [IO.File]::ReadLines($inFilePath)) {
if ($word -like $prefix) { continue }
$word
$prefix = "$word*"
}
# Sort the prefixes by length in ascending order (shorter ones first).
# Note: This is a more time- and space-efficient alternative to:
# $uniquePrefixWords = $uniquePrefixWords | Sort-Object -Property Length
[Array]::Sort($uniquePrefixWords.ForEach('Length'), $uniquePrefixWords)
# Build the result lists of unique shortest words with the help of a regex.
# Skip later - and therefore longer - words, if they are already represented
# in the result list of word by a substring.
$regexUniqueWords = ''; $first = $true
foreach ($word in $uniquePrefixWords) {
if ($first) { # first word
$regexUniqueWords = $word
$first = $false
} elseif ($word -notmatch $regexUniqueWords) {
# New unique word found: add it to the regex as an alternation (|)
$regexUniqueWords += '|' + $word
}
}
# The regex now contains all unique words, separated by "|".
# Split it into an array of individual words, sort the array again...
$resultWords = $regexUniqueWords.Split('|')
[Array]::Sort($resultWords)
# ... and write it to the output file.
$outFilePath = 'C:\Temp\Results.txt' # Be sure to use a full path.
[IO.File]::WriteAllLines($outFilePath, $resultWords)
Reducing arbitrary substrings is a bit more complicated than prefix matching, as we can no longer rely on alphabetical sorting.
Instead, you could sort by length, and then keep track of patterns that can't be satisfied by a shorter one, by using a hash set:
function Reduce-Wildcard
{
param(
[string[]]$Strings,
[switch]$SkipSort
)
# Create set containing all patterns, removes all duplicates
$Patterns = [System.Collections.Generic.HashSet[string]]::new($Strings, [StringComparer]::CurrentCultureIgnoreCase)
# Now that we only have unique terms, sort them by length
$Strings = $Patterns |Sort-Object -Property Length
# Start from the shortest possible pattern
for ($i = 0; $i -lt ($Strings.Count - 1); $i++) {
$current = $Strings[$i]
if(-not $Patterns.Contains($current)){
# Check that we haven't eliminated current string already
continue
}
# There's no reason to search for this substring
# in any of the shorter strings
$j = $i + 1
do {
$next = $Strings[$j]
if($Patterns.Contains($next)){
# Do we have a substring match?
if($next -like "*$current*"){
# Eliminate the superstring
[void]$Patterns.Remove($next)
}
}
$j++
} while ($j -lt $Strings.Count)
}
# Return the substrings we have left
return $Patterns
}
Then use like:
$strings = [IO.File]::ReadLines('C:\Temp\Words.txt')
$reducedSet = Reduce-Wildcard -Strings $strings
Now, this is definitely not the most space-efficient way of reducing your patterns, but the good news is that you can easily divide-and-conquer a large set of inputs by merging and reducing the intermediate results:
Reduce-Wildcard #(
Reduce-Wildcard -Strings #('App','Apple')
Reduce-Wildcard -Strings #('Sword', 'Words')
Reduce-Wildcard -Strings #('Swords', 'Word')
)
Or, in case of multiple files, you can chain successive reductions like this:
$patterns = #()
Get-ChildItem dictionaries\*.txt |ForEach-Object {
$patterns = Reduce-Wildcard -String #(
$_ |Get-Content
$patterns
)
}
My two cents:
Using -Like or RegEx might get expensive on the long run knowing that they used in the inner loop of the selection the invocation will increase exponentially with the size of the word list. Besides, the pattern of the -Like and RegEx operation might need to be escaped (especially for Regex where e.g. a dot . has a special meaning. I Suspect that this question has something to do with checking for password complexity).
Presuming that it doesn't matter whether the output list is in lower case, I would use the String.Contains() method. Otherwise, If the case of the output does matter, you might prepare a hash table like $List[$Word.ToLower()] = $Word and use that restore the actual case at the end.
# Remove empty words, sort by word length and change everything to lowercase
# knowing that .Contains is case sensitive (and therefore presumably a little faster)
$Words = $Words | Where-Object {$_} | Sort-Object Length | ForEach-Object {$_.ToLower()}
# Start with a list of the smallest words (I guess this is a list of all the words with 3 characters)
$Result = [System.Collections.ArrayList]#($Words | Where-Object Length -Eq $Words[0].Length)
# Add the word to the list if it doesn't contain any of the all ready listed words
ForEach($Word in $Words) {
If (!$Result.Where({$Word.Contains($_)},'First')) { $Null = $Result.Add($Word) }
}
2020-04-23 updated the script with the suggestion from #Mathias:
You may want to use Where({$Word.Contains($_)},'First') to avoid comparing against all of $Result everytime
which is about twice as fast.

Sort by named capture group within regular expression in powershell

I have a regular expression defined as follows:
$regexpo = "^Amma\s(?'version'(\d+\.){3}\d)\.zip$"
The above expression has label attached with a group - (\d+.){3}\d)
And I am using it as follows:
$Library = $wholeContent |
Where-Object { $_.Name -match $regexpo } |
Sort-Object Name -Descending
But I want to sort it by the labelled group within the defined regular expression instead, which represents a version number.
Use Sort-Object with a calculated property (a script block ({ ... }) that is evaluated for each input object, reflected in $_) as follows:
# Sample input
$wholeContent =
[pscustomobject] #{ Name = 'Amma 10.0.0.1.zip'; Type = '...' },
[pscustomobject] #{ Name = 'Not a match' ; Type = '...' },
[pscustomobject] #{ Name = 'Amma 1.0.0.2.zip' ; Type = '...' },
[pscustomobject] #{ Name = 'Amma 2.1.2.3.zip' ; Type = '...' }
# Define the regex that matches the full name
# and captures the embedded version number in named capture group 'version'.
# Better to use '...' (single quotes) to define regexes, to prevent
# confusion with string expansion inside "..."
# Note the alternate syntax `<version>` instead of `'version'`.
$regex = '^Amma\s(?<version>(\d+\.){3}\d+)\.zip$'
# Filter by the line of interest, then sort by the extracted version number.
# Automatic variable $Matches is a hashtable that contains the results of
# the regex match, with entry 'version' containing the capture group's value.
# Casting to [version] ensures that version-appropriate sorting is used.
$wholeContent |
Where-Object { $_.Name -match $regex } |
Sort-Object { [version] ($_.Name -replace $regex, '${version}') }
Note that matching twice is required here[1]: once to filter in the lines of interest, and again to extract the embedded version text, via the -replace operator.
Note: Use of -replace with the original regex here is possible, because the regex at hand is designed to match the whole input string, which allows replacing the whole string with just the named capture group's value (${version}) to yield only the latter; the more verbose alternative is to use another -match operation to obtain the capture-group value via $Matches:
$null = $_.Name -match $regex; $Matches['version']
The above yields the following, showing that only the lines of interest were extracted, properly sorted by version number:
Name Type
---- ----
Amma 1.0.0.2.zip ...
Amma 2.1.2.3.zip ...
Amma 10.0.0.1.zip ...
[1] While the automatic $Matches variable, populated by a -match operation, is available in script blocks in subsequent pipeline segments in principle, allowing access to the results of the matching operation, it cannot be utilized here, because Sort-Object is of necessity an aggregating cmdlet; that is, it must collect all input first in order to perform sorting, at which point using $Matches in the calculated property only contains the last input object's matches.
Simple example of sorting by a regex match, by the last character.
echo a3 b2 c1 | sort { $_ -match "(?'last'.$)"; $matches.last }
c1
b2
a3

Powershell Find String Between Characters and Replace

In Powershell script, I have Hashtable contains personal information. The hashtable looks like
{first = "James", last = "Brown", phone = "12345"...}
Using this hashtable, I would like to replace strings in template text file. For each string matches #key# format, I want to replace this string to value that correspond to key in hashtable. Here is a sample input and output:
input.txt
My first name is #first# and last name is #last#.
Call me at #phone#
output.txt
My first name is James and last name is Brown.
Call me at 12345
Could you advise me how to return "key" string between "#"s so I can find their value for the string replacement function? Any other ideas for this problem is welcomed.
You could do this with pure regex, but for the sake of readability, I like doing this as more code than regex:
$tmpl = 'My first name is #first# and last name is #last#.
Call me at #phone#'
$h = #{
first = "James"
last = "Brown"
phone = "12345"
}
$new = $tmpl
foreach ($key in $h.Keys) {
$escKey = [Regex]::Escape($key)
$new = $new -replace "#$escKey#", $h[$key]
}
$new
Explanation
$tmpl contains the template string.
$h is the hashtable.
$new will contain the replaced string.
We enumerate through each of the keys in the hash.
We store a regex escaped version of the key in $escKey.
We replace $escKey surrounded by # characters with the hashtable lookup for the particular key.
One of the nice things about doing this is that you can change your hashtable and your template, and never have to update the regex. It will also gracefully handle the cases where a key has no corresponding replacable section in the template (and vice-versa).
You can create a template using an expandable (double-quoted) here-string:
$Template = #"
My first name is $($hash.first) and last name is $($hash.last).
Call me at $($hash.phone)
"#
$hash = #{first = "James"; last = "Brown"; phone = "12345"}
$Template
My first name is James and last name is Brown.
Call me at 12345

Powershell: Replacing regex named groups with variables

Say I have a regular expression like the following, but I loaded it from a file into a variable $regex, and so have no idea at design time what its contents are, but at runtime I can discover that it includes the "version1", "version2", "version3" and "version4" named groups:
"Version (?<version1>\d),(?<version2>\d),(?<version3>\d),(?<version4>\d)"
...and I have these variables:
$version1 = "3"
$version2 = "2"
$version3 = "1"
$version4 = "0"
...and I come across the following string in a file:
Version 7,7,0,0
...which is stored in a variable $input, so that ($input -match $regex) evaluates to $true.
How can I replace the named groups from $regex in the string $input with the values of $version1, $version2, $version3, $version4 if I do not know the order in which they appear in $regex (I only know that $regex includes these named groups)?
I can't find any references describing the syntax for replacing a named group with the value of a variable by using the group name as an index to the match - is this even supported?
EDIT:
To clarify - the goal is to replace templated version strings in any kind of text file where the version string in a given file requires replacement of a variable number of version fields (could be 2, 3, or all 4 fields). For example, the text in a file could look like any of these (but is not restricted to these):
#define SOME_MACRO(4, 1, 0, 0)
Version "1.2.3.4"
SomeStruct vs = { 99,99,99,99 }
Users can specify a file set and a regular expression to match the line containing the fields, with the original idea being that the individual fields would be captured by named groups. The utility has the individual version field values that should be substituted in the file, but has to preserve the original format of the line that will contain the substitutions, and substitute only the requested fields.
EDIT-2:
I think I can get the result I need with substring calculations based on the position and extent of each of the matches, but was hoping Powershell's replace operation was going to save me some work.
EDIT-3:
So, as Ansgar correctly and succinctly describes below, there isn't a way (using only the original input string, a regular expression about which you only know the named groups, and the resulting matches) to use the "-replace" operation (or other regex operations) to perform substitutions of the captures of the named groups, while leaving the rest of the original string intact. For this problem, if anybody's curious, I ended up using the solution below. YMMV, other solutions possible. Many thanks to Ansgar for his feedback and options provided.
In the following code block:
$input is a line of text on which substitution is to be performed
$regex is a regular expression (of type [string]) read from a file that has been verified to contain at least one of the supported named groups
$regexToGroupName is a hash table that maps a regex string to an array of group names ordered according to the order of the array returned by [regex]::GetGroupNames(), which matches the left-to-right order in which they appear in the expression
$groupNameToVersionNumber is a hash table that maps a group name to a version number.
Constraints on the named groups within $regex are only (I think) that the expression within the named groups cannot be nested, and should match at most once within the input string.
# This will give us the index and extent of each substring
# that we will be replacing (the parts that we will not keep)
$matchResults = ([regex]$regex).match($input)
# This will hold substrings from $input that were not captured
# by any of the supported named groups, as well as the replacement
# version strings, properly ordered, but will omit substrings captured
# by the named groups
$lineParts = #()
$startingIndex = 0
foreach ($groupName in $regexToGroupName.$regex)
{
# Excise the substring leading up to the match for this group...
$lineParts = $lineParts + $input.Substring($startingIndex, $matchResults.groups[$groupName].Index - $startingIndex)
# Instead of the matched substring, we'll use the substitution
$lineParts = $lineParts + $groupNameToVersionNumber.$groupName
# Set the starting index of the next substring that we will keep...
$startingIndex = $matchResults.groups[$groupName].Index + $matchResults.groups[$groupName].Length
}
# Keep the end of the original string (if there's anything left)
$lineParts = $lineParts + $input.Substring($startingIndex, $input.Length - $startingIndex)
$newLine = ""
foreach ($part in $lineParts)
{
$newLine = $newLine + $part
}
$input= $newLine
Simple Solution
In the scenario where you simply want to replace a version number found somewhere in your $input text, you could simply do this:
$input -replace '(Version\s+)\d+,\d+,\d+,\d+',"`$1$Version1,$Version2,$Version3,$Version4"
Using Named Captures in PowerShell
Regarding your question about named captures, that can be done by using curly brackets. i.e.
'dogcatcher' -replace '(?<pet>dog|cat)','I have a pet ${pet}. '
Gives:
I have a pet dog. I have a pet cat. cher
Issue with multiple captures & solution
You can't replace multiple values in the same replace statement, since the replacement string is used for everything. i.e. if you did this:
'dogcatcher' -replace '(?<pet>dog|cat)|(?<singer>cher)','I have a pet ${pet}. I like ${singer}''s songs. '
You'd get:
I have a pet dog. I like 's songs. I have a pet cat. I like 's songs. I have a pet . I like cher's songs.
...which is probably not what you're hoping for.
Rather, you'd have to do a match per item:
'dogcatcher' -replace '(?<pet>dog|cat)','I have a pet ${pet}. ' -replace '(?<singer>cher)', 'I like ${singer}''s songs. '
...to get:
I have a pet dog. I have a pet cat. I like cher's songs.
More Complex Solution
Bringing this back to your scenario, you're not actually using the captured values; rather you're hoping to replace the spaces they were in with new values. For that, you'd simply want this:
$input = 'I''m running Programmer''s Notepad version 2.4.2.1440, and am a big fan. I also have Chrome v 56.0.2924.87 (64-bit).'
$version1 = 1
$version2 = 3
$version3 = 5
$version4 = 7
$v1Pattern = '(?<=\bv(?:ersion)?\s+)\d+(?=\.\d+\.\d+\.\d+)'
$v2Pattern = '(?<=\bv(?:ersion)?\s+\d+\.)\d+(?=\.\d+\.\d+)'
$v3Pattern = '(?<=\bv(?:ersion)?\s+\d+\.\d+\.)\d+(?=\.\d+)'
$v4Pattern = '(?<=\bv(?:ersion)?\s+\d+\.\d+\.\d+\.)\d+'
$input -replace $v1Pattern, $version1 -replace $v2Pattern, $version2 -replace $v3Pattern,$version3 -replace $v4Pattern,$version4
Which would give:
I'm running Programmer's Notepad version 1.3.5.7, and am a big fan. I also have Chrome v 1.3.5.7 (64-bit).
NB: The above could be written as a 1 liner, but I've broken it down to make it simpler to read.
This takes advantage of regex lookarounds; a way of checking the content before and after the string you're capturing, without including those in the match. i.e. so when we select what to replace we can say "match the number that appears after the word version" without saying "replace the word version".
More info on those here: http://www.regular-expressions.info/lookaround.html
Your Example
Adapting the above to work for your example (i.e. where versions may be separated by commas or dots, and there's no consistency to their format beyond being 4 sets of numbers:
$input = #'
#define SOME_MACRO(4, 1, 0, 0)
Version "1.2.3.4"
SomeStruct vs = { 99,99,99,99 }
'#
$version1 = 1
$version2 = 3
$version3 = 5
$version4 = 7
$v1Pattern = '(?<=\b)\d+(?=\s*[\.,]\s*\d+\s*[\.,]\s*\d+\s*[\.,]\s*\d+\b)'
$v2Pattern = '(?<=\b\d+\s*[\.,]\s*)\d+(?=\s*[\.,]\s*\d+\s*[\.,]\s*\d+\b)'
$v3Pattern = '(?<=\b\d+\s*[\.,]\s*\d+\s*[\.,]\s*)\d+(?=\s*[\.,]\s*\d+\b)'
$v4Pattern = '(?<=\b\d+\s*[\.,]\s*\d+\s*[\.,]\s*\d+\s*[\.,]\s*)\d+\b'
$input -replace $v1Pattern, $version1 -replace $v2Pattern, $version2 -replace $v3Pattern,$version3 -replace $v4Pattern,$version4
Gives:
#define SOME_MACRO(1, 3, 5, 7)
Version "1.3.5.7"
SomeStruct vs = { 1,3,5,7 }
Regular expressions don't work that way, so you can't. Not directly, that is. What you can do (short of using a more appropriate regular expression that groups the parts you want to keep) is to extract the version string and then in a second step replace that substring with the new version string:
$oldver = $input -replace $regexp, '$1,$2,$3,$4'
$newver = $input -replace $oldver, "$Version1,$Version2,$Version3,$Version4"
Edit:
If you don't even know the structure, you must extract that from the regular expression as well.
$version = #($version1, $version2, $version3, $version4)
$input -match $regexp
$oldver = $regexp
$newver = $regexp
for ($i = 1; $i -le 4; $i++) {
$oldver = $oldver -replace "\(\?<version$i>\\d\)", $matches["version$i"]
$newver = $newver -replace "\(\?<version$i>\\d\)", $version[$i-1]
}
$input -replace $oldver, $newver