Commands/ModuleMember/Import-ModuleMember.ps1
function Import-ModuleMember { <# .SYNOPSIS Imports members into a module .DESCRIPTION Imports members from an object into a module. A few types of members can easily be turned into commands: * A ScriptMethod with named blocks * A Property with a ScriptBlock value * A Property with a relative path Each of these can easily be turned into a function or alias. .EXAMPLE $importedMembers = [PSCustomObject]@{ "Did you know you PowerShell can have commands with spaces" = { "It's a pretty unique feature of the PowerShell language" } } | Import-ModuleMember -PassThru $importedMembers # Should -BeOfType ([Management.Automation.PSModuleInfo]) & "Did you know you PowerShell can have commands with spaces" # Should -BeLike '*PowerShell*' #> [CmdletBinding(PositionalBinding=$false)] [Reflection.AssemblyMetadata("HelpOut.TellStory", $true)] [Reflection.AssemblyMetadata("HelpOut.Story.Process", "For Each Input")] param( # The Source of additional module members [Parameter(ValueFromPipeline)] [Alias('Source','Member','InputObject')] [PSObject[]] $From, # If provided, will only include members that match any of these wildcards or patterns [Parameter(ValueFromPipelineByPropertyName)] [Alias('IncludeMember')] [PSObject[]] $IncludeProperty, # If provided, will exclude any members that match any of these wildcards or patterns [Parameter(ValueFromPipelineByPropertyName)] [Alias('ExcludeMember')] [PSObject[]] $ExcludeProperty, # The module the members should be imported into. # If this is not provided, or the module has already been imported, a dynamic module will be generated. [Parameter(ValueFromPipelineByPropertyName)] $Module, # Any custom member conversions. # If these are provided, they can convert any type of object or value into a member. # If this is a Dictionary, `[type]` and `[ScriptBlock]` keys can be used to constrain the type. # If this is a PSObject, the property names will be treated as wildcards and the value should be a string or scriptblock that will make the function. [Parameter(ValueFromPipelineByPropertyName)] [Alias('ConvertProperty','TransformProperty','TransformMember')] [PSObject] $ConvertMember, # If set, will pass thru any imported members # If a new module is created, this will pass thru the created module. # If a -Module is provided, and has not yet been imported, then the created functions and aliases will be referenced instead. [Parameter(ValueFromPipelineByPropertyName)] [switch] $PassThru ) process { #region Convert Members to Commands # First up, we need to take our input and turn it into something to import $importMembers = :nextObject foreach ($fromObject in $from) { # (we turn any dictionary into a psuedo-object, for consistency). if ($fromObject -is [Collections.IDictionary]) { $fromObject = [PSCustomObject]$fromObject } :nextMember foreach ($member in $fromObject.PSObject.Members) { #region -Including and -Excluding # We need to look at each potential member # and make sure it's not something we want to -Exclude. if ($ExcludeProperty) { foreach ($exProp in $ExcludeProperty) { # If it is, move onto the next member. if (($exProp -is [regex] -and $member.Name -match $exProp) -or ($exProp -isnot [regex] -and $member.Name -like $exProp)) { continue nextMember } } } # If we're whitelisting as well if ($IncludeProperty) { included :do { # make sure each item is in the whitelist. foreach ($inProp in $IncludeProperty) { if ($inProp -is [Regex] -and $member.Name -match $inProp) { break included } if ($inProp -isnot [Regex] -and $member.Name -like $inProp) { break included } } continue nextMember } while ($false) } #endregion -Including and -Excluding #region Convert Each Member to A Command # Now what we're sure we want this member, let's see if we can have it: #region Custom Member Conversions # If there were custom conversions if ($ConvertMember) { # see if it was a dictionary or not. if ($ConvertMember -is [Collections.IDictionary]) { # For dictionaries we can check for a `[type]`, or a `[ScriptBlock]`, or a `[Regex]` or wildcard `[string]`, # so we'll have to walk thru the dictionary foreach ($convertKeyValue in $ConvertMember.GetEnumerator()) { # (skipping anything that does not have a `[ScriptBlock]` value). if ($convertKeyValue.Value -isnot [scriptblock]) { continue } # Do we have a match? $GotAMatch = # If the key is a [type] if ($convertKeyValue.Key -is [type] -and # and the member is that type, $member.Value -is $convertKeyValue.Key) { $true # we've got a match. } # If the key is `[Regex]` elseif ($convertKeyValue.Key -is [Regex] -and # and the member name matches the pattern $member.Name -match $convertKeyValue ) { $true # we've got a match. } # If the key is a `[ScriptBlock]` elseif ($convertKeyValue.Key -is [scriptblock] -and # and it has a truthy result $(& $convertKeyValue.Key $member)) { $true # we've got a match. } elseif ( # As a last attempt, it's a member is a match if the pstypenames contains the key $member.Value.pstypenames -contains $convertKeyValue.Key -or # or it's value is like the key. $member.Value -like $convertKeyValue.Key ) { $true } # If we have no match, continue if (-not $GotAMatch) { continue } # Run the converter. $convertedScriptOutput = & $convertKeyValue.Value $member # If there's output, continue to the next member. if ($convertedScriptOutput) { @{"function:$($member.Name)" = $convertedScriptOutput};continue nextMember } } } else { # For regular conversion objects, we walk over each property. switch ($ConvertMember.psobject.properties) { { # If the value is a scriptblock and $_.Value -is [ScriptBlock] -and ( # the member's value's typenames contains the convert member name $member.Value.pstypenames -contains $_.Name -or $member.Name -like $_.Name # (or the member's value is like the convert member name ) ) } { # Run the converter. $convertedScriptOutput = & $_.Value $member # If there's output, continue to the next member. if ($convertedScriptOutput) { @{"function:$($member.Name)" = $convertedScriptOutput};continue nextMember } } } } } #endregion Custom Member Conversions #region Automatic Member Conversions switch ($member.Value) { {$_ -is [ScriptBlock]} { # * If it's a `[ScriptBlock]`, it can become a function @{"function:$($member.Name)"= $member.Value} # (just set it directly). } {$_ -is [PSScriptMethod]} { # * If it's a `[PSScriptMethod]`, it can also become a function @{"function:$($member.Name)"= $member.Value.Script} # (just set it to the .Script property) (be aware, `$this` will not work without some additional work). } {$_ -is [string]} { # For strings, we can see if they are a relative path to the module # (assuming there is a module) if ($module.Path) { $absoluteItemPath = $module | Split-Path | Join-Path -ChildPath ( $member.Value -replace '[\\/]', [IO.Path]::DirectorySeparatorChar ) # If the path exists if (Test-Path $absoluteItemPath) { # alias it. @{"alias:$($member.Name)" = "$absoluteItemPath"} } } } } #endregion Automatic Member Conversions #endregion Convert Each Member to A Command } } # If we have no properties we can import, now is the time to return. if (-not $importMembers) { return } #endregion Convert Members to Commands # Now we have to determine how we're declaring and importing these functions. # We're either going to be Generating a New Module. # or Importing into a Loading Module. # In these two scenarios we will want to generate a new module: if ( (-not $module) -or # If we did not provide a module (because how else should we import it?) ($module.Version -ge '0.0') # or if the module has a version (because during load, a module has no version) ) { #region Generating a New Module # If the module has a version, we're postloading. # We'd still love for the functions we create to keep the same module scope, so they could access internal members. if ($module.Version -ge '0.0') { $importMembers = # to make this happen we walk over each member foreach ($dictionaryToImport in $importMembers) { $newDictionary = [Ordered]@{} foreach ($keyValueToImport in $dictionaryToImport.GetEnumerator()) { # and recreate any `[ScriptBlock]` if ($keyValueToImport.Value -is [scriptblock]) { # as a `[ScriptBlock]` from the `$Module`. $newDictionary[$keyValueToImport.Key] = . $module { [ScriptBlock]::Create("$args") } "$($keyValueToImport.Value)" } else { $newDictionary[$keyValueToImport.Key] = $keyValueToImport.Value } } $newDictionary } } # We'll want to timestamp the module $timestamp = $([Datetime]::now.ToString('s')) # and might want to use our own invocation name to name the module. $MyInvocationName = $MyInvocation.InvocationName New-Module -ScriptBlock { # The definition is straightforward enough, foreach ($_ in @($args | & { process { $_.GetEnumerator() }})) { # it just sets each argument with the providers $ExecutionContext.SessionState.InvokeProvider.Item.Set($_.Key, $_.Value, $true, $false) } # and exports everything. Export-ModuleMember -Function * -Variable * -Cmdlet * -Alias * } -Name "$( # We name the new module based off of the module (if present) if ($module) { "$($module.Name)@$timestamp" } # or the command name (if not) else { "$MyInvocationName@$timestamp"} )" -ArgumentList $importMembers | # We pass our ImportMembers as the argument to make it all work Import-Module -Global -PassThru:$PassThru -Force # and import the module globally. #endregion Generating a New Module } elseif ($module -and $module.Version -eq '0.0') { #region Importing into a Loading Module foreach ($_ in @($importMembers | & { process { $_.GetEnumerator() }})) { # If we're importing into a module that hasn't finished loading # get a pointer to it's context. $moduleContext = . $Module { $ExecutionContext } # and use the providers to set the item (and we're good). $moduleContext.SessionState.InvokeProvider.Item.Set($_.Key, $_.Value,$true, $false) # If -PassThru was provided if ($PassThru) { # Pass thru each command. $commandType, $commandName = $_.Key -split ':', 2 $moduleContext.SessionState.InvokeCommand.GetCommand($commandName, $commandType) } } #endregion Importing into a Loading Module } } } |