functions/public.ps1


Function Get-PSTypeExtension {
    [cmdletbinding()]
    [outputtype("PSTypeExtension")]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            HelpMessage = "Enter the name of type like System.IO.FileInfo",
            ValueFromPipelineByPropertyName,
            ValueFromPipeline
        )]
        [ValidateNotNullorEmpty()]
        [ValidateScript( {
                #check if typename can be found with Get-TypeData
                if ((Get-TypeData).typename -contains "$_") {
                    $True
                }
                elseif ($_ -as [type]) {
                    #test if string resolves as a typename
                    $True
                }
                else {
                    Throw "$_ does not appear to be a valid type."
                }
            })]
        [string]$TypeName,
        [Parameter(
            HelpMessage = "Enter a comma separated list of member names",
            ParameterSetName = "members"
        )]
        [string[]]$Members,
        [Parameter(HelpMessage = "Show CodeProperty custom properties")]
        [switch]$CodeProperty
    )

    Begin {
        Write-Verbose "Starting: $($MyInvocation.Mycommand)"
        $typedata = @()
        $TypeName=_convertTypeName $TypeName
    } #begin
    Process {
        Write-Verbose "Analyzing $typename"
        $typedata += Get-TypeData -TypeName $typename

    } #process
    End {
        $typedata = $typedata | Select-Object -Unique

        if ($typedata) {
            $out = [System.Collections.Generic.List[object]]::new()
            if (-Not $Members) {
                Write-Verbose "Getting all member names"
                $Members = $typedata.members.keys
            }
            foreach ($name in $Members) {
                Try {
                    Write-Verbose "Analyzing member $name"
                    $member = $typedata.members[$name]
                    $datatype = $member.gettype().name

                    Write-Verbose "Processing type $datatype"
                    Switch ($datatype) {
                        "AliasPropertyData" {
                            $def = [pscustomobject]@{
                                PSTypename = 'PSTypeExtension'
                                MemberType = "AliasProperty"
                                MemberName = $member.name
                                Value      = $member.ReferencedMemberName
                                Typename = $TypeName
                            }
                        } #alias
                        "ScriptpropertyData" {
                            if ($member.GetScriptBlock) {
                                $code = $member.GetScriptBlock.ToString()
                            }
                            else {
                                $code = $member.SetScriptBlock.ToString()
                            }
                            $def = [pscustomobject]@{
                                PSTypename = 'PSTypeExtension'
                                MemberType = "ScriptProperty"
                                MemberName = $member.name
                                Value      = $code
                                Typename = $TypeName
                            }
                        } #scriptproperty
                        "ScriptMethodData" {
                            $def = [pscustomobject]@{
                                PSTypename = 'PSTypeExtension'
                                MemberType = "ScriptMethod"
                                MemberName = $member.name
                                Value      = $member.script.ToString().trim()
                                Typename = $TypeName
                            }
                        } #scriptmethod
                        "NotePropertyData" {
                            $def = [pscustomobject]@{
                                PSTypename = 'PSTypeExtension'
                                MemberType = "Noteproperty"
                                MemberName = $member.name
                                Value      = $member.Value
                                Typename = $TypeName
                            }
                        } #noteproperty
                        "CodePropertyData" {
                            #only show these if requested with -CodeProperty
                            if ($CodeProperty) {
                                if ($member.GetCodeReference) {
                                    $code = $member.GetCodeReference.ToString()
                                }
                                else {
                                    $code = $member.SetCodeReference.ToString()
                                }
                                $def = [pscustomobject]@{
                                    PSTypename = 'PSTypeExtension'
                                    MemberType = "CodeProperty"
                                    MemberName = $member.name
                                    Value      = $code
                                    Typename = $TypeName
                                }
                            }
                            else {
                                $def = $False
                            }
                        } #codeproperty
                        Default {
                            Write-Warning "Cannot process $datatype type for $($typedata.typename)."
                            $def = [pscustomobject]@{
                                PSTypename = 'PSTypeExtension'
                                MemberType = $datatype
                                MemberName = $member.name
                                Value      = $member.Value
                                Typename = $TypeName
                            }
                        }
                    }
                    if ($def) {
                        $out.Add($def)
                    }

                }
                Catch {
                    Write-Warning "Could not find an extension member called $name"
                    Write-Debug $_.exception.message
                }

            } #foreach
            #write sorted results
            $out | Sort-Object -Property MemberType, Name
        }
        else {
            Write-Warning "Failed to find any type extensions for [$Typename]."
        }
        Write-Verbose "Ending: $($MyInvocation.Mycommand)"
    }

} #end Get-PSTypeExtension

Function Get-PSType {
    [cmdletbinding()]
    [outputtype("System.String")]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline
        )]
        [object]$Inputobject
    )

    Begin {
        Write-Verbose "Starting: $($MyInvocation.Mycommand)"
        $data = @()
    }
    Process {
        #get the type of each pipelined object
        $data += ($Inputobject | Get-Member | Select-Object -First 1).typename
    }
    End {
        #write unique values to the pipeline
        $data | Get-Unique
        Write-Verbose "Ending: $($MyInvocation.Mycommand)"
    }
} #end Get-PSType

Function Export-PSTypeExtension {
    [cmdletbinding(SupportsShouldProcess, DefaultParameterSetName = "Object")]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            HelpMessage = "The type name to export like System.IO.FileInfo",
            ParameterSetName = "Name"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$TypeName,

        [Parameter(
            Mandatory,
            HelpMessage = "The type extension name",
            ParameterSetName = "Name"
        )]
        [ValidateNotNullOrEmpty()]
        [string[]]$MemberName,

        [Parameter(
            Mandatory,
            HelpMessage = "The name of the export file. The extension must be .json,.xml or .ps1xml"
        )]
        [ValidatePattern("\.(xml|json|ps1xml)$")]
        [string]$Path,

        [Parameter(ParameterSetName = "Object", ValueFromPipeline)]
        [object]$InputObject,

        [switch]$Passthru
    )
    DynamicParam {
        #create a dynamic parameter to append to .ps1xml files.
        if ($Path -match '\.ps1xml$') {

            #define a parameter attribute object
            $attributes = New-Object System.Management.Automation.ParameterAttribute
            $attributes.HelpMessage = "Append to an existing .ps1xml file"

            #define a collection for attributes
            $attributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $attributeCollection.Add($attributes)

            #define the dynamic param
            $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Append", [Switch], $attributeCollection)

            #create array of dynamic parameters
            $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
            $paramDictionary.Add("Append", $dynParam1)
            #use the array
            return $paramDictionary

        } #if
    } #dynamic parameter

    Begin {
        Write-Verbose "Starting: $($MyInvocation.Mycommand)"
        Write-Verbose "Detected parameter set $($pscmdlet.ParameterSetName)"
        $PathParent = Split-Path -Path $Path

        if (Test-Path -Path $PathParent) {
            $validPath = $true
        }
        else {
            Write-Warning "Can't find parent path $pathParent"
            $validPath = $False
        }

        #test if appending
        if ($append -AND (Test-Path $Path)) {
            $validPath = $true
        }
        elseif($append) {
            Write-Warning "Can't find $path for appending."
            $validPath = $False
        }

        #initialize a list of objects
        $data = [System.Collections.Generic.list[object]]::new()

        if ($TypeName) {
            #convert typename to a properly cased name
            Write-Verbose "Converting $typename to properly cased type name."
            $TypeName=_convertTypeName $TypeName
        }
    }
    Process {
        #test if parent path exists
        If ($validPath) {
            if ($Inputobject) {
                Write-Verbose "Processing input type: $($InputObject.TypeName)"
                #convert the type name just in case
                $typeName = _convertTypeName $InputObject.TypeName
                $data.Add($InputObject)
            }
            else {
                Write-Verbose "Processing type: $TypeName"
                foreach ($member in $membername) {
                    $typemember = Get-PSTypeExtension -TypeName $Typename -Members $Member
                    $data.Add($typemember)
                }
            }
        } #test path parent
    }
    End {
        if ($validPath) {
            Write-Verbose "Exporting data to $path"

            if ($Path -match "\.ps1xml$") {

                if ($PSBoundParameters["Append"]) {
                    $cpath = Convert-Path $path
                    Write-Verbose "Appending to $cpath"

                    [xml]$doc = Get-Content $cPath
                    $members = $doc.types.SelectNodes("Type[Name='$typeName']").Members

                    if ($members) {
                        Write-Verbose "Appending to existing typename entry"
                    }
                    else {
                        Write-Verbose "Creating a new typename entry for $TypeName"
                        $main = $doc.CreateNode("element", "Type", $null)
                        $tName = $doc.CreateElement("Name")
                        $tName.InnerText = $typename
                        [void]($main.AppendChild($tname))
                        $members = $doc.CreateNode("element", "Members", $null)
                        $IsNewType = $True
                    }

                        foreach ($extension in $data) {
                            Write-Verbose "Exporting $($extension.membername)"
                            $membertype = $doc.createNode("element", $extension.memberType, $null)
                            $membernameEL = $doc.CreateElement("Name")

                            $membernameEL.innertext = $extension.memberName
                            [void]($membertype.AppendChild($membernameEL))

                            Switch ($extension.Membertype) {
                                "ScriptMethod" {
                                    $memberdef = $doc.createelement("Script")
                                }
                                "ScriptProperty" {
                                    $memberdef = $doc.createelement("GetScriptBlock")
                                }
                                "AliasProperty" {
                                    $memberdef = $doc.createelement("ReferencedMemberName")
                                }
                                "NoteProperty" {
                                    $memberdef = $doc.createelement("Value")
                                }
                                Default {
                                    Throw "Can't process a type of $($extension.MemberType)"
                                }
                            } #switch membertype

                            $memberdef.InnerText = $extension.value
                            [void]($membertype.AppendChild($memberdef))
                            [void]($members.AppendChild($membertype))

                        } #foreach extension

                        if ($IsnewType) {
                            Write-Verbose "Appending new type"
                            [void]($main.AppendChild($members))
                            [void]($doc.types.AppendChild($main))
                        }

                        if ($PSCmdlet.ShouldProcess($cpath)) {
                            $doc.save($cpath)
                        } #end Whatif

                    } #if append

                else {
                    Write-Verbose "Saving as new PS1XML"
                    #create a placeholder file so that I can later convert the path
                    [void](New-Item -Path $path -Force)
                    [xml]$Doc = New-Object System.Xml.XmlDocument

                    #create declaration
                    $dec = $Doc.CreateXmlDeclaration("1.0", "UTF-8", $null)
                    #append to document
                    [void]($doc.AppendChild($dec))

                    #create a comment and append it in one line
                    $text = @"
 
This file was created with Export-PSTypeExtenstion from the
PSTypeExtensionTools module which you can install from
the PowerShell Gallery.
 
Use Update-TypeData to import this file in your PowerShell session.
 
Created $(Get-Date)
 
"@

                    [void]($doc.AppendChild($doc.CreateComment($text)))

                    #create root Node
                    $root = $doc.CreateNode("element", "Types", $null)
                    $main = $doc.CreateNode("element", "Type", $null)
                    $name = $doc.CreateElement("Name")
                    $name.innerText = $data[0].TypeName
                    [void]($main.AppendChild($name))
                    $members = $doc.CreateNode("element", "Members", $null)
                    foreach ($extension in $data) {
                        Write-Verbose "Exporting $($extension.membername)"
                        $membertype = $doc.createNode("element", $extension.memberType, $null)
                        $membernameEL = $doc.CreateElement("Name")

                        $membernameEL.innertext = $extension.memberName
                        [void]($membertype.AppendChild($membernameEL))

                        Switch ($extension.Membertype) {
                            "ScriptMethod" {
                                $memberdef = $doc.createelement("Script")
                            }
                            "ScriptProperty" {
                                $memberdef = $doc.createelement("GetScriptBlock")
                            }
                            "AliasProperty" {
                                $memberdef = $doc.createelement("ReferencedMemberName")
                            }
                            "NoteProperty" {
                                $memberdef = $doc.createelement("Value")
                            }
                            Default {
                                Throw "Can't process a type of $($extension.MemberType)"
                            }
                        }
                        $memberdef.InnerText = $extension.value
                        [void]($membertype.AppendChild($memberdef))
                        [void]($members.AppendChild($membertype))

                    } #foreach extension
                    [void]($main.AppendChild($members))
                    [void]($root.AppendChild($main))
                    [void]($doc.AppendChild($root))
                    $out = Convert-Path $path
                    if ($PSCmdlet.ShouldProcess($out)) {
                        $doc.save($out)
                    } #end Whatif
                } #else
            } #if ps1xml
            elseif ($path -match "\.xml$") {
                Write-Verbose "Saving as XML"
                $data | Export-Clixml -Path $path
            }
            else {
                Write-Verbose "Saving as JSON"
                $data | ConvertTo-Json | Set-Content -Path $Path
            }
        } #if valid path

        if ($Passthru) {
            Get-Item -path $Path
        }
        Write-Verbose "Ending: $($MyInvocation.Mycommand)"
    }

} #end Export-PSTypeExtension

Function Import-PSTypeExtension {
    [CmdletBinding(SupportsShouldProcess)]
    Param(
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            HelpMessage = "The name of the imported file. The extension must be .xml or .json")]
        [ValidatePattern("\.(xml|json)$")]
        [ValidateScript( { Test-Path $(Convert-Path $_) })]
        [alias("fullname")]
        [string]$Path
    )

    Begin {
        Write-Verbose "Starting: $($myInvocation.mycommand)"
    }
    Process {
        Write-Verbose "Importing file $(Convert-Path $Path)"
        if ($path -match "\.xml$") {
            #xml format seems to add an extra entry
            $import = Import-Clixml -Path $path | Where-Object MemberType
        }
        else {
            $import = Get-Content -Path $path | ConvertFrom-Json
        }

        foreach ($item in $import) {
            Write-Verbose "Processing $($item.MemberType) : $($item.MemberName)"
            if ($item.MemberType -match "^Code") {
                Write-Warning "Skipping Code related member"
            }
            if ($item.MemberType -match "^Script") {
                Write-Verbose "Creating scriptblock from value"
                $value = [scriptblock]::create($item.value)
            }
            else {
                $value = $item.Value
            }
            #add a custom -WhatIf message
            if ($PSCmdlet.ShouldProcess($Item.typename, "Adding $($item.membertype) $($item.MemberName)")) {
                #implement the change
                Update-TypeData -TypeName $item.Typename -MemberType $item.MemberType -MemberName $item.MemberName -Value $value -Force
            }
        } #foreach
    }
    End {
        Write-Verbose "Ending: $($myInvocation.mycommand)"
    }

} #end Import-PSTypeExtension

Function Add-PSTypeExtension {
    [cmdletbinding(SupportsShouldProcess)]
    [Alias('Set-PSTypeExtension')]

    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            HelpMessage = "Enter the name of a type like system.io.fileinfo")]
        [string]$TypeName,
        [Parameter(
            Mandatory,
            HelpMessage = "The member type"
        )]
        [ValidateSet("AliasProperty", "Noteproperty", "ScriptProperty", "ScriptMethod")]
        [alias("Type")]
        [string]$MemberType,
        [Parameter(
            Mandatory,
            HelpMessage = "The name of your type extension"
        )]
        [ValidateNotNullOrEmpty()]
        [alias("Name")]
        [string]$MemberName,
        [Parameter(
            Mandatory,
            HelpMessage = "The value for your type extension. Remember to enclose scriptblocks in {} and use `$this"
        )]
        [ValidateNotNullOrEmpty()]
        [Object]$Value,
        [Parameter(HelpMessage = "Create the extension in the deserialized version of the specified type including the original type.")]
        [switch]$IncludeDeserialized

    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"

    } #begin

    Process {
        #force overwrite of existing extensions
        $PSBoundParameters.Add("Force", $True)
        if ($PSBoundParameters.ContainsKey("IncludeDeserialized")) {
            [void]$PSBoundParameters.Remove("IncludeDeserialized")
            $PSBoundParameters.Typename = "deserialized.$Typename"
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Adding $MemberType $Membername to $($psboundparameters.TypeName)"
            Update-TypeData @PSBoundParameters
            $PSBoundParameters.Typename = $Typename
        }
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Adding $MemberType $Membername to $($psboundparameters.TypeName)"
        Update-TypeData @PSBoundParameters
    } #process

    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"

    } #end

} #close Add-MyTypeExtension

Function New-PSPropertySet {
    [cmdletbinding(SupportsShouldProcess, DefaultParameterSetName = "new")]
    Param(
        [Parameter(Position = 0, Mandatory, HelpMessage = "Enter the object typename.")]
        [ValidateNotNullOrEmpty()]
        [string]$Typename,
        [Parameter(Mandatory, HelpMessage = "Enter the new property set name. It should be alphanumeric.")]
        [ValidatePattern("^\w+$")]
        [string]$Name,
        [Parameter(Mandatory, HelpMessage = "Enter the existing property names or aliases to belong to this property set.")]
        [ValidateNotNullOrEmpty()]
        [string[]]$Properties,
        [Parameter(Mandatory, HelpMessage = "Enter the name of the .ps1xml file to create.")]
        [ValidatePattern("\.ps1xml$")]
        [string]$FilePath,
        [Parameter(HelpMessage = "Append to an existing file.", ParameterSetName = "append")]
        [switch]$Append,
        [Parameter(HelpMessage = "Don't overwrite an existing file.", ParameterSetName = "new")]
        [switch]$NoClobber
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
        $settings = [System.Xml.XmlWriterSettings]::new()
        $settings.Indent = $True

        $newComment = @"
 
This file was created with New-PSPropertySet from the
PSTypeExtensionTools module which you can install from
the PowerShell Gallery.
 
Use Update-TypeData to append this file in your PowerShell session.
 
Created $(Get-Date)
 
"@

    } #begin

    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating a property set called $Name for $Typename"
        #convert file path to a true file system path.
        $cPath = Join-Path -Path (Convert-Path (Split-Path $filepath)) -ChildPath (Split-Path $FilePath -Leaf)
        if ($Append -AND (-Not (Test-Path $FilePath))) {
            Write-Warning "Failed to find $Filepath for appending."
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Ending $($myinvocation.mycommand)"
            #bail out
            return
        }
        elseif ($Append) {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Appending to $filepath"
            [xml]$doc = Get-Content $cPath
            $members = $doc.types.SelectNodes("Type[Name='$typeName']").Members

            if ($members) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Appending to existing typename entry"
            }
            else {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating a new typename entry"
                $newType = $doc.CreateNode("element", "Type", $null)
                $tName = $doc.CreateElement("Name")
                $tName.InnerText = $typename
                [void]($newType.AppendChild($tname))
                $members = $doc.CreateNode("element", "Members", $null)
                $IsNewType = $True
            }

            $propSet = $doc.CreateNode("element", "PropertySet", $null)
            $eName = $doc.CreateElement("Name")
            $eName.InnerText = $Name
            [void]($propset.AppendChild($eName))
            $ref = $doc.CreateNode("element", "ReferencedProperties", $null)
            foreach ($item in $properties) {
                $prop = $doc.CreateElement("Name")
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Adding property $item"
                $prop.InnerText = $item
                [void]($ref.AppendChild($prop))
            }
            [void]($propset.AppendChild($ref))
            [void]($members.AppendChild($propset))

            if ($IsnewType) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Appending new type"
                [void]($newType.AppendChild($members))
                [void]($doc.types.AppendChild($newtype))
            }

        } #else append
        else {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating a new XML document"
            <#
            use a random temp name to create the xml file. At the end of the process copy the temp file
            to the specified file path. This makes it possible to use -WhatIf
            #>

            $tmpFile = [System.IO.Path]::GetTempFileName()
            $doc = [System.Xml.XmlWriter]::Create($tmpFile, $settings)
            $doc.WriteStartDocument()
            $doc.WriteWhitespace("`n")
            $doc.WriteComment($newComment)
            $doc.WriteWhitespace("`n")
            $doc.WriteStartElement("Types")

            $doc.WriteStartElement("Type")
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Defining type as $Typename"
            $doc.WriteElementString("Name", $TypeName)

            $doc.WriteStartElement("Members")
            $doc.WriteStartElement("PropertySet")
            #the property set name
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Defining property set name $Name"
            $doc.WriteElementString("Name", $Name)

            $doc.WriteStartElement("ReferencedProperties")
            foreach ($item in $properties) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Adding property $item"
                $doc.WriteElementString("Name", $item)
            }

            #end type
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Closing and saving file."
            $doc.WriteEndElement()
            $doc.WriteEndDocument()
            $doc.Close()
            $doc.Dispose()
        }

        if ($PSCmdlet.ShouldProcess($cpath)) {
            if ((-Not $Append) -AND (Test-Path $tmpFile)) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Copying temp file to $cpath"

                if ($NoClobber -AND (Test-Path $cpath)) {
                    Write-Warning "The file $cpath exists and NoClobber was specified."
                }
                else {
                    Copy-Item -Path $tmpFile -Destination $cpath
                }

                #always clean up the temp file
                Remove-Item -Path $tmpFile -WhatIf:$false -ErrorAction SilentlyContinue
            }
            else {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Saving to $cpath"
                $doc.Save($cpath)
            }
        }

    } #process

    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
    } #end

} #close New-PSPropertySet