Commands/Types.PS1XML/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( # The path containing type information. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [Alias('FullName')] [string[]] $FilePath, # If set, will generate an identical typeview for the deserialized form of each typename. [switch]$Deserialized, # Any file paths to exclude. [Parameter(ValueFromPipelineByPropertyName)] [SupportsWildcards()] [PSObject[]] $ExcludeFilePath, # A pattern describing the types of files that will embedded as constant note properties containing the file's text. [Parameter(ValueFromPipelineByPropertyName)] [Alias('TextFilePattern')] [SupportsWildcards()] [PSObject[]] $TextFileType = @('.cs','.js','.ts','.htm*','.*proj','.h','.cpp','*.class.ps1') ) 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 :nextFile foreach ($fileBeneathRoot in $filesBeneathRoot) { if ($fileBeneathRoot -is [IO.DirectoryInfo]) { continue } if ($ExcludeFilePath) { foreach ($exclusion in $ExcludeFilePath) { if ($exclusion -is [string] -and ($file.Name -like $exclusion -or $file.FullName -like $exclusion)) { continue nextFile } if ($exclusion -is [regex] -and ($file.Name -match $exclusion -or $file.FullName -match $exclusion)) { continue nextFile } } } if ($fileBeneathRoot.Directory.FullName -eq $filePathRoot.FullName) { # Files directly beneath the root become methods / properties shared by all typenames if (-not $membersByType['*']) { $membersByType['*'] = @() } $membersByType['*'] += $fileBeneathRoot } else { # Files in subdirectories become the methods / properties used by a directory sharing that typename. $subTypeNames = @($fileBeneathRoot.FullName.Substring( $filePathRoot.FullName.Length ).TrimStart( [IO.Path]::DirectorySeparatorChar ).Split([IO.Path]::DirectorySeparatorChar)) # Initialize the name of the subtype to zero $subType = '' for ( # and walk backwards thru name segements # (skipping the first one, since it's our file name) $subtypeIndex = $subTypeNames.Length - 1; $subtypeIndex -ge 0; $subTypeIndex-- ) { # If it's the second to last segment, it's our directory name # (the bucket we're using to psuedo-type). if ($subTypeindex -eq ($subTypeNames.Length - 2)) { # Map the subtype $subType = $subTypeNames[$subtypeIndex] if (-not $membersByType[$subType]) { $membersByType[$subType] = @() } # and add this file to it $membersByType[$subType] += $fileBeneathRoot } elseif ( # If it's _not_ the subtype, but information is defined on that level $membersByType[$subTypeNames[$subtypeIndex]] -and $subType ) { # we will take everything that is not PSTypeName.txt $membersByType[$subType] += @( @($membersByType[$subTypeNames[$subtypeIndex]]) -notmatch '^(?:PS)?TypeNames{0,1}\.txt$' ) # Congratulations! These very few lines are all it takes to enable inheritance! } } } } } 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). } :nextMember 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' $typeNameFileNames = 'TypeName','TypeNames','PSTypeName', 'PSTypeNames', '.TypeName', '.TypeNames','.PSTypeName', '.PSTypeNames' $defaultDisplayFileName = 'DefaultDisplay','.DefaultDisplay' $scriptMethods = [Ordered]@{} $eventGenerators = [Ordered]@{} $eventNames = @() $scriptPropertyGet = [Ordered]@{} $scriptPropertySet = [Ordered]@{} $propertySets = [Ordered]@{} $aliasProperty = [Ordered]@{} $noteProperty = [Ordered]@{} $hideProperty = [Collections.Generic.List[string]]::new() foreach ($item in $sortedValues) { # If the file is a [PipeScript](https://github.com/StartAutomating/PipeScript) source generator. if ($item.Name -match '\.ps1{0,1}\.(?<ext>[^.]+$)') { continue # then skip it # (this can simplify development of ScriptProperties, Methods, and other files). } # Skip format/view files (this allows them to be in the same directory as types, if that is preferred) if ($item.Name -match '\.(?>format|view|control)\.ps1$') { continue } $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 ($isScript -and -not ($scriptBlock -replace '\s')) { continue } 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.Substring(1) $hideProperty += $itemName -replace '^(?>get|set)_' } 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 -and # Otherwise, if it's a script, it's a method. ($item.Name -notlike '*.class.ps1') # (unless it is a class.ps1, in which case skip it or let it become a note property) ) { $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 } if ($TextFileType) { foreach ($textFilePattern in $TextFileType) { if ($textFilePattern -is [string] -and ($item.Name -like "*$textFilePattern" -or $item.FullName -like "*$textFilePattern")) { $noteProperty[$item.Name] = $fileText } elseif ($exclusion -is [regex] -and ($item.Name -match $textFilePattern -or $item.FullName -match $textFilePattern)) { $noteProperty[$item.Name] = $fileText } } } # 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 '' } if ($typeNameFileNames -contains $itemName) { $WriteTypeViewSplat.TypeName = $fileText -split '(?>\r\n|\n)' -ne '' } elseif ($itemName -like '*.propertySet') { # If it's a property set file (.propertyset.txt) $propertySets[$itemName -replace '\.propertySet$'] = # Create a property set with the file name. $fileText -split '(?>\r\n|\n)' -ne '' # Each line will be treated as a name of a property. } 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 } } .md { if (-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] } elseif (-not $noteProperty.Contains($itemName)) { # 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] if (-not $noteProperty.Contains($itemName)) { $scriptPropertyGet[$itemName] = [ScriptBlock]::Create(@" [xml]@' $fileText '@ "@) } } #endregion XML files #region JSON files .json { # Json files are piped to ConvertFrom-Json if (-not $noteProperty.Contains($itemName)) { $scriptPropertyGet[$itemName] = [ScriptBlock]::Create(@" @' $fileText '@ | ConvertFrom-Json "@) } } #endregion JSON files #region CliXML files # Clixml files are embedded into a ScriptProperty and Deserialized. .clixml { if (-not $noteProperty.Contains($itemName)) { $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.Substring(1) $hideProperty += $itemName } if ((-not $scriptPropertyGet.Contains($itemName)) -and (-not $noteProperty[$item.Name])) { # 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 ($propertySets.Count) { $WriteTypeViewSplat.PropertySet = $propertySets } 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] = {return $null}, $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 } } } } |