Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5dcc022cf | |||
| 5a57021e6a |
531
Compiler.ps1
531
Compiler.ps1
@@ -2923,55 +2923,75 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Concat mit einer dynamischen Anzahl von Argumenten
|
# Concat mit einer dynamischen Anzahl von Argumenten
|
||||||
static [string] Concat([string[]]$input) {
|
static [string] Concat([object[]]$input) {
|
||||||
return $input -join ''
|
return (($input | ForEach-Object { [string]$_ }) -join '')
|
||||||
}
|
}
|
||||||
|
|
||||||
# ToLower-Methode
|
# ToLower-Methode
|
||||||
static [string] ToLower([string[]]$input) {
|
static [string] ToLower([object[]]$input) {
|
||||||
return $input[0].ToLower()
|
if ($input.Count -lt 1) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return ([string]$input[0]).ToLowerInvariant()
|
||||||
}
|
}
|
||||||
|
|
||||||
# ToUpper-Methode
|
# ToUpper-Methode
|
||||||
static [string] ToUpper([string[]]$input) {
|
static [string] ToUpper([object[]]$input) {
|
||||||
return $input[0].ToUpper()
|
if ($input.Count -lt 1) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return ([string]$input[0]).ToUpperInvariant()
|
||||||
}
|
}
|
||||||
|
|
||||||
# FirstIndexOf-Methode
|
# FirstIndexOf-Methode
|
||||||
static [string] FirstIndexOf([string[]] $input){
|
static [string] FirstIndexOf([object[]] $input){
|
||||||
return $input[0]
|
if ($input.Count -lt 1) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$input[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
# LastIndexOf-Methode
|
# LastIndexOf-Methode
|
||||||
static [string] LastIndexOf([string[]] $input){
|
static [string] LastIndexOf([object[]] $input){
|
||||||
return $input[-1]
|
if ($input.Count -lt 1) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$input[-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
# IndexOf-Methode
|
# IndexOf-Methode
|
||||||
static [string] IndexOf([string[]] $input){
|
static [string] IndexOf([object[]] $input){
|
||||||
return $input[$input[-1]]
|
if ($input.Count -lt 2) {
|
||||||
|
throw "IndexOf benötigt mindestens zwei Argumente."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$input[[int]$input[-1]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Substring-Methode
|
# Substring-Methode
|
||||||
static [String] Substring([string[]] $input){
|
static [String] Substring([object[]] $input){
|
||||||
if($input.Count -eq 2){
|
if($input.Count -eq 2){
|
||||||
$array = $input.Split(',')
|
return ([string]$input[0]).Substring([int]$input[1])
|
||||||
return $array[0].Substring($array[1])
|
|
||||||
|
|
||||||
}elseif($input.Count -eq 3){
|
}elseif($input.Count -eq 3){
|
||||||
$array = $input.Split(',')
|
return ([string]$input[0]).Substring([int]$input[1],[int]$input[2])
|
||||||
return $array[0].Substring($array[1],$array[2])
|
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
Write-Warning "Zuviele Parameter"
|
throw "Substring erwartet zwei oder drei Argumente."
|
||||||
return $false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace-Methode
|
# Replace-Methode
|
||||||
static [String] Replace([string[]] $input){
|
static [String] Replace([object[]] $input){
|
||||||
$array = $input.Split(',')
|
if ($input.Count -lt 3) {
|
||||||
return $array[0].Replace($array[1],$array[2])
|
throw "Replace erwartet drei Argumente."
|
||||||
|
}
|
||||||
|
|
||||||
|
return ([string]$input[0]).Replace([string]$input[1],[string]$input[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
hidden [System.Collections.Specialized.OrderedDictionary] ResolveDeploymentParameterDefaultValues(){
|
hidden [System.Collections.Specialized.OrderedDictionary] ResolveDeploymentParameterDefaultValues(){
|
||||||
@@ -3025,6 +3045,10 @@
|
|||||||
[ref]$errors
|
[ref]$errors
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
throw "Fehler beim Parsen der Datei: $($errors[0].Message)"
|
||||||
|
}
|
||||||
|
|
||||||
$hashtableAst = $ast.Find({
|
$hashtableAst = $ast.Find({
|
||||||
$args[0] -is [System.Management.Automation.Language.HashtableAst]
|
$args[0] -is [System.Management.Automation.Language.HashtableAst]
|
||||||
}, $true)
|
}, $true)
|
||||||
@@ -3038,176 +3062,108 @@
|
|||||||
return $this.Template
|
return $this.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
hidden [System.Collections.Specialized.OrderedDictionary] ConvertToOrderedHashtable($HashtableAst){
|
hidden [System.Collections.Specialized.OrderedDictionary] ConvertToOrderedHashtable([System.Management.Automation.Language.HashtableAst]$HashtableAst){
|
||||||
|
$ordered = New-Object System.Collections.Specialized.OrderedDictionary
|
||||||
# Verwende OrderedDictionary statt [ordered]@{}
|
|
||||||
$ordered = New-Object System.Collections.Specialized.OrderedDictionary
|
$sortedKvps = $HashtableAst.KeyValuePairs | Sort-Object { $_.Item1.Extent.StartOffset }
|
||||||
|
|
||||||
# Sortiere KeyValuePairs nach ihrer Position im Quelltext
|
foreach ($kvp in $sortedKvps) {
|
||||||
$sortedKvps = $HashtableAst.KeyValuePairs | Sort-Object { $_.Item1.Extent.StartOffset }
|
$key = [string]$this.ConvertAstValue($kvp.Item1)
|
||||||
|
$value = $this.ConvertAstValue($kvp.Item2)
|
||||||
foreach ($kvp in $sortedKvps) {
|
$ordered.Add($key, $value)
|
||||||
# Schlüssel extrahieren
|
}
|
||||||
$key = $kvp.Item1.Extent.Text -replace '[''"]', ''
|
|
||||||
|
return $ordered
|
||||||
# Wert verarbeiten
|
}
|
||||||
$valueAst = $kvp.Item2
|
|
||||||
|
hidden [System.Object] ConvertAstValue([System.Management.Automation.Language.Ast]$Ast) {
|
||||||
# Suche nach HashtableAst in PipelineAst
|
if ($null -eq $Ast) {
|
||||||
$hashtable = $null
|
return $null
|
||||||
if ($valueAst -is [System.Management.Automation.Language.HashtableAst]) {
|
}
|
||||||
$hashtable = $valueAst
|
|
||||||
}
|
if ($Ast -is [System.Management.Automation.Language.PipelineAst]) {
|
||||||
elseif ($valueAst -is [System.Management.Automation.Language.PipelineAst]) {
|
if ($Ast.PipelineElements.Count -eq 1 -and $Ast.PipelineElements[0] -is [System.Management.Automation.Language.CommandExpressionAst]) {
|
||||||
# Suche HashtableAst innerhalb der Pipeline
|
return $this.ConvertAstValue($Ast.PipelineElements[0].Expression)
|
||||||
$hashtable = $valueAst.Find({
|
|
||||||
$args[0] -is [System.Management.Automation.Language.HashtableAst]
|
|
||||||
}, $false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hashtable) {
|
|
||||||
$value = $this.ConvertToOrderedHashtable($hashtable)
|
|
||||||
}
|
|
||||||
elseif ($valueAst -is [System.Management.Automation.Language.ArrayLiteralAst]) {
|
|
||||||
$value = @()
|
|
||||||
foreach ($element in $valueAst.Elements) {
|
|
||||||
if ($element -is [System.Management.Automation.Language.HashtableAst]) {
|
|
||||||
$value += $this.ConvertToOrderedHashtable($element)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$value += Invoke-Expression $element.Extent.Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
$value = Invoke-Expression $valueAst.Extent.Text
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$value = $valueAst.Extent.Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$ordered.Add($key, $value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $ordered
|
return $Ast.SafeGetValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Ast -is [System.Management.Automation.Language.CommandExpressionAst]) {
|
||||||
|
return $this.ConvertAstValue($Ast.Expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Ast -is [System.Management.Automation.Language.ParenExpressionAst]) {
|
||||||
|
return $this.ConvertAstValue($Ast.Pipeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Ast -is [System.Management.Automation.Language.ArrayExpressionAst]) {
|
||||||
|
return $this.ConvertAstValue($Ast.SubExpression)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Ast -is [System.Management.Automation.Language.StatementBlockAst]) {
|
||||||
|
if ($Ast.Statements.Count -eq 1 -and $Ast.Statements[0] -is [System.Management.Automation.Language.PipelineAst]) {
|
||||||
|
return $this.ConvertAstValue($Ast.Statements[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Nicht unterstützter Ausdruck in PSD1-Datei: $($Ast.Extent.Text)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Ast -is [System.Management.Automation.Language.ArrayLiteralAst]) {
|
||||||
|
$result = @()
|
||||||
|
foreach ($element in $Ast.Elements) {
|
||||||
|
$result += $this.ConvertAstValue($element)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Ast -is [System.Management.Automation.Language.HashtableAst]) {
|
||||||
|
return $this.ConvertToOrderedHashtable($Ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Ast -is [System.Management.Automation.Language.VariableExpressionAst]) {
|
||||||
|
switch ($Ast.VariablePath.UserPath) {
|
||||||
|
'true' { return $true }
|
||||||
|
'false' { return $false }
|
||||||
|
'null' { return $null }
|
||||||
|
default { return $Ast.Extent.Text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $Ast.SafeGetValue()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $Ast.Extent.Text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hidden [System.Object] ResolveValue($Value, $Parameters, $Variables){
|
hidden [System.Object] ResolveValue($Value, $Parameters, $Variables){
|
||||||
|
$resolutionStack = [System.Collections.Generic.List[string]]::new()
|
||||||
|
return $this.ResolveValueInternal($Value, $Parameters, $Variables, $resolutionStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [System.Object] ResolveValueInternal($Value, $Parameters, $Variables, [System.Collections.Generic.List[string]]$ResolutionStack){
|
||||||
if ($Value -is [string]) {
|
if ($Value -is [string]) {
|
||||||
# Resolve Parameter-Referenzen: [Parameter('name')]
|
$trimmedValue = $Value.Trim()
|
||||||
while ($Value -match "\[Parameter\('([^']*)'\)\]") {
|
if ($this.IsTemplateExpression($trimmedValue)) {
|
||||||
$paramName = $matches[1]
|
return $this.EvaluateTemplateExpression($trimmedValue.Substring(1, $trimmedValue.Length - 2), $Parameters, $Variables, $ResolutionStack)
|
||||||
if ($Parameters.Contains($paramName)) {
|
|
||||||
|
|
||||||
# Prüfe erst auf Value, dann auf DefaultValue
|
|
||||||
$paramValue = $Parameters[$paramName].Value
|
|
||||||
|
|
||||||
if ($null -ne $paramValue) {
|
|
||||||
$Value = $Value -replace "\[Parameter\('$([regex]::Escape($paramName))'\)\]", $paramValue
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Warning "Parameter '$paramName' nicht gefunden"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Resolve Variable-Referenzen: [Variable('name')]
|
|
||||||
while ($Value -match "\[Variable\('([^']*)'\)\]") {
|
|
||||||
$varName = $matches[1]
|
|
||||||
if ($Variables.Contains($varName)) {
|
|
||||||
$varValue = $this.ResolveValue($Variables[$varName],$Parameters,$Variables)
|
|
||||||
$Value = $Value -replace "\[Variable\('$([regex]::Escape($varName))'\)\]", $varValue
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Warning "Variable '$varName' nicht gefunden"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($Value -match "Parameter\('([^']*)'\)") {
|
|
||||||
$paramName = $matches[1]
|
|
||||||
if ($Parameters.Contains($paramName)) {
|
|
||||||
# Prüfe erst auf Value, dann auf DefaultValue
|
|
||||||
$paramValue = if ($Parameters[$paramName].Value) {
|
|
||||||
$Parameters[$paramName].Value
|
|
||||||
}
|
|
||||||
elseif ($Parameters[$paramName].DefaultValue) {
|
|
||||||
$Parameters[$paramName].DefaultValue
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Warning "Parameter '$paramName' hat weder Value noch DefaultValue"
|
|
||||||
$null
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($null -ne $paramValue) {
|
|
||||||
$Value = $Value -replace "Parameter\('$([regex]::Escape($paramName))'\)", $("'"+$paramValue+"'")
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Warning "Parameter '$paramName' nicht gefunden"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Resolve Variable-Referenzen: Variable('name')
|
return $this.ResolveInlineTemplateExpressions($Value, $Parameters, $Variables, $ResolutionStack)
|
||||||
while ($Value -match "Variable\('([^']*)'\)"){
|
|
||||||
$varName = $matches[1]
|
|
||||||
if ($Variables.Contains($varName)) {
|
|
||||||
$varValue = $this.ResolveValue($Variables[$varName],$Parameters,$Variables)
|
|
||||||
$Value = $Value -replace "Variable\('$([regex]::Escape($varName))'\)", $("'"+$varValue+"'")
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Warning "Variable '$varName' nicht gefunden"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Resolve SSPUtility-Funktionen
|
|
||||||
foreach($Function in $([TemplateBuilder].GetMethods('Static, Public')).Name | Get-Unique) {
|
|
||||||
$pattern = "\["+$Function+"\((.*?)\)\]"
|
|
||||||
while ($Value -match $pattern) {
|
|
||||||
try {
|
|
||||||
$fullMatch = $matches[0]
|
|
||||||
$argsString = $matches[1]
|
|
||||||
|
|
||||||
# Parse die Argumente korrekt - sie sind bereits als PowerShell-Array formatiert
|
|
||||||
# Wir müssen sie nur evaluieren
|
|
||||||
$scriptBlock = [scriptblock]::Create("@($argsString)")
|
|
||||||
$argsArray = & $scriptBlock
|
|
||||||
|
|
||||||
# Rufe die Methode direkt auf statt Invoke-Expression
|
|
||||||
$result = [TemplateBuilder]::$Function($argsArray)
|
|
||||||
$Value = $Value.Replace($fullMatch, $result)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Warning "Fehler beim Ausführen von $matches[0] : $_"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $Value
|
|
||||||
}
|
}
|
||||||
elseif ($Value -is [System.Collections.IDictionary]) {
|
elseif ($Value -is [System.Collections.IDictionary]) {
|
||||||
$resolved = New-Object System.Collections.Specialized.OrderedDictionary
|
$resolved = New-Object System.Collections.Specialized.OrderedDictionary
|
||||||
foreach ($key in $Value.Keys) {
|
foreach ($key in $Value.Keys) {
|
||||||
$resolved[$key] = $this.ResolveValue($Value[$key],$Parameters,$Variables)
|
$resolved[$key] = $this.ResolveValueInternal($Value[$key], $Parameters, $Variables, $ResolutionStack)
|
||||||
}
|
}
|
||||||
return $resolved
|
return $resolved
|
||||||
}
|
}
|
||||||
elseif ($Value -is [array]) {
|
elseif ($Value -is [array]) {
|
||||||
$resolved = @()
|
$resolved = @()
|
||||||
foreach ($item in $Value) {
|
foreach ($item in $Value) {
|
||||||
$resolved += $this.ResolveValue($item,$Parameters,$Variables)
|
$resolved += $this.ResolveValueInternal($item, $Parameters, $Variables, $ResolutionStack)
|
||||||
}
|
}
|
||||||
return $resolved
|
return $resolved
|
||||||
}
|
}
|
||||||
@@ -3215,6 +3171,229 @@
|
|||||||
return $Value
|
return $Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hidden [bool] IsTemplateExpression([string]$Value) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not ($Value.StartsWith('[') -and $Value.EndsWith(']'))) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this.IsFunctionInvocation($Value.Substring(1, $Value.Length - 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [bool] IsFunctionInvocation([string]$Value) {
|
||||||
|
return $Value.Trim() -match '^[A-Za-z_][A-Za-z0-9_]*\s*\(.*\)$'
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [string] ResolveInlineTemplateExpressions([string]$Value, $Parameters, $Variables, [System.Collections.Generic.List[string]]$ResolutionStack) {
|
||||||
|
$resolved = $Value
|
||||||
|
$matches = [regex]::Matches($Value, '\[[^\[\]]+\]')
|
||||||
|
|
||||||
|
for ($i = $matches.Count - 1; $i -ge 0; $i--) {
|
||||||
|
$match = $matches[$i]
|
||||||
|
$expressionText = $match.Value.Substring(1, $match.Value.Length - 2)
|
||||||
|
if (-not $this.IsFunctionInvocation($expressionText)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$replacement = $this.EvaluateTemplateExpression($expressionText, $Parameters, $Variables, $ResolutionStack)
|
||||||
|
$resolved = $resolved.Remove($match.Index, $match.Length).Insert($match.Index, [string]$replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [System.Object] EvaluateTemplateExpression([string]$ExpressionText, $Parameters, $Variables, [System.Collections.Generic.List[string]]$ResolutionStack) {
|
||||||
|
$trimmedExpression = $ExpressionText.Trim()
|
||||||
|
if ($trimmedExpression -notmatch '^([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)$') {
|
||||||
|
return $ExpressionText
|
||||||
|
}
|
||||||
|
|
||||||
|
$functionName = $matches[1]
|
||||||
|
$argumentsText = $matches[2]
|
||||||
|
$resolvedArguments = @()
|
||||||
|
$result = $null
|
||||||
|
|
||||||
|
foreach ($argument in $this.SplitExpressionArguments($argumentsText)) {
|
||||||
|
$resolvedArguments += ,($this.ResolveExpressionToken($argument, $Parameters, $Variables, $ResolutionStack))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($functionName.ToLowerInvariant()) {
|
||||||
|
'parameter' { $result = $this.GetParameterValue([string]$resolvedArguments[0], $Parameters); break }
|
||||||
|
'parameters' { $result = $this.GetParameterValue([string]$resolvedArguments[0], $Parameters); break }
|
||||||
|
'variable' { $result = $this.GetVariableValue([string]$resolvedArguments[0], $Parameters, $Variables, $ResolutionStack); break }
|
||||||
|
'variables' { $result = $this.GetVariableValue([string]$resolvedArguments[0], $Parameters, $Variables, $ResolutionStack); break }
|
||||||
|
'concat' { $result = [TemplateBuilder]::Concat($resolvedArguments); break }
|
||||||
|
'tolower' { $result = [TemplateBuilder]::ToLower($resolvedArguments); break }
|
||||||
|
'toupper' { $result = [TemplateBuilder]::ToUpper($resolvedArguments); break }
|
||||||
|
'firstindexof' { $result = [TemplateBuilder]::FirstIndexOf($resolvedArguments); break }
|
||||||
|
'lastindexof' { $result = [TemplateBuilder]::LastIndexOf($resolvedArguments); break }
|
||||||
|
'indexof' { $result = [TemplateBuilder]::IndexOf($resolvedArguments); break }
|
||||||
|
'substring' { $result = [TemplateBuilder]::Substring($resolvedArguments); break }
|
||||||
|
'replace' { $result = [TemplateBuilder]::Replace($resolvedArguments); break }
|
||||||
|
default {
|
||||||
|
throw "Nicht unterstützte Template-Funktion '$functionName'"
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [string[]] SplitExpressionArguments([string]$ArgumentsText) {
|
||||||
|
$arguments = New-Object 'System.Collections.Generic.List[string]'
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ArgumentsText)) {
|
||||||
|
return $arguments.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder = New-Object System.Text.StringBuilder
|
||||||
|
$depth = 0
|
||||||
|
$inSingleQuote = $false
|
||||||
|
$inDoubleQuote = $false
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $ArgumentsText.Length; $i++) {
|
||||||
|
$char = $ArgumentsText[$i]
|
||||||
|
|
||||||
|
if ($char -eq "'" -and -not $inDoubleQuote) {
|
||||||
|
[void]$builder.Append($char)
|
||||||
|
if ($inSingleQuote -and $i + 1 -lt $ArgumentsText.Length -and $ArgumentsText[$i + 1] -eq "'") {
|
||||||
|
$i++
|
||||||
|
[void]$builder.Append($ArgumentsText[$i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$inSingleQuote = -not $inSingleQuote
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char -eq '"' -and -not $inSingleQuote) {
|
||||||
|
$inDoubleQuote = -not $inDoubleQuote
|
||||||
|
[void]$builder.Append($char)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $inSingleQuote -and -not $inDoubleQuote) {
|
||||||
|
if ($char -eq '(') {
|
||||||
|
$depth++
|
||||||
|
}
|
||||||
|
elseif ($char -eq ')') {
|
||||||
|
$depth--
|
||||||
|
}
|
||||||
|
elseif ($char -eq ',' -and $depth -eq 0) {
|
||||||
|
$arguments.Add($builder.ToString().Trim())
|
||||||
|
[void]$builder.Clear()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[void]$builder.Append($char)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($builder.Length -gt 0 -or $ArgumentsText.Trim().Length -gt 0) {
|
||||||
|
$arguments.Add($builder.ToString().Trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [System.Object] ResolveExpressionToken([string]$Token, $Parameters, $Variables, [System.Collections.Generic.List[string]]$ResolutionStack) {
|
||||||
|
$trimmedToken = $Token.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($trimmedToken)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this.IsTemplateExpression($trimmedToken)) {
|
||||||
|
return $this.EvaluateTemplateExpression($trimmedToken.Substring(1, $trimmedToken.Length - 2), $Parameters, $Variables, $ResolutionStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this.IsFunctionInvocation($trimmedToken)) {
|
||||||
|
return $this.EvaluateTemplateExpression($trimmedToken, $Parameters, $Variables, $ResolutionStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($trimmedToken.StartsWith("'") -and $trimmedToken.EndsWith("'")) -or ($trimmedToken.StartsWith('"') -and $trimmedToken.EndsWith('"'))) {
|
||||||
|
return $this.UnquoteString($trimmedToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trimmedToken -match '^\$?true$') {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trimmedToken -match '^\$?false$') {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trimmedToken -match '^\$?null$') {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trimmedToken -match '^-?\d+$') {
|
||||||
|
return [int]$trimmedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($trimmedToken -match '^-?\d+\.\d+$') {
|
||||||
|
return [double]$trimmedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return $trimmedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [string] UnquoteString([string]$Value) {
|
||||||
|
if ($Value.StartsWith("'") -and $Value.EndsWith("'")) {
|
||||||
|
return ($Value.Substring(1, $Value.Length - 2) -replace "''", "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Value.StartsWith('"') -and $Value.EndsWith('"')) {
|
||||||
|
return $Value.Substring(1, $Value.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Value
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [System.Object] GetParameterValue([string]$ParameterName, $Parameters) {
|
||||||
|
if ($null -eq $Parameters -or -not $Parameters.Contains($ParameterName)) {
|
||||||
|
throw "Parameter '$ParameterName' nicht gefunden"
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameter = $Parameters[$ParameterName]
|
||||||
|
if ($parameter -is [System.Collections.IDictionary]) {
|
||||||
|
if ($parameter.Contains('Value') -and $null -ne $parameter.Value) {
|
||||||
|
return $parameter.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parameter.Contains('DefaultValue')) {
|
||||||
|
return $parameter.DefaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden [System.Object] GetVariableValue([string]$VariableName, $Parameters, $Variables, [System.Collections.Generic.List[string]]$ResolutionStack) {
|
||||||
|
if ($null -eq $Variables -or -not $Variables.Contains($VariableName)) {
|
||||||
|
throw "Variable '$VariableName' nicht gefunden"
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ResolutionStack.Contains($VariableName)) {
|
||||||
|
$cycle = @($ResolutionStack.ToArray()) + $VariableName
|
||||||
|
throw "Zirkuläre Variablenreferenz erkannt: $($cycle -join ' -> ')"
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$ResolutionStack.Add($VariableName)
|
||||||
|
$resolvedValue = $null
|
||||||
|
try {
|
||||||
|
$resolvedValue = $this.ResolveValueInternal($Variables[$VariableName], $Parameters, $Variables, $ResolutionStack)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
[void]$ResolutionStack.Remove($VariableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolvedValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabControlBuilder : BaseComponent{
|
class TabControlBuilder : BaseComponent{
|
||||||
@@ -3811,4 +3990,4 @@
|
|||||||
# Initial alle Button-States aktualisieren
|
# Initial alle Button-States aktualisieren
|
||||||
$stateManager.UpdateAllButtons()
|
$stateManager.UpdateAllButtons()
|
||||||
|
|
||||||
$form.Show()
|
$form.Show()
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
function Import-OrderedPowerShellDataFile {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Path
|
|
||||||
)
|
|
||||||
|
|
||||||
function ConvertTo-OrderedHashtable {
|
|
||||||
param(
|
|
||||||
$HashtableAst
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verwende OrderedDictionary statt [ordered]@{}
|
|
||||||
$ordered = New-Object System.Collections.Specialized.OrderedDictionary
|
|
||||||
|
|
||||||
# Sortiere KeyValuePairs nach ihrer Position im Quelltext
|
|
||||||
$sortedKvps = $HashtableAst.KeyValuePairs | Sort-Object { $_.Item1.Extent.StartOffset }
|
|
||||||
|
|
||||||
foreach ($kvp in $sortedKvps) {
|
|
||||||
# Schlüssel extrahieren
|
|
||||||
$key = $kvp.Item1.Extent.Text -replace '[''"]', ''
|
|
||||||
|
|
||||||
# Wert verarbeiten
|
|
||||||
$valueAst = $kvp.Item2
|
|
||||||
|
|
||||||
# Suche nach HashtableAst in PipelineAst
|
|
||||||
$hashtable = $null
|
|
||||||
if ($valueAst -is [System.Management.Automation.Language.HashtableAst]) {
|
|
||||||
$hashtable = $valueAst
|
|
||||||
}
|
|
||||||
elseif ($valueAst -is [System.Management.Automation.Language.PipelineAst]) {
|
|
||||||
# Suche HashtableAst innerhalb der Pipeline
|
|
||||||
$hashtable = $valueAst.Find({
|
|
||||||
$args[0] -is [System.Management.Automation.Language.HashtableAst]
|
|
||||||
}, $false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($hashtable) {
|
|
||||||
$value = ConvertTo-OrderedHashtable -HashtableAst $hashtable
|
|
||||||
}
|
|
||||||
elseif ($valueAst -is [System.Management.Automation.Language.ArrayLiteralAst]) {
|
|
||||||
$value = @()
|
|
||||||
foreach ($element in $valueAst.Elements) {
|
|
||||||
if ($element -is [System.Management.Automation.Language.HashtableAst]) {
|
|
||||||
$value += ConvertTo-OrderedHashtable -HashtableAst $element
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$value += Invoke-Expression $element.Extent.Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
$value = Invoke-Expression $valueAst.Extent.Text
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$value = $valueAst.Extent.Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$ordered.Add($key, $value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ordered
|
|
||||||
}
|
|
||||||
|
|
||||||
# Datei parsen
|
|
||||||
$errors = $null
|
|
||||||
$tokens = $null
|
|
||||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
|
||||||
$Path,
|
|
||||||
[ref]$tokens,
|
|
||||||
[ref]$errors
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($errors) {
|
|
||||||
throw "Fehler beim Parsen der Datei: $($errors[0].Message)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Finde die oberste Hashtable
|
|
||||||
$hashtableAst = $ast.Find({
|
|
||||||
$args[0] -is [System.Management.Automation.Language.HashtableAst]
|
|
||||||
}, $true)
|
|
||||||
|
|
||||||
if (-not $hashtableAst) {
|
|
||||||
throw "Keine Hashtable in der Datei gefunden"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ConvertTo-OrderedHashtable -HashtableAst $hashtableAst
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,11 @@ function Merge-Templates {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
$TemplateData = Import-OrderedPowerShellDataFile -Path $file -ErrorAction Stop
|
$TemplateData = [TemplateBuilder]::new($file).Template
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
$Errors += "Datei konnte nicht importiert werden"
|
$Errors += "Datei konnte nicht importiert werden"
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
$ConfigurationData = Merge-ConfigurationData -Template $ConfigurationData -Deployment $TemplateData
|
$ConfigurationData = Merge-ConfigurationData -Template $ConfigurationData -Deployment $TemplateData
|
||||||
@@ -40,4 +41,4 @@ function Merge-Templates {
|
|||||||
else {
|
else {
|
||||||
return $ConfigurationData
|
return $ConfigurationData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
185
README.md
Normal file
185
README.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# DSC Configuration Compiler
|
||||||
|
|
||||||
|
Ein WinForms-basiertes PowerShell-Tool zum Laden, Kombinieren, Anzeigen und Exportieren von DSC-nahen Konfigurationen auf Basis von `psd1`-Dateien.
|
||||||
|
|
||||||
|
Der Fokus des Projekts liegt auf einem einfachen Template-Workflow:
|
||||||
|
|
||||||
|
- Templates aus Ordnern laden
|
||||||
|
- mehrere Templates zu einer Zielkonfiguration zusammenfuehren
|
||||||
|
- Parameter und Variablen aufloesen
|
||||||
|
- die resultierende Struktur im TreeView pruefen
|
||||||
|
- die zusammengefuehrte Konfiguration wieder als `psd1` exportieren
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Windows
|
||||||
|
- PowerShell mit WinForms-Unterstuetzung
|
||||||
|
- Schreibzugriff auf `%APPDATA%`
|
||||||
|
|
||||||
|
Das Tool speichert Einstellungen und Logs unterhalb von `%APPDATA%\DSC Tool`.
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
Das Projekt kann direkt ueber `Compiler.ps1` gestartet werden:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\Compiler.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pwsh -File .\Compiler.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
|-- Compiler.ps1
|
||||||
|
|-- Functions/
|
||||||
|
|-- Templates/
|
||||||
|
`-- Deployments/
|
||||||
|
```
|
||||||
|
|
||||||
|
- `Compiler.ps1`: Hauptanwendung, UI, Parser- und Resolver-Logik
|
||||||
|
- `Functions/`: zusaetzliche Funktionen fuer Import, Export, Merge und UI-Aktualisierung
|
||||||
|
- `Templates/`: wiederverwendbare Konfigurationsbausteine
|
||||||
|
- `Deployments/`: exportierte oder vorbereitete Zielkonfigurationen
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
1. Anwendung starten.
|
||||||
|
2. Falls noetig ueber `Einstellungen -> Ordner waehlen...` den Root-Ordner setzen.
|
||||||
|
3. Templates aus den Kategorien laden.
|
||||||
|
4. Gewuenschte Templates per Doppelklick zur Auswahl hinzufuegen.
|
||||||
|
5. Die Auswahl zusammenfuehren.
|
||||||
|
6. Das Ergebnis im TreeView pruefen.
|
||||||
|
7. Die zusammengefuehrte Konfiguration als `psd1` exportieren.
|
||||||
|
|
||||||
|
Import und Export werden ueber den Deployment-Ordner abgewickelt.
|
||||||
|
|
||||||
|
## Template-Modell
|
||||||
|
|
||||||
|
Templates bestehen aus drei optionalen Hauptbereichen:
|
||||||
|
|
||||||
|
- `Parameters`
|
||||||
|
- `Variables`
|
||||||
|
- `Resources`
|
||||||
|
|
||||||
|
Ein vereinfachtes Beispiel:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
@{
|
||||||
|
Parameters = @{
|
||||||
|
DatabasePrefix = @{
|
||||||
|
Type = 'string'
|
||||||
|
DefaultValue = 'SP'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variables = @{
|
||||||
|
ConfigDbName = "[concat(parameters('DatabasePrefix'),'_','Farm_Config')]"
|
||||||
|
}
|
||||||
|
|
||||||
|
Resources = @{
|
||||||
|
NonNodeData = @{
|
||||||
|
Services = @{
|
||||||
|
Example = @{
|
||||||
|
DatabaseName = "[variables('ConfigDbName')]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ausdruckssyntax
|
||||||
|
|
||||||
|
Die Auswertung ist ARM-inspiriert. Template-Ausdruecke stehen in eckigen Klammern:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
"[concat(parameters('Prefix'),'_','Database')]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Unterstuetzte Referenzen:
|
||||||
|
|
||||||
|
- `parameters('Name')`
|
||||||
|
- `variables('Name')`
|
||||||
|
|
||||||
|
Zusaetzlich werden aus Rueckwaertskompatibilitaet auch diese Varianten verstanden:
|
||||||
|
|
||||||
|
- `Parameter('Name')`
|
||||||
|
- `Variable('Name')`
|
||||||
|
|
||||||
|
Unterstuetzte Funktionen:
|
||||||
|
|
||||||
|
- `concat(...)`
|
||||||
|
- `toLower(...)`
|
||||||
|
- `toUpper(...)`
|
||||||
|
- `substring(...)`
|
||||||
|
- `replace(...)`
|
||||||
|
- `indexOf(...)`
|
||||||
|
- `firstIndexOf(...)`
|
||||||
|
- `lastIndexOf(...)`
|
||||||
|
|
||||||
|
Funktionsnamen werden ohne freie PowerShell-Ausfuehrung ausgewertet. Die `psd1`-Dateien werden als Daten geparst, nicht als beliebiger Script-Code interpretiert.
|
||||||
|
|
||||||
|
## Aufloesungsreihenfolge
|
||||||
|
|
||||||
|
Die Aufloesung erfolgt in dieser Reihenfolge:
|
||||||
|
|
||||||
|
1. Parameterwerte und Defaultwerte
|
||||||
|
2. Variablen
|
||||||
|
3. Ressourcenwerte
|
||||||
|
|
||||||
|
Zirkulaere Variablenreferenzen werden erkannt und als Fehler behandelt.
|
||||||
|
|
||||||
|
## Merge-Verhalten
|
||||||
|
|
||||||
|
Beim Zusammenfuehren mehrerer Templates werden vorhandene Eintraege rekursiv kombiniert:
|
||||||
|
|
||||||
|
- Hashtables werden rekursiv gemerged
|
||||||
|
- Arrays werden anhand passender Name-Felder zusammengefuehrt
|
||||||
|
- fehlende Werte aus Templates werden in das Ergebnis uebernommen
|
||||||
|
|
||||||
|
Die finale Anzeige im TreeView basiert auf dem zusammengefuehrten und bereits aufgeloesten Modell.
|
||||||
|
|
||||||
|
## Export
|
||||||
|
|
||||||
|
Der Export schreibt die aktuelle TreeView-Struktur wieder in eine `psd1`-Datei.
|
||||||
|
|
||||||
|
Standardziel beim Speichern ist:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Deployments\merged_config.psd1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- Einstellungen werden benutzerbezogen gespeichert.
|
||||||
|
- Logs werden pro Tag unter `%APPDATA%\DSC Tool\Logs` geschrieben.
|
||||||
|
- Templates sollten deklarativ bleiben und nur Daten plus Template-Ausdruecke enthalten.
|
||||||
|
- Fuer neue Templates wird die ARM-aehnliche Syntax mit `parameters()` und `variables()` empfohlen.
|
||||||
|
|
||||||
|
## Beispielinhalt
|
||||||
|
|
||||||
|
Im Repository ist bereits ein groesseres Beispiel enthalten:
|
||||||
|
|
||||||
|
- `Templates\Service\SharePointServer.psd1`
|
||||||
|
|
||||||
|
Dieses Template zeigt die Verwendung von:
|
||||||
|
|
||||||
|
- Parametern
|
||||||
|
- Variablen
|
||||||
|
- verschachtelten Ressourcen
|
||||||
|
- String-Funktionen wie `concat()` und `substring()`
|
||||||
|
|
||||||
|
## Weiterentwicklung
|
||||||
|
|
||||||
|
Sinnvolle naechste Schritte fuer das Projekt sind:
|
||||||
|
|
||||||
|
- Tests fuer Parser, Resolver und Merge-Verhalten
|
||||||
|
- typsicherer Export-Roundtrip
|
||||||
|
- robustere Initialisierung der Pfade und Settings
|
||||||
|
- weitere Template-Funktionen nach Bedarf
|
||||||
Reference in New Issue
Block a user