Вступление
Не так давно передо мной возникла задача рендерить из PowerShell скрипта различные HTML отчеты для дальнейшей отсылки по e-mail. Поиск готовых решений дал не очень много. Кто-то подключает Razor, кто-то свои самописные сложноватые велосипеды движки.
Скромный список требований был такой:
- Код вьюх должен быть в отдельных файлах.
- Внутри вьюх должна быть поддержка вложенности, и вставок кода на PowerShell.
- Должен работать на любых хостах с PowerShell 2.0 без дополнительных настроек.
Так как ничего подобного не удалось найти, то был реализован простой (и одновременно мощный) движок рендеринга вьюх в стиле классического Asp.
Подробности реализации
Изучая вопрос (как и сам PowerShell) я обратил внимание на синтаксис вычисления PowerShell выражений внутри строк. Например выражение "<div> $($env:COMPUTERNAME)</div>"
будет во время выполнения интерпретировано и на выходе мы получим что-то вроде <div>MYCOMPUTER</div>
.
Это уже собственно и есть темплейтинг в простейшем виде. Он позволяет рендерить достаточно сложные вьюхи:
$Model = @{}
$Model.Title = 'Hello, this is a test'
$Model.Clients = @('Ivan', 'Sergiy', 'John')
$html = "<h1>
$($Model.Title)
</h1>
<div class=""test"">
<ul>
$( foreach($client in $Model.Clients) {"
<li>
$( $client )
</li>
"})
</ul>
</div>"
$html
Как видим, парсер PowerShell позволяет использовать вложенные вставки кода заключенного в $() в строки, это очень удобно для реализации ветвлений и циклов.
Данный метод уже можно использовать для небольших задач, хотя есть и недостатки:
- Код вьюхи содержится в коде скрипта, а не в отдельном файле.
- Нет возможности использовать вложенные вьюхи.
- Синтаксис немного не совсем нагляден, и часто из-за пропущенной скобки или кавычки приходится напряженно все проверять.
- Необходимо в текстовых вставках кодировать двойную кавычку
"
как""
.
Первые два недостатка решаются довольно просто – темплейт перемещается в отдельный файл в подпапку Views, и пишется функция для рендера модели:
function RenderViewNativePowerShell(
[Parameter(Mandatory=$true)][string] $viewName,
[Parameter(Mandatory=$true)][Object] $model
)
{
$viewFileName = Resolve-Path ('Views' + $viewName)
$templateContent = Get-Content $viewFileName | Out-String
return $ExecutionContext.InvokeCommand.ExpandString('"' + $templateContent + '"')
}
После чего ее можно вызывать так:
RenderViewNativePowerShell 'Test_ps.html' $Model
При этом поддерживаются вложенные вьюхи. Вот так выглядит код test_ps.html:
$( RenderViewNativePowerShell 'header_ps.html' $Model )
<div class=""test"">
<ul>
$( foreach($client in $Model.Clients) {"
<li>
$( $client )
</li>
"})
</ul>
</div>
Кому-то этого может показаться достаточно, но я решил побороть оставшиеся недостатки – перейти на использование скобок ASP <%...%>, так как этот синтаксис поддерживается во многих текстовых редакторах, и верстка страницы выглядит намного читабельнее.
Итак, основная идея реализации довольно проста: взять и заменить все скобки <%...%> на их PowerShell эквиваленты $(…). Некоторая сложность состояла в том, что замена должна быть неоднозначной, чтобы учитывать вложенные вьюхи, так как они должны быть в “…” блоках.
После некоторых мучений возникла такая функция:
function RenderView(
[Parameter(Mandatory=$true)][string] $viewName,
[Parameter(Mandatory=$true)][Object] $model
)
{
$viewFileName = Resolve-Path ("Views" + $viewName)
$templateContent = Get-Content $viewFileName | Out-String
$rx = New-Object System.Text.RegularExpressions.Regex('(<%.*?%>)', [System.Text.RegularExpressions.RegexOptions]::Singleline)
$res = @()
$splitted = $rx.split($templateContent);
foreach($part in $splitted)
{
if ($part.StartsWith('<%') -and $part.EndsWith('%>')) #transform <%...%> blocks
{
$expr = $part.Substring(2, $part.Length-4) #remove <%%> quotes
$normExpr = $expr.Replace('`n','').Replace('`r','').Trim();
$startClosure = '$('
$endClosure = ')'
if ($normExpr.endswith('{')) {
$endClosure = '"'
}
if ($normExpr.startsWith('}')) {
$startClosure = '"'
}
$res += @($startClosure + $expr + $endClosure)
}
else #encode text blocks
{
$expr = $part.Replace('"', '""');
$res += @($expr)
}
}
$viewExpr = $res -join ''
return $ExecutionContext.InvokeCommand.ExpandString('"' + $viewExpr + '"')
}
Кроме требуемой замены <%%> на их PowerShell эквиваленты также выполняется замена “ на “” в текстовых блоках.
В итоге наша вьюха выглядит довольно неплохо в Visual Studio:
В заключение остается отметить, что исходный код с некоторыми тестами и примерами выложен на GitHub.
Автор: megaboich