PowerConfig.psm1
using namespace Microsoft.Extensions.Configuration using namespace Microsoft.Extensions.Configuration.Memory using namespace System.Collections using namespace System.Collections.Generic #PSES on Windows 5.1 is currently unsupported try { if ([Microsoft.Extensions.Configuration.ConfigurationBuilder].Assembly.Location -match 'PowershellEditorServices') { throw [NotSupportedException]'Sorry, PowerConfig is currently not supported if Powershell Editor Services is loaded on Windows Powershell due to a conflict. See: https://github.com/PowerShell/PowerShellEditorServices/issues/1499' } } catch { if ($PSItem.FullyQualifiedErrorId -ne 'TypeNotFound') {throw} } $libroot = Resolve-Path "$PSScriptRoot/lib" #If this is a "debug build", use the assemblies from buildoutput if (Test-Path "$PSScriptRoot/../BuildOutput/PowerConfig/lib") { $libroot = Resolve-Path "$PSScriptRoot/../BuildOutput/PowerConfig/lib" } $libPath = Resolve-Path $( if ($PSEdition -eq 'Desktop') { "$libroot/winps" } else { "$libroot/pwsh" } ) Write-Verbose "Loading PowerConfig Assemblies from $libPath" Add-Type -Path "$libPath/*.dll" try { Update-TypeData -Erroraction Stop -TypeName Microsoft.Extensions.Configuration.ConfigurationBuilder -MemberName AddYamlFile -MemberType ScriptMethod -Value { param([String]$Path) [Microsoft.Extensions.Configuration.YamlConfigurationExtensions]::AddYamlFile($this, $Path) } } catch { if ([String]$PSItem -match 'The member .+ is already present') { Write-Verbose "Extension Method already present" $return } #Write-Error $PSItem.exception } try { Update-TypeData -Erroraction Stop -TypeName Microsoft.Extensions.Configuration.ConfigurationBuilder -MemberName AddJsonFile -MemberType ScriptMethod -Value { param([String]$Path) [Microsoft.Extensions.Configuration.JsonConfigurationExtensions]::AddJsonFile($this, $Path) } } catch { if ([String]$PSItem -match 'The member .+ is already present') { Write-Verbose "Extension Method already present" $return } #Write-Error $PSItem.exception } #Taken with love from https://github.com/austoonz/Convert/blob/master/src/Convert/Public/ConvertFrom-StringToMemoryStream.ps1 function ConvertFrom-StringToMemoryStream { <# .SYNOPSIS Converts a string to a MemoryStream object. .DESCRIPTION Converts a string to a MemoryStream object. .PARAMETER String A string object for conversion. .PARAMETER Encoding The encoding to use for conversion. Defaults to UTF8. Valid options are ASCII, BigEndianUnicode, Default, Unicode, UTF32, UTF7, and UTF8. .PARAMETER Compress If supplied, the output will be compressed using Gzip. .EXAMPLE $stream = ConvertFrom-StringToMemoryStream -String 'A string' $stream.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True MemoryStream System.IO.Stream .EXAMPLE $stream = 'A string' | ConvertFrom-StringToMemoryStream $stream.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True MemoryStream System.IO.Stream .EXAMPLE $streams = ConvertFrom-StringToMemoryStream -String 'A string','Another string' $streams.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array $streams[0].GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True MemoryStream System.IO.Stream .EXAMPLE $streams = 'A string','Another string' | ConvertFrom-StringToMemoryStream $streams.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array $streams[0].GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True MemoryStream System.IO.Stream .EXAMPLE $stream = ConvertFrom-StringToMemoryStream -String 'This string has two string values' $stream.Length 33 $stream = ConvertFrom-StringToMemoryStream -String 'This string has two string values' -Compress $stream.Length 10 .OUTPUTS [System.IO.MemoryStream[]] .LINK http://convert.readthedocs.io/en/latest/functions/ConvertFrom-StringToMemoryStream/ #> [CmdletBinding(HelpUri = 'http://convert.readthedocs.io/en/latest/functions/ConvertFrom-StringToMemoryStream/')] param ( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [String[]] $String, [ValidateSet('ASCII', 'BigEndianUnicode', 'Default', 'Unicode', 'UTF32', 'UTF7', 'UTF8')] [String] $Encoding = 'UTF8', [Switch] $Compress ) begin { $userErrorActionPreference = $ErrorActionPreference } process { foreach ($s in $String) { try { [System.IO.MemoryStream]$stream = [System.IO.MemoryStream]::new() if ($Compress) { $byteArray = [System.Text.Encoding]::$Encoding.GetBytes($s) $gzipStream = [System.IO.Compression.GzipStream]::new($stream, ([IO.Compression.CompressionMode]::Compress)) $gzipStream.Write( $byteArray, 0, $byteArray.Length ) } else { $writer = [System.IO.StreamWriter]::new($stream) $writer.Write($s) $writer.Flush() } $stream } catch { Write-Error -ErrorRecord $_ -ErrorAction $userErrorActionPreference } } } } function ConvertTo-Dictionary { [CmdletBinding()] param ( [System.Collections.HashTable]$Hashtable ) #Make a string dictionary that the memorycollection requires $dictionary = [System.Collections.Generic.Dictionary[String,String]]::new() #Take the hashtable values and import them into the dictionary $hashtable.keys.foreach{ $null = $Dictionary.Add($PSItem,$HashTable[$PSItem]) } return $dictionary } #Big Thanks to IISResetMe: https://gist.github.com/IISResetMe/2fdb0c7097545b4c86ddf60fe7fb5056#file-flatten-ps1-L5 function ConvertTo-FlatDictionary { [CmdletBinding()] param( [IDictionary]$Dictionary, [string]$KeyDelimiter = ':' ) $newDict = @{} $stackOfTrees = [Stack]::new() foreach($kvp in $Dictionary.GetEnumerator()){ $stackOfTrees.Push(@($kvp.Key,$kvp.Value)) } while($stackOfTrees.Count -gt 0) { $prefix,$next = $stackOfTrees.Pop() if($next -is [IDictionary]){ foreach($kvp in $next.GetEnumerator()) { $stackOfTrees.Push(@("${prefix}${KeyDelimiter}$($kvp.Key)", $kvp.Value)) } } else { $newDict["${prefix}"] = $next } } return $newDict } <# .SYNOPSIS Takes an enumerable keyvaluepair from Microsoft.Extensions.Configuration and converts it to a nested hashtable #> function ConvertTo-NestedHashTable { [CmdletBinding()] param ( [Collections.Generic.KeyValuePair[String,String][]]$InputObject ) #First group the entries by hierarchy $depthGroups = $InputObject | Group-Object { $PSItem.key.split(':').count } $result = [ordered]@{} foreach ($DepthItem in ($DepthGroups | Sort-Object Name)) { foreach ($ConfigItem in ($DepthItem.Group)) { $ConfigItemLevels = $ConfigItem.key.split(':') #Iterate through the levels and create them if not already present $lastLevel = $result For ($i=0;$i -lt ($ConfigItemLevels.count -1);$i++) { if ($lastLevel[$ConfigItemLevels[$i]] -isnot [System.Collections.Specialized.OrderedDictionary]) { $lastLevel[$ConfigItemLevels[$i]] = [ordered]@{} } #Step up to the new level for the next activity $lastLevel = $lastLevel[$ConfigItemLevels[$i]] } #Assign the value now that the levels have been created $valueKey = $ConfigItemLevels[($ConfigItemLevels.count -1)] $lastLevel.$valueKey = $ConfigItem.Value } #Emit the result before foreach cleanup } return $result } function Add-PowerConfigCommandLineSource { [CmdletBinding(PositionalBinding=$false)] param ( #The PowerConfig object to operate on [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject, # A hashtable that remaps arguments to their intented destination, for instance @{'-f'='force'} remaps the shorthand -f to the force key [HashTable]$ArgumentMap, #The arguments that were passed to your script. You can pass the arguments directly to this script, or supply them as a variable similar to $args (an array of strings, one statement per string) [Parameter(Mandatory,ValueFromRemainingArguments)]$ArgumentList ) #Couldn't cast a hashtable directly because it was seeing it as new properties, so here is a workaround $ArgumentMapDictionary = [Collections.Generic.Dictionary[String,String]]::new() $ArgumentMap.keys.foreach{ $ArgumentMapDictionary[$PSItem] = $ArgumentMap[$PSItem] } [CommandLineConfigurationExtensions]::AddCommandLine($InputObject, $ArgumentList, $ArgumentMapDictionary) } function Add-PowerConfigEnvironmentVariableSource { [CmdletBinding()] param ( #The PowerConfig object to operate on [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject, #The prefix for your environment variables. Default is no prefix [String]$Prefix = '' ) [EnvironmentVariablesExtensions]::AddEnvironmentVariables($InputObject, $Prefix) } # Powershell 5.1 using namespace doesn't work with classes unfortunately class HashTableConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider { hidden [HashTableConfigurationSource]$source HashTableConfigurationProvider ($source) { $flatHashTable = ConvertTo-FlatDictionary $source.hashtable $flatHashTable.GetEnumerator().Foreach{ $this.Set($PSItem.Name, $PSItem.Value) } } } class HashTableConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource { #The hashtable reference that will be used for the memoryconfigsource [hashtable]$hashtable HashTableConfigurationSource ([hashtable]$hashtable) { $this.hashtable = $hashtable } [Microsoft.Extensions.Configuration.IConfigurationProvider] Build([Microsoft.Extensions.Configuration.IConfigurationBuilder]$builder) { return [HashTableConfigurationProvider]::new($this) } } function Add-PowerConfigHashTable { [CmdletBinding()] param ( #The PowerConfig object to operate on [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject, #The hashtable to add to your configuration values. Use colons (:) to separate sections of configuration. [Parameter(Mandatory,Position=0)][Hashtable]$Object ) $InputObject.Add( [HashTableConfigurationSource]::new($Object) ) } function Add-PowerConfigJsonSource { [CmdletBinding()] param ( #The PowerConfig object to operate on [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject, #The prefix for your environment variables. Default is no prefix [Parameter(Mandatory)]$Path, #Specify this parameter if the configuration file is mandatory. PowerConfig will show an error if this file is not present. [Switch]$Mandatory, #By default, if the file changes the configuration will automatically be updated. If you want to disable this behavior, specify this parameter. [Switch]$NoRefresh ) [JsonConfigurationExtensions]::AddJsonFile($InputObject, $Path, !$Mandatory, !$NoRefresh) } function Add-PowerConfigObject { [CmdletBinding()] param ( #The PowerConfig object to operate on [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject, #The hashtable to add to your configuration values. Use colons (:) to separate sections of configuration [Parameter(Mandatory)][Object]$Object, #How deep to go on nested properties. You should normally not touch this and instead filter your inputs first $Depth = 5, #Optional path to save the converted Json. This is normally a temporary file and you shouldn't need to change this. $JsonTempFile = [io.path]::GetTempFileName() ) $WarningPreference = 'SilentlyContinue' $ObjectJson = $Object | ConvertTo-Json -Compress -ErrorAction Stop | Out-File -FilePath $JsonTempFile [JsonConfigurationExtensions]::AddJsonFile($InputObject,$JsonTempFile) #TODO: Use the stream method when we can bump to Configuration Extensions 3.0 #$JsonStream = ConvertFrom-StringToMemoryStream $ObjectJson #[JsonConfigurationExtensions]::AddJsonStream($InputObject,$JsonStream) } function Add-PowerConfigTomlSource { [CmdletBinding()] param ( #The PowerConfig object to operate on [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject, #The prefix for your environment variables. Default is no prefix [Parameter(Mandatory)]$Path, #Specify this parameter if the configuration file is mandatory. PowerConfig will show an error if this file is not present. [Switch]$Mandatory, #By default, if the file changes the configuration will automatically be updated. If you want to disable this behavior, specify this parameter. [Switch]$NoRefresh ) [TomlConfigurationExtensions]::AddTomlFile($InputObject, $Path, !$Mandatory, !$NoRefresh) } function Add-PowerConfigYamlSource { [CmdletBinding()] param ( #The PowerConfig object to operate on [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject, #The prefix for your environment variables. Default is no prefix [Parameter(Mandatory)]$Path, #Specify this parameter if the configuration file is mandatory. PowerConfig will show an error if this file is not present. [Switch]$Mandatory, #By default, if the file changes the configuration will automatically be updated. If you want to disable this behavior, specify this parameter. [Switch]$NoRefresh ) [YamlConfigurationExtensions]::AddYamlFile($InputObject, $Path, !$Mandatory, !$NoRefresh) } function Get-PowerConfig { param ( [Microsoft.Extensions.Configuration.ConfigurationBuilder][Parameter(Mandatory,ValueFromPipeline)]$InputObject ) $RenderedPowerConfig = $InputObject.build() ConvertTo-NestedHashTable ([ConfigurationExtensions]::AsEnumerable($RenderedPowerConfig)) } <# .SYNOPSIS Create a new Powerconfig Object #> function New-PowerConfig { [CmdletBinding()] param() [ConfigurationBuilder]::new() } #This is needed because assemblies must be loaded before classes that reference them in Windows Powershell 5.1 #It should be referenced from "ScriptsToProcess" in the manifest file <# .SYNOPSIS Used to add automatic binding redirection to related modules for Powerconfig to redirect CompilerServices to the net5.0 assembly version .NOTES CompilerServices.Unsafe won't load without this Reference: https://github.com/PowerShell/PowerShellStandard/issues/72 #> if ($PSEdition -ne 'Desktop') { $bindingRedirectHandler = [ResolveEventHandler] { param($sender, $assembly) try { Write-Debug "BindingRedirectHandler: Resolving $($assembly.name)" #Skip Powershell Assemblies if ($assembly.name -like '*Management.Automation*') { return $null } $assemblyShortName = $assembly.name.split(',')[0] $matchingAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object fullname -Match ('^' + [Regex]::Escape($assemblyShortName)) if ($matchingAssembly.count -eq 1) { Write-Debug "BindingRedirectHandler: Redirecting $($assembly.name) to $($matchingAssembly.Location)" return $MatchingAssembly } } catch { #Write-Error will blackhole, which is why write-host is required. This should never occur so it should be a red flag Write-Host -fore red "BindingRedirectHandler ERROR: $PSITEM" return $null } return $null } [Appdomain]::CurrentDomain.Add_AssemblyResolve($bindingRedirectHandler) } $libroot = "$PSScriptRoot/../lib" #If this is a "debug build", use the assemblies from buildoutput $debugLibPath = "$PSScriptRoot/../../BuildOutput/PowerConfig/lib" if (Test-Path $debugLibPath) { $libroot = Resolve-Path $debugLibPath } $libPath = Resolve-Path $( if ($PSEdition -eq 'Desktop') { "$libroot/winps" } else { "$libroot/pwsh" } ) Write-Verbose "Loading PowerConfig Assemblies from $libPath" Add-Type -Path "$libPath/*.dll" # if ('AddYamlFile' -notin (get-typedata "Microsoft.Extensions.Configuration.ConfigurationBuilder").members.keys) { # Update-TypeData -TypeName Microsoft.Extensions.Configuration.ConfigurationBuilder -MemberName AddYamlFile -MemberType ScriptMethod -Value { # param([String]$Path) # [Microsoft.Extensions.Configuration.YamlConfigurationExtensions]::AddYamlFile($this, $Path) # } # } |