ConvertTo-Expression.ps1

<#PSScriptInfo
.VERSION 2.4.1
.GUID 5f167621-6abe-4153-a26c-f643e1716720
.AUTHOR Ronald Bode (iRon)
.DESCRIPTION Serializes an object to a PowerShell expression (PSON, PowerShell Object Notation).
.COMPANYNAME
.COPYRIGHT
.TAGS PSON PowerShell Object Notation expression serialize
.LICENSEURI https://github.com/iRon7/ConvertTo-Expression/LICENSE.txt
.PROJECTURI https://github.com/iRon7/ConvertTo-Expression
.ICONURI https://raw.githubusercontent.com/iRon7/ConvertTo-Expression/master/ConvertTo-Expression.png
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
.PRIVATEDATA
#>


Function ConvertTo-Expression {
    <#
        .SYNOPSIS
            Serializes an object to a PowerShell expression.
 
        .DESCRIPTION
            The ConvertTo-Expression cmdlet converts (serializes) an object to
            a PowerShell expression. The object can be stored in a variable,
            file or any other common storage for later use or to be ported to
            another system.
 
            Convert from expression
            An expression can be restored to an object by preceding it with an
            ampersand (&). An expression that is casted to a string can be
            restored to an object using the native Invoke-Expression cmdlet.
            An expression that is stored in a PowerShell (.ps1) file might also
            be directly invoked by the PowerShell dot-sourcing technique.
 
        .PARAMETER InputObject
            Specifies the objects to convert to a PowerShell expression. Enter
            a variable that contains the objects, or type a command or
            expression that gets the objects. You can also pipe one or more
            objects to ConvertTo-Expression.
 
        .PARAMETER Depth
            Specifies how many levels of contained objects are included in the
            PowerShell representation. The default value is 9.
 
        .PARAMETER Expand
            Specifies till what level the contained objects are expanded over
            separate lines and indented according to the -Indentation and
            -IndentChar parameters. The default value is 9.
             
            A negative value will remove redundant spaces and compress the
            PowerShell expression to a single line (except for multi-line
            strings).
             
            Xml documents and multi-line strings are embedded in a
            "here string" and aligned to the left.
             
        .PARAMETER Indentation
            Specifies how many IndentChars to write for each level in the
            hierarchy.
 
        .PARAMETER IndentChar
            Specifies which character to use for indenting.
 
        .PARAMETER TypePrefix
            Defines how the explicit the object type is being parsed:
 
            -TypePrefix None
                No type information will be added to the (embedded) objects and
                values in the PowerShell expression. This means that objects
                and values will be parsed to one of the following data types
                when reading them back with Invoke-Expression: a numeric value,
                a [String] ('...'), an [Array] (@(...)) or a [HashTable]
                (@{...}).
 
            -TypePrefix Native
                The original type prefix is added to the (embedded) objects and
                values in the PowerShell expression. Note that most system
                (.Net) objects can’t be read back with Invoke-Expression, but
                option might help to reveal (embedded) object types and
                hierarchies.
 
            -TypePrefix Cast (Default)
                The type prefix is only added to (embedded) objects and values
                when required and optimized for read back with
                Invoke-Expression by e.g. converting system (.Net) objects to
                PSCustomObject objects. Numeric values won't have a strict
                type and therefor parsed to the default type that fits the
                value when restored.
 
            -TypePrefix Strict
                All (embedded) objects and values will have an explicit type
                prefix optimized for read back with Invoke-Expression by e.g.
                converting system (.Net) objects to PSCustomObject objects.
 
        .PARAMETER NewLine
            Specifies which characters to use for a new line. The default is
            defined by the operating system.
 
        .PARAMETER Iteration
            Do not use (for internal use only).
 
        .EXAMPLE
 
            PS C:\> $Calendar = (Get-UICulture).Calendar | ConvertTo-Expression
             
            PS C:\ $Calendar
             
            [PSCustomObject]@{
                    'AlgorithmType' = 'SolarCalendar'
                    'CalendarType' = 'Localized'
                    'Eras' = 1
                    'IsReadOnly' = $False
                    'MaxSupportedDateTime' = [DateTime]'9999-12-31T23:59:59.9999999'
                    'MinSupportedDateTime' = [DateTime]'0001-01-01T00:00:00.0000000'
                    'TwoDigitYearMax' = 2029
            }
             
            PS C:\> &$Calendar
 
            AlgorithmType : SolarCalendar
            CalendarType : Localized
            Eras : 1
            IsReadOnly : False
            MaxSupportedDateTime : 9999-12-31 11:59:59 PM
            MinSupportedDateTime : 0001-01-01 12:00:00 AM
            TwoDigitYearMax : 2029
 
        .EXAMPLE
 
            PS C:\>Get-Date | Select-Object -Property * | ConvertTo-Expression | Out-File .\Now.ps1
 
            PS C:\>$Now = .\Now.ps1 # $Now = Get-Content .\Now.Ps1 -Raw | Invoke-Expression
 
            PS C:\>$Now
 
            Date : 1963-10-07 12:00:00 AM
            DateTime : Monday, October 7, 1963 10:47:00 PM
            Day : 7
            DayOfWeek : Monday
            DayOfYear : 280
            DisplayHint : DateTime
            Hour : 22
            Kind : Local
            Millisecond : 0
            Minute : 22
            Month : 1
            Second : 0
            Ticks : 619388596200000000
            TimeOfDay : 22:47:00
            Year : 1963
 
        .EXAMPLE
 
            PS C:\>@{Account="User01";Domain="Domain01";Admin="True"} | ConvertTo-Expression -Expand -1 # Compress the PowerShell output
 
            @{'Admin'='True';'Account'='User01';'Domain'='Domain01'}
 
        .EXAMPLE
 
            PS C:\>WinInitProcess = Get-Process WinInit | ConvertTo-Expression # Convert the WinInit Process to a PowerShell expression
 
        .EXAMPLE
 
            PS C:\>Get-Host | ConvertTo-Expression -Depth 4 # Reveal complex object hierarchies
 
        .LINK
            Invoke-Expression (Alias ConvertFrom-Pson)
    #>

    [CmdletBinding()][OutputType([ScriptBlock])]Param (
        [Parameter(ValueFromPipeLine = $True)][Object[]]$InputObject, [Int]$Depth = 9, [Int]$Expand = 9,
        [Int]$Indentation = 1, [String]$IndentChar = "`t", [ValidateSet("None", "Native", "Cast", "Strict")][String]$TypePrefix = "Cast",
        [String]$NewLine = [System.Environment]::NewLine, [Int]$Iteration = 0
    )
    $PipeLine = $Input | ForEach-Object {$_}; If ($PipeLine) {$InputObject = $PipeLine}
    Function Iterate ($Value) {ConvertTo-Expression @(,$Value) $Depth $Expand $Indentation $IndentChar $TypePrefix $NewLine ($Iteration + 1)}
    Function Embed ($List, $Dictionary) {If ($Iteration -ge $Depth) {If ($Null -ne $Dictionary) {Return "@{}"} Else {Return "@()"}}
        $Items = ForEach ($Key in $List) {If ($Null -ne $Dictionary) {"'$Key'$Space=$Space" + (Iterate $Dictionary.$Key)} Else {Iterate $Key}}
        $Open, $Join, $Separator, $Close = If ($Null -ne $Dictionary) {"@{", ";$Space", "$LineUp$Tab", "}"} Else {"@(", ",$Space", ",$LineUp$Tab", ")"}
        $Open + (&{If (($Iteration -ge $Expand) -or (@($Items).Count -le 1)) {$Items -Join $Join} Else {"$LineUp$Tab$($Items -Join $Separator)$LineUp"}}) + $Close
    }
    $Object = If (@($InputObject).Count -eq 1) {@($InputObject)[0]} Else {$InputObject}
    If ($Null -eq $Object) {"`$Null"} Else {
        $Space = If ($Iteration -gt $Expand) {""} Else {" "}; $Tab = $IndentChar * $Indentation; $LineUp = "$NewLine$($Tab * $Iteration)"
        $Type = $Object.GetType().Name; $Cast = $Null; $Enumerator = $Object.GetEnumerator.OverloadDefinitions
        $Expression = If ($Object -is [Boolean]) {If ($Object) {'$True'} Else {'$False'}}
        ElseIf ($Object -is [Char]) {$Cast = $Type; "'$Object'"}
        ElseIf ($Object -is [String]) {If ($Object -Match "[`r`n]") {"@'$NewLine$Object$NewLine'@$NewLine"} Else {"'$($Object.Replace('''', ''''''))'"}}
        ElseIf ($Object -is [DateTime]) {$Cast = $Type; "'$($Object.ToString('o'))'"}
        ElseIf ($Object -is [TimeSpan] -or $Object -is [Version]) {$Cast = $Type; "'$Object'"}
        ElseIf ($Object -is [ScriptBlock]) {"{$Object}"}
        ElseIf ($Object -is [Enum]) {$Type = "String"; "'$($Object)'"}
        ElseIf ($Object -is [Xml]) {$Cast = "Xml"; $SW = New-Object System.IO.StringWriter; $XW = New-Object System.Xml.XmlTextWriter $SW
            $XW.Formatting = If ($Level -gt $Expand) {"None"} Else {"Indented"}; $XW.Indentation = $Indentation; $XW.IndentChar = $IndentChar
            $Object.WriteContentTo($XW); If ($Level -gt $Expand) {"'$SW'"} Else {"@'$NewLine$SW$NewLine'@$NewLine"}}
        ElseIf ($Object.GetType().Name -eq "DictionaryEntry" -or $Type -like "KeyValuePair*") {$Type = "Hashtable"; Embed $Object.Key @{$Object.Key = $Object.Value}}
        ElseIf ($Object.GetType().Name -eq "OrderedDictionary") {$Type = "Hashtable"; $Cast = "Ordered"; Embed $Object.Keys $Object}
        ElseIf ($Enumerator -match "[\W]IDictionaryEnumerator[\W]") {$Type = "Hashtable"; Embed $Object.Keys $Object}
        ElseIf ($Enumerator -match "[\W]IEnumerator[\W]" -or $Object.GetType().Name -eq "DataTable") {$Type = "Array"; Embed $Object}
        Else {$Property = $Object | Get-Member -Type Property; If (!$Property) {$Property = $Object | Get-Member -Type NoteProperty}
            $Names = ForEach ($Name in ($Property | Select-Object -Expand "Name")) {$Object.PSObject.Properties |
                Where-Object {$_.Name -eq $Name -and $_.IsGettable} | Select-Object -Expand "Name"}
            If ($Property) {$Type = "PSCustomObject"; $Cast = $Type; Embed $Names $Object} Else {$Object}
        }
        $Expression = Switch ($TypePrefix) {
            'None'      {"$Expression"}
            'Native'    {"[$($Object.GetType().Name)]$Expression"}
            'Cast'      {If ($Cast) {"[$Cast]$Expression"} Else {"$Expression"}}
            'Strict'    {If ($Cast) {"[$Cast]$Expression"} Else {"[$Type]$Expression"}}
        }
        If ($Iteration) {$Expression} Else {[ScriptBlock]::Create($Expression)}
    }
} Set-Alias pson ConvertTo-Expression; Set-Alias ctex ConvertTo-Expression
Set-Alias ConvertTo-Pson ConvertTo-Expression -Description "Serializes an object to a PowerShell expression."
Set-Alias ConvertFrom-Pson  Invoke-Expression -Description "Parses a PowerShell expression to an object."