2

I've read several posts (like Convert JSON to CSV using PowerShell) regarding using PowerShell to CSV. I have also read that it is relatively poor form to use the pipe syntax in scripts -- that it's really meant for command line and can create a hassle for developers to maintain over time.

Using this sample JSON file...

[
    {
        "a":  "Value 1",
        "b":  20,
        "g":  "Arizona"
    },
    {
        "a":  "Value 2",
        "b":  40
    },
    {
        "a":  "Value 3"
    },
    {
        "a":  "Value 4",
        "b":  60
    }
]

...this code...

((Get-Content -Path $pathToInputFile -Raw) | ConvertFrom-Json) | Export-CSV $pathToOutputFile -NoTypeInformation

...creates a file containing CSV as expected.

"a","b","g"
"Value 1","20","Arizona"
"Value 2","40",
"Value 3",,
"Value 4","60",

This code...

$content = Get-Content -Path $pathToInputFile -Raw
$psObj = ConvertFrom-Json -InputObject $content
Export-Csv -InputObject $psObj -LiteralPath $pathToOutputFile -NoTypeInformation

...creates a file containing nonsense:

"Count","Length","LongLength","Rank","SyncRoot","IsReadOnly","IsFixedSize","IsSynchronized"
"4","4","4","1","System.Object[]","False","True","False"

It looks like maybe an object definition(?).
What is the difference? What PowerShell nuance did I miss when converting the code?

The answer to Powershell - Export a List of Objects to CSV says the problem is from the -InputObject option causing the object, not it's contents, to be sent to Export-Csv, but doesn't state how to remedy the problem without using the pipe syntax. I'm thinking something like -InputObject $psObj.contents. I realize that's not a real thing, but I Get-Members doesn't show me anything that looks like it will solve this.

dougp
  • 2,810
  • 1
  • 8
  • 31
  • 1
    `-InputObject` binds to _each object passed through the pipeline_, it's not meant to be used like what you're doing on your second example. – Santiago Squarzon Jan 04 '22 at 21:19
  • OK. Is it possible to solve this problem without using pipes? – dougp Jan 04 '22 at 21:24
  • The cmdlet is built to work with the pipeline, the _`begin` block_ is the one that captures the "Headers" of the first object passed through then it _process_ each pipeline object. This is based on assumptions but I have a feeling that's how it works. – Santiago Squarzon Jan 04 '22 at 21:28
  • If you try something like this: `$json.ForEach({ConvertTo-Csv -InputObject $_})` you would see that each object generates a new set of _"Headers"_ based on the object's properties. – Santiago Squarzon Jan 04 '22 at 21:29
  • I can confirm that the schema of the CSV matches that of the first object. That's actually the larger problem I'm trying to solve. Looks like I'll use pipes. thanks. – dougp Jan 04 '22 at 21:35
  • Here you have 2 examples of how you can solve your problem. Someone asked something very similar not long ago: https://stackoverflow.com/questions/70484662/powershell-export-csv-missing-columns/70484836#70484836 and https://stackoverflow.com/questions/44428189/not-all-properties-displayed – Santiago Squarzon Jan 04 '22 at 22:11

2 Answers2

2

This is not meant as an answer but just to give you a vague representation of what ConvertTo-Csv and Export-Csv are doing and to help you understand why -InputObject is meant to be bound from the pipeline and should not be used manually.

function ConvertTo-Csv2 {
    param(
        [parameter(ValueFromPipeline)]
        [Object] $InputObject
    )

    begin {
        $isFirstObject = $true
        filter Normalize {
            if($_ -match '"') { return $_.Replace('"','""') }
            $_
        }
    }

    process {
        if($isFirstObject) {
            $headers = $InputObject.PSObject.Properties.Name | Normalize
            $isFirstObject = $false
            [string]::Format('"{0}"', [string]::Join('","', $headers))
        }

        $values = foreach($value in $InputObject.PSObject.Properties.Value) {
            $value | Normalize
        }
        [string]::Format('"{0}"', [string]::Join('","', $values))
    }
}

As we can observe, there is no loop enumerating the $InputObject in the process block of this function, yet, because of how this block works, each object coming from the pipeline is processed and converted to a Csv string representation of the object.

Within a pipeline, the Process block executes once for each input object that reaches the function.

If instead, we attempt to use the InputObject parameter from the function, the object being passed as argument will be processed only once.

Calling the function at the beginning, or outside of a pipeline, executes the Process block once.

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Thank you. Reading about and experimenting with the BEGIN-PROCESS-END pattern in PowerShell has been rather enlightening. Huge boost to my understanding. – dougp Jan 05 '22 at 22:30
0

Get-Members doesn't show me anything that looks like it will solve this

Get-Member

It's because how you pass values has different behavior.

The pipeline enumerates values, it's almost like a foreach($item in $pipeline). Passing by Parameter skips that

Here I have an array of 3 letters.

$Letters = 'a'..'c'

I'm getting different types

Get-Member -InputObject $Letters
# [Object[]]

# [char]
$letters | Get-Member

Processed for each item

$letters | ForEach-Object { 
    "iteration: $_"
}
iteration: a
iteration: b
iteration: c

Compare to

ForEach-Object -InputObject $Letters { 
    "iteration: $_"
}
iteration: a b c

Detecting types

Here's a few ways to inspect objects.

using ClassExplorer

PS> ($Letters).GetType().FullName
PS> ($Letters[0]).GetType().FullName  # first child

    System.Object[]
    System.Char

PS> $Letters.count
PS> $Letters[0].Count

    3
    1

$Letters.pstypenames -join ', '
$Letters[0].pstypenames -join ', '

    System.Object[], System.Array, System.Object
    System.Char, System.ValueType, System.Object

Tip: $null.count always returns 0. It does not throw an error.

if($neverExisted.count -gt 1) { ... }

Misc

I have also read that it is relatively poor form to use the pipe syntax in scripts

This is not true, Powershell is designed around piping objects.

Tip: $null.count always returns 0. It does not throw an error.

Maybe They were talking about

Example2: slow operations

Some cases when you need something fast, the overhead to Foreach-Object over a foreach can be an issue. It makes it so you have to use some extra syntax.

If you really need speed, you should probably be calling dotnet methods anyway.

Example1: Piping when you could use a parameter

I'm guessing they meant piping a variable in cases where you can pass parameters?

$text = "hi-world"

# then
$text | Write-Host 

# vs
Write-Host -InputObject $Text
ninMonkey
  • 7,211
  • 8
  • 37
  • 66
  • Sorry, but I'm pretty new to PowerShell. I don't understand what's happening in those two examples. That looks exactly opposite what I'm seeing with `Export-CSV`. Your example appears to show that using `-InputObject` causes the content, not the object (wrapper) to be passed in and the pipe is passing the object. I could test and inspect the objects, but your code throws an error. – dougp Jan 04 '22 at 22:00