Import-TypeView.ps1
function Import-TypeView { <# .Synopsis Imports a Type View .Description Imports a Type View, defined in a external file .method or .property file .Link Write-TypeView .Example Import-TypeView .\Types #> param( [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] [Alias('FullName')] [string[]] $FilePath, # If set, will generate an identical typeview for the deserialized form of each typename. [switch]$Deserialized ) process { <# In order to make something like inheritance work, we want to be able to define properties and methods a few places: * In the -FilePath directory (these apply to all types) * In a directory (these apply to that a type sharing the name of the directory) * In any nested subdirectory (these should apply like inherited types) #> $membersByType = @{} foreach ($fp in $FilePath) { $filePathRoot = Get-Item -Path $fp $filesBeneathRoot = Get-ChildItem -Recurse -Path $fp -Force foreach ($fbr in $filesBeneathRoot) { if ($fbr -is [IO.DirectoryInfo]) { continue } if ($fbr.Directory.FullName -eq $filePathRoot.FullName) { # Files directly beneath the root become methods / properties shared by all typenames if (-not $membersByType['*']) { $membersByType['*'] = @() } $membersByType['*'] += $fbr } else { # Files in subdirectories become the methods / properties used by a directory sharing that typename. $subTypeNames = @($fbr.FullName.Substring( $filePathRoot.FullName.Length ).TrimStart( [IO.Path]::DirectorySeparatorChar ).Split([IO.Path]::DirectorySeparatorChar)) foreach ($subType in $subTypeNames) { if ($subType -eq $subTypeNames[-1]) { continue } if (-not $membersByType[$subType]) { $membersByType[$subType] = @() } $membersByType[$subType] += $fbr } } } } if ($membersByType['*']) # If any members applied to all types { # Apply them to each member first (so that it happens in time), foreach ($k in @($membersByType.Keys)) { if ($k -ne '*') { $membersByType[$k] += $membersByType['*'] } } $membersByType.Remove('*') # then remove it (so we don't do it twice). } foreach ($mt in $membersByType.GetEnumerator() | Sort-Object Key) { # Walk thru the members by type $WriteTypeViewSplat = @{ # and create a hashtable to splat. TypeName = $mt.Key Deserialized = $Deserialized } # Then, sort the values by name and by if it comes from this directory. $sortedValues = $mt.Value | Sort-Object Name, { $_.Directory.Name -ne $mt.Key } <# At a high level, what we're about to do so is turn a bunch of files into a bunch of input for Write-TypeView. By default .ps1s will become methods. .ps1 files that start with get_ or set_ become properties. .ps1 files that start with += become events. All other files will become noteproperties. Starting a file with . will hide the property. It should be noted that hidden ScriptMethods are not "private" Methods (though neither are C# private Methods, if you use Reflection). #> $aliasFileNames = 'Alias','Aliases','AliasProperty', '.Alias','.Aliases','.AliasProperty' $defaultDisplayFileName = 'DefaultDisplay','.DefaultDisplay' $scriptMethods = [Ordered]@{} $eventGenerators = [Ordered]@{} $eventNames = @() $scriptPropertyGet = [Ordered]@{} $scriptPropertySet = [Ordered]@{} $aliasProperty = [Ordered]@{} $noteProperty = [Ordered]@{} $hideProperty = [Collections.Generic.List[string]]::new() foreach ($item in $sortedValues) { $itemName = $item.Name.Substring(0, $item.Name.Length - $item.Extension.Length) # If it's a .ps1, it will become a method, property, or event. if ($item.Extension -eq '.ps1') { $isScript = $true $scriptBlock = # We'll want the script block. $ExecutionContext.SessionState.InvokeCommand.GetCommand( $item.Fullname, 'ExternalScript' ).ScriptBlock } else { $isScript = $false } if (-not $isScript -and $itemName -eq $item.Directory.Name) { # If it's a data file that shares the name of the directory # treat it as a "Default" value, and hide the property. $itemName = 'Default' $hideProperty += $itemName } elseif ($itemName.StartsWith('.')) { # If the file starts with a ., hide the property. $itemName = $itemName.TrimStart('.') $hideProperty += $itemName } if ($isScript -and # If the file is a script $itemName -match '(?<GetSet>get|set)_' # and it starts with get_ or set_ ) { $propertyName = $itemName.Substring(4) # it's a property. # If it's a get, store it along with the other gets if ($matches.GetSet -eq 'get' -and -not $scriptPropertyGet.Contains($propertyName)) { $scriptPropertyGet[$propertyName] = $scriptBlock } # Otherwise, store it with the sets. elseif (-not $scriptPropertySet.Contains($propertyName)) { if ($scriptPropertySet.Contains($propertyName)) { continue } $scriptPropertySet[$propertyName] = $scriptBlock } } elseif ($isScript -and # If this is a script and it's an event ($itemName -match '^@' -or # (prefaced with @ -or ending with .event.ps1) $itemName -match '\.event\.ps1$' ) ) { $eventName = $itemName.Substring(2) $eventGenerators[$eventName] = $scriptBlock # store it for later. } elseif ($isScript) # Otherwise, if it's a script, it's a method. { $methodName =$itemName if ($scriptMethods.Contains($methodName)) { continue } $scriptMethods[$methodName] = $scriptBlock } else { # If it's not a method, it's a data file. # Most of these will become properties. $fileText = [IO.File]::ReadAllText($item.FullName) # If the file was a structure all PowerShell engines can read, we'll load it. # Currently, .clixml, .json, .psd1, .txt, and .xml are supported. if ($item.Extension -in '.txt','.psd1', '.xml','.json','.clixml' -and $scriptPropertyGet[$itemName]) { # Of course if we've already given this a .ps1, we'd prefer that and will move onto the next. continue } # Let's take a look at the extension to figure out what we do. switch ($item.Extension) { #region .txt Files .txt { if ($defaultDisplayFileName -contains $itemName) # If it's a default display file { # Use each line of the file text as the name of a property to display $WriteTypeViewSplat.DefaultDisplay = $fileText -split '(?>\r\n|\n)' -ne '' } elseif ($itemName -match '^@') { $eventNames += $itemName.Substring(1) } elseif ($itemName -match '\.event\.txt') { $eventNames += $itemName -replace '\.event$' } elseif (-not $noteProperty.Contains($itemName)) # Otherwise, it's a simple string noteproperty { $noteProperty[$itemName] = $fileText } } #endregion .txt Files #region .psd1 Files .psd1 { # If it's a .psd1 # we load it in a data block # Load it in a data block $dataScriptBlock = [ScriptBlock]::Create(@" data { $([ScriptBlock]::Create($fileText)) } "@) if ($aliasFileNames -contains $itemName) # If it's an Alias file { # we load it now $aliasProperty = (& $dataScriptBlock) -as [Collections.IDictionary] } else { # otherwise, we load it in a ScriptProperty $scriptPropertyGet[$itemName] = $dataScriptBlock } } #endregion .psd1 Files #region XML files .xml { # Xml content is cached inline in a ScriptProperty and returned casted to [xml] $scriptPropertyGet[$itemName] = [ScriptBlock]::Create(@" [xml]@' $fileText '@ "@) } #endregion XML files #region JSON files .json { # Json files are piped to ConvertFrom-Json $scriptPropertyGet[$itemName] = [ScriptBlock]::Create(@" @' $fileText '@ | ConvertFrom-Json "@) } #endregion JSON files #region CliXML files # Clixml files are embedded into a ScriptProperty and Deserialized. .clixml { $scriptPropertyGet[$itemName] = [ScriptBlock]::Create(@" [Management.Automation.PSSerializer]::Deserialize(@' $fileText '@) "@) } #endregion CliXML files default { # If we have no clue what kind of file it is, the only good way we can handle it as a byte[] # This is tricky because we are creating a .types.ps1xml file, # which is XML and big enough already. # So we compress it. $fileBytes = # Read all the bytes [IO.File]::ReadAllBytes($item.FullName) $ms = [IO.MemoryStream]::new() # Create a new memory stream $gzipStream = # Create a gzip stream [IO.Compression.GZipStream]::new($ms, [Io.Compression.CompressionMode]"Compress") $null = # Write the file bytes to the stream $gzipStream.Write($fileBytes, 0, $fileBytes.Length) $null = $gzipStream.Close() # close the stream. This was the easy part. $itemName = $item.Name if ($itemName.StartsWith('.')) { $itemName = $itemName.TrimStart('.') $hideProperty += $itemName } if (-not $scriptPropertyGet.Contains($itemName)) { # The hard part is dynamically creating the unpacker. # The get script will need to do the above steps in reverse, and read the bytes back $scriptPropertyGet[$itemName] = [ScriptBlock]::Create(@" `$stream = [IO.Compression.GZipStream]::new( [IO.MemoryStream]::new( [Convert]::FromBase64String(@' $( # We do this by embedding the byte[] inside a Heredoc in Base64 [Convert]::ToBase64String($ms.ToArray(), "InsertLineBreaks") ) '@) ), [IO.Compression.CompressionMode]'Decompress' ); "@ + @' $BufferSize = 1kb $buffer = [Byte[]]::new($BufferSize) $bytes = do { $bytesRead= $stream.Read($buffer, 0, $BufferSize ) $buffer[0..($bytesRead - 1)] if ($bytesRead -lt $BufferSize ) { break } } while ($bytesRead -eq $BufferSize ) $bytes -as [byte[]] $stream.Close() $stream.Dispose() '@) } } } } } if ($scriptMethods.Count) { $WriteTypeViewSplat.ScriptMethod = $scriptMethods } if ($aliasProperty.Count) { $WriteTypeViewSplat.AliasProperty = $aliasProperty } if ($scriptPropertyGet.Count -or $scriptPropertySet.Count) { $scriptProperties = [Ordered]@{} foreach ($k in $scriptPropertyGet.Keys) { $scriptProperties[$k] = $scriptPropertyGet[$k] } foreach ($k in $scriptPropertySet.Keys) { if (-not $scriptProperties[$k]) { $scriptProperties[$k] = {}, $scriptPropertySet[$k] } else { $scriptProperties[$k] = $scriptProperties[$k], $scriptPropertySet[$k] } } $WriteTypeViewSplat.ScriptProperty = $scriptProperties } if ($noteProperty.Count) { $WriteTypeViewSplat.NoteProperty = $noteProperty } if ($eventGenerators.Count) { $WriteTypeViewSplat.EventGenerator = $eventGenerators } if ($eventNames) { $WriteTypeViewSplat.EventName = $eventNames } if ($WriteTypeViewSplat.Count -gt 1) { $WriteTypeViewSplat.HideProperty = $hideProperty Write-TypeView @WriteTypeViewSplat } } $null = $null } } |