问题
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
}
})
回答1:
A couple things:
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-Expressionto 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,$tokensand 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:
/*
Author: John Doe
Analyst: Jane Doe
Request: Lorem ipsum dolor sit amet [9999]
3PTY: A
Name:x
*/
回答2:
You can just reference the element with dot notation
$tokens.author.email
Then you could do things like this as well if you wanted to check if the name was empty for example. Note that there is a caveat: Author should exist for this to work exactly as intended.)
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 briantist
$tokens['Author']['Email']
Dynamic replacement
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 more 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.
Lets get freaky
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
Something else
This borrows from mike z 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 something. For every match replace that text with its dot notation equivalent.
Output from either samples should match what you have for Should become:
来源:https://stackoverflow.com/questions/29039940/hashtable-key-syntax-to-refer-to-embedded-hashtable-element