1

If I have:

$a=$null
$b=''
$c=@($null,$null)
$d='foo'
write-host $a
write-host $b
write-host $c
write-host $d

the output is

foo

I'd really like to be able to easily get output that shows the variable values, e.g.,

$Null
''
@($Null,$Null)
'foo'

I can write a function to do this, but I'm guessing/hoping there's something built-in that I'm missing. Is there, or does everyone just roll their own function for something like this?

At the moment the quickest thing I've come up with is running a value through ConvertTo-Json before printing it. It doesn't handle a plain $null, but it shows me the other values nicely.

aggieNick02
  • 2,557
  • 2
  • 23
  • 36

3 Answers3

2

What you're looking for is similar to Ruby's .inspect method. It's something I always loved in Ruby and do miss in PowerShell/.Net.

Unfortunately there is no such thing to my knowledge, so you will somewhat have to roll your own.

The closest you get in .Net is the .ToString() method that, at a minimum, just displays the object type (it's inherited from [System.Object]).

So you're going to have to do some checking on your own. Let's talk about the edge case checks.

Arrays

You should check if you're dealing with an array first, because PowerShell often unrolls arrays and coalesces objects for you so if you start doing other checks you may not handle them correctly.

To check that you have an array:

$obj -is [array]

1 -is [array]  # false
1,2,3 -is [array]  # true
,1 -is [array]  #true

In the case of an array, you'll have to iterate it if you want to properly serialize its elements as well. This is basically the part where your function will end up being recursive.

function Format-MyObject {
param(
    $obj
)

    if ($obj -is [array]) {
        # initial array display, like "@(" or "["
        foreach ($o in $obj) {
            Format-MyObject $obj
        }
        # closing array display, like ")" or "]"
    }
}

Nulls

Simply check if it's equal to $null:

$obj -eq $null

Strings

You can first test that you're dealing with a string by using -is [string].

For empty, you can compare the string to an empty string, or better, to [string]::Empty. You can also use the .IsNullOrEmpty() method, but only if you've already ruled out a null value (or checked that it is indeed a string):

if ($obj -is [string) {
    # pick one

    if ([string]::IsNullOrEmpty($obj)) {
        # display empty string
    }

    if ($obj -eq [string]::Empty) {
        # display empty string
    }

    if ($obj -eq "") { # this has no advantage over the previous test
        # display empty string
    }
}

Alternative

You could use the built-in XML serialization, then parse the XML to get the values out of it.

It's work (enough that I'm not going to do it in an SO answer), but it removes a lot of potential human error, and sort of future-proofs the approach.

The basic idea:

$serialized = [System.Management.Automation.PSSerializer]::Serialize($obj) -as [xml]

Now, use the built in XML methods to parse it and pull out what you need. You still need to convert some stuff to other stuff to display the way you want (like interpreting <nil /> and the list of types to properly display arrays and such), but I like leaving the actual serialization to an official component.

Quick example:

[System.Management.Automation.PSSerializer]::Serialize(@(
    $null,
    1,
    'string',
    @(
        'start of nested array',
        $null,
        '2 empty strings next',
        '', 
        ([string]::Empty)
    )
)
)

And the output:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Object[]</T>
      <T>System.Array</T>
      <T>System.Object</T>
    </TN>
    <LST>
      <Nil />
      <I32>1</I32>
      <S>string</S>
      <Obj RefId="1">
        <TNRef RefId="0" />
        <LST>
          <S>start of nested array</S>
          <Nil />
          <S>2 empty strings next</S>
          <S></S>
          <S></S>
        </LST>
      </Obj>
    </LST>
  </Obj>
</Objs>
briantist
  • 45,546
  • 6
  • 82
  • 127
  • 1
    Nice answer; thanks for pointing out PSSerializer. That seems like the most versatile and future-proof since I imagine that is what PS uses for all of its data transfer, even if it is a little verbose. Between that and ConvertTo-Json -Depth 100 for general plain datatypes and certain collections, I think I've now got a couple good options. – aggieNick02 Oct 18 '17 at 19:55
2

I shared two functions that reveal PowerShell values (including the empty $Null's, empty arrays etc.) further than the usually do:

  • One that the serializes the PowerShell objects to a PowerShell Object Notation (PSON) which ultimate goal is to be able to reverse everything with the standard command Invoke-Expression and parse it back to a PowerShell object.

  • The other is the ConvertTo-Text (alias CText) function that I used in my Log-Entry framework. note the specific line: Log "Several examples that usually aren't displayed by Write-Host:" $NotSet @() @(@()) @(@(), @()) @($Null) that I wrote in the example.


Function Global:ConvertTo-Text1([Alias("Value")]$O, [Int]$Depth = 9, [Switch]$Type, [Switch]$Expand, [Int]$Strip = -1, [String]$Prefix, [Int]$i) {
    Function Iterate($Value, [String]$Prefix, [Int]$i = $i + 1) {ConvertTo-Text $Value -Depth:$Depth -Strip:$Strip -Type:$Type -Expand:$Expand -Prefix:$Prefix -i:$i}
    $NewLine, $Space = If ($Expand) {"`r`n", ("`t" * $i)} Else{"", ""}
    If ($O -eq $Null) {$V = '$Null'} Else {
        $V = If ($O -is "Boolean")  {"`$$O"}
        ElseIf ($O -is "String") {If ($Strip -ge 0) {'"' + (($O -Replace "[\s]+", " ") -Replace "(?<=[\s\S]{$Strip})[\s\S]+", "...") + '"'} Else {"""$O"""}}
        ElseIf ($O -is "DateTime") {$O.ToString("yyyy-MM-dd HH:mm:ss")} 
        ElseIf ($O -is "ValueType" -or ($O.Value.GetTypeCode -and $O.ToString.OverloadDefinitions)) {$O.ToString()}
        ElseIf ($O -is "Xml") {(@(Select-XML -XML $O *) -Join "$NewLine$Space") + $NewLine}
        ElseIf ($i -gt $Depth) {$Type = $True; "..."}
        ElseIf ($O -is "Array") {"@(", @(&{For ($_ = 0; $_ -lt $O.Count; $_++) {Iterate $O[$_]}}), ")"}
        ElseIf ($O.GetEnumerator.OverloadDefinitions) {"@{", (@(ForEach($_ in $O.Keys) {Iterate $O.$_ "$_ = "}) -Join "; "), "}"}
        ElseIf ($O.PSObject.Properties -and !$O.value.GetTypeCode) {"{", (@(ForEach($_ in $O.PSObject.Properties | Select -Exp Name) {Iterate $O.$_ "$_`: "}) -Join "; "), "}"}
        Else {$Type = $True; "?"}}
    If ($Type) {$Prefix += "[" + $(Try {$O.GetType()} Catch {$Error.Remove($Error[0]); "$Var.PSTypeNames[0]"}).ToString().Split(".")[-1] + "]"}
    "$Space$Prefix" + $(If ($V -is "Array") {$V[0] + $(If ($V[1]) {$NewLine + ($V[1] -Join ", $NewLine") + "$NewLine$Space"} Else {""}) + $V[2]} Else {$V})
}; Set-Alias CText ConvertTo-Text -Scope:Global -Description "Convert value to readable text"

ConvertTo-Text

The ConvertTo-Text function (Alias CText) recursively converts PowerShell object to readable text this includes hash tables, custom objects and revealing type details (like $Null vs an empty string).

Syntax

ConvertTo-Text [<Object>] [[-Depth] <int>] [[-Strip] <int>] <string>] [-Expand] [-Type]

Parameters

<Object>
The object (position 0) that should be converted a readable value.

-⁠Depth <int>
The maximal number of recursive iterations to reveal embedded objects.
The default depth for ConvertTo-Text is 9.

-Strip <int>
Truncates strings at the given length and removes redundant white space characters if the value supplied is equal or larger than 0. Set -Strip -1 prevents truncating and the removal of with space characters.
The default value for ConvertTo-Text is -1.

-Expand
Expands embedded objects over multiple lines for better readability.

-Type
Explicitly reveals the type of the object by adding [<Type>] in front of the objects.

Note: the parameter $Prefix is for internal use.

Examples

The following command returns a string that describes the object contained by the $var variable:

ConvertTo-Text $Var

The following command returns a string containing the hash table as shown in the example (rather then System.Collections.DictionaryEntry...):

ConvertTo-Text @{one = 1; two = 2; three = 3}

The following command reveals values (as e.g. $Null) that are usually not displayed by PowerShell:

ConvertTo-Text @{Null = $Null; EmptyString = ""; EmptyArray = @(); ArrayWithNull = @($Null); DoubleEmptyArray = @(@(), @())} -Expand

The following command returns a string revealing the WinNT User object up to a level of 5 deep and expands the embedded object over multiple lines:

ConvertTo-Text ([ADSI]"WinNT://./$Env:Username") -Depth 5 -Expand
iRon
  • 20,463
  • 10
  • 53
  • 79
  • This is pretty cool. I recommend you clean it up a bit, make a module out of it, put it on GitHub, and then publish it to PowerShell Gallery so it can be used as a package. – briantist Oct 18 '17 at 20:36
0

A quick self-rolled option good for some datatypes.

function Format-MyObject {
param(
    $obj
)
    #equality comparison order is important due to array -eq overloading
    if ($null -eq $obj)
    {
        return 'null'
    }
    #Specify depth because the default is 2, because powershell
    return ConvertTo-Json -Depth 100 $obj 
}
aggieNick02
  • 2,557
  • 2
  • 23
  • 36