I am trying to learn PowerShell by translating an old batch script that I made for converting videos using FFmpeg.
That has almost nothing to do with the issue at hand, I believe.
This is the code snippet giving me trouble:
[string]$FileList = (Get-Clipboard).Split("`n")
[int]$Counter = 0
$List = @(ForEach ($i in $FileList)
{
[PSCustomObject]
@{
VideoHeight = (ffprobe.exe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 "$i")
VideoDuration = (ffprobe.exe -v error -select_streams v:0 -show_entries stream=duration -of csv=s=x:p=0 "$i")
}
$Counter++
})
Store in $FileList
the list of files from the Clipboard separated by a new line.
Store in $Counter
the integer 0
.
For each item in $FileList
, create a new object containing the item's height and duration and add 1 to $Counter
.
Seems straight-forward, right? Here's the catch: if there is only one file with a height of 1080 in $FileList
, $List.VideoHeight[0]
will return only 1
but if there is at least two files in $FileList
, $List.VideoHeight[0]
will return 1080
.
Command line output:
Single File
$List.VideoHeight:
1080
$List.VideoHeight[0]:
1
Multiple Files
$List.VideoHeight:
1080
1080
720
$List.VideoHeight[0]:
1080
Any ideas what is happening here? I am stuck.
Use $List[0].VideoHeight
, not $List.VideoHeight[0]
.
After all, from a conceptual standpoint, you want to get the .VideoHeight
value of the first list item ($List[0]
), not the first element of the whole list's video-height values ($List.VideoHeight
).[1]
The reason it works with multiple items in $List
is that PowerShell's member-access enumeration then returns an array of .VideoHeight
property values, where indexing with [0]
works as expected.
The reason it doesn't work as expected with a single item in $List
is that then only a scalar (single) .VideoHeight
property value is returned, and that scalar is of type [string]
. Indexing into a single string returns the individual characters in the string, which is what you saw.
Simple demonstration:
PS> ([pscustomobject] @{ VideoHeight = '1080' }).VideoHeight[0]
1 # [char] '1', the first character in string '1080'
vs.
PS> ([pscustomobject] @{ VideoHeight = '1080' },
[pscustomobject] @{ VideoHeight = '1081' }).VideoHeight[0]
1080 # [string] '1080', the first element in array '1080', '1081'
So there are two factors contributing to the unexpected behavior:
PowerShell's member-access enumeration applies the same logic as when pipeline output is collected: If the collection whose members are being enumerated happens to have just one element, that member (property) value is returned as-is; only 2 or more elements result in an [object[]]
array of values.
The standard behavior of the .NET System.String
type ([string]
), which allows direct indexing of the characters that make up the string (e.g. "foo"[0]
yielding [char] 'f'
).
Strictly speaking, it is whatever language consumes the type that implements the [...]
indexer syntax, which is a form of syntactic sugar; the underlying type only has a parameterized property named Chars
, which both C# and PowerShell expose more conveniently via [...]
.
In the case of [string]
, however, this unfortunately conflicts with PowerShell's unified handling of collections and scalars, where usually $scalar[0]
is the same as $scalar
- see this answer for background information.
[1] If collecting the values of all $List
.VideoHeight
property values consistently returned an array (why it doesn't is explained in the second section), the two statements would be functionally equivalent, though $List[0].VideoHeight
is still preferable for efficiency (no intermediate array of property values must be constructed) and also for the sake of avoiding potential member-name conflicts, which member-access enumeration inherently entails - see this GitHub issue.