'Understanding Powershell: example - Convert JSON to CSV
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.
Solution 1:[1]
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.
Solution 2:[2]
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
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |