
Question:
Assuming that I have a hashtable:
$tokens = @{
Id=9999;
Title="Lorem ipsum dolor sit amet";
Author=@{Name="John Doe"; Email='john.doe@foo.xyz'};
Analyst=@{Name="Jane Doe"; Email='jane.doe@foo.xyz'}
}
And a template that I would like to populate, replacing the tokens (e.g. __Title__
) with the corresponding hashtable's value:
/*
Author: __Author.Name__ <__Author.Email__>
Analyst: __Analyst.Name__ <__Analyst.Email__>
Request: __Title__ [__Id__]
*/
...
Should become:
/*
Author: John Doe <john.doe@foo.xyz>
Analyst: Jane Doe <jane.doe@foo.xyz>
Request: Lorem ipsum dolor sit amet [9999]
*/
Is there a way to refer to an embedded hashtable's elements in the 'parent' hashtable? $tokens['Author.Email']
, for example, doesn't work.
The code:
...
return [regex]::Replace( $template, '__(?<tokenName>\w+)__', {
# __TOKEN__
param($match)
$tokenName = $match.Groups['tokenName'].Value
if ($tokens[$tokenName]) {
# matching token returns value from hashtable;
works for simple keys `$tokens['Title']`, not complex keys `$tokens['Author.Name']`
return $tokens[$tokenName]
}
else {
# non-matching token returns token
return $match
}
})
Answer1:A couple things:
<ol><li>You need to fix the regular expression to actually match the nested properties. Right now it doesn't you need it to be __(?<tokenName>[\w\.]+)__
Use Invoke-Expression
to dynamically expand the nested properties. Just build a string the represents the expression you want to evaluate. This is good because it doesn't rely on the model objects, $tokens
and its properties, being hashtables at all. All it needs is for the properties to resolve on the objects that are there.
A short example is below. Note: if the template is coming from an unsecure source, be careful with this and sanitize the input first:
$tokens = @{
Id=9999;
Title="Lorem ipsum dolor sit amet";
Author=@{Name="John Doe"; Email='john.doe@foo.xyz'};
Analyst=@{Name="Jane Doe"; Email='jane.doe@foo.xyz'};
'3PTY' = "A";
Test=@{'Name with space' = 'x' }
}
$template = @"
/*
Author: __Author.Name__ <__Author.Email__>
Analyst: __Analyst.Name__ <__Analyst.Email__>
Request: __Title__ [__Id__]
3PTY: __"3PTY"__
Name:__Test.'Name with space'__
*/
"@
function Replace-Template {
param ([string]$template, $model)
[regex]::Replace( $template, '__(?<tokenName>[\w .\''\"]+)__', {
# __TOKEN__
# Note that TOKEN should be a valid PS property name. It may need to be enclosed in quotes
# if it starts with a number or has spaces in the name. See the example above for usage.
param($match)
$tokenName = $match.Groups['tokenName'].Value
Write-Verbose "Replacing '$tokenName'"
$tokenValue = Invoke-Expression "`$model.$tokenName" -ErrorAction SilentlyContinue
if ($tokenValue) {
# there was a value. return it.
return $tokenValue
}
else {
# non-matching token returns token
return $match
}
})
}
Replace-Template $template $tokens
Output:
<blockquote>/*<br /> Author: John Doe <br /> Analyst: Jane Doe <br /> Request: Lorem ipsum dolor sit amet [9999]<br /> 3PTY: A<br /> Name:x<br /> */
</blockquote> Answer2:You can just reference the element with dot notation
<pre class="lang-powershell prettyprint-override">$tokens.author.email
Then you could do things like this as well if you wanted to check if the name was empty for example. <strong>Note</strong> that there is a caveat: Author should exist for this to work exactly as intended.)
<pre class="lang-powershell prettyprint-override">If(!$tokens.author.name){$tokens.author.name = "Awesome Sauce"; }
Write-Host ("Author Name: {0}" -f $tokens.author.name)
You can also use hashtable notation as suggested by <a href="https://stackoverflow.com/users/3905079/briantist" rel="nofollow">briantist</a>
<pre class="lang-powershell prettyprint-override">$tokens['Author']['Email']
<strong>Dynamic replacement</strong>
You use the word dynamic but I am not sure how far you want to take that. For now lets assume that the $tokens
elements all exist and we are going to replace the text from a here-string.
$text = @"
/*
Author: __Author.Name__ <__Author.Email__>
Analyst: __Analyst.Name__ <__Analyst.Email__>
Request: __Title__ [__Id__]
*/
"@
$text -replace "__Author\.Name__",$tokens.Author.Name -replace "__Author\.Email__",$tokens.Author.Email `
-replace "__Analyst\.Name__",$tokens.Analyst.Name -replace "__Analyst\.Email__",$tokens.Analyst.Email `
-replace "__Title__",$tokens.Title -replace "__Id__",$tokens.Id
But I feel you mean <em>more</em> dynamic since all of this requires knowing information about the $Tokens
and the the source string. Let me know how we stand now. We could get deeper with this.
<strong>Lets get freaky</strong>
Let say you know that the hashtable $tokens
and the source $text
have values in common but you don't know the names of them. This will dynamically populate text based on the key names on the hashtables. Currently this only works if there is only one hashtable depth.
ForEach($childKey in $tokens.Keys){
If($tokens[$childKey] -is [System.Collections.Hashtable]){
ForEach($grandChildKey in $tokens[$childKey].Keys){
Write-Host "GrandChildKey = $childKey"
$text = $text -replace "__$childKey\.$($grandChildKey)__", $tokens.$childKey.$grandChildKey
}
} Else {
$text = $text -replace "__$($childKey)__", $tokens.$childKey
}
}
$text
<strong>Something else</strong>
This borrows from <a href="https://stackoverflow.com/users/517852/mike-z" rel="nofollow">mike z</a> suggestion about Invoke-Expression
as it makes less guess work involved.
$output = $text
$placeHolders = $text | Select-String '__([\w.]+)__' -AllMatches | ForEach-Object{$_.matches} | ForEach-Object{$_.Value}
$placeHolders.count
$placeHolders | ForEach-Object {
$output = $output -replace [regex]::Escape($_), (Invoke-Expression "`$tokens.$($_ -replace "_")")
}
$output
Search the $text
for all strings like <strong>something</strong>. For every match replace that text with its dot notation equivalent.
Output from either samples should match what you have for <em>Should become:</em>