1

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.

HASJ
  • 57
  • 7
  • As an aside: I assume `[string]$FileList` is meant to be `[string[]]$FileList` - otherwise, your `foreach` loop won't work. – mklement0 Mar 21 '20 at 12:35

1 Answers1

3

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.

    • It is somewhat unfortunate that member-access enumeration behaves this way; the behavior can be even more surprising when the property values are arrays themselves: the resulting arrays are then concatenated instead of becoming individual subarrays in the output array - see this GitHub issue.
  • 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.

mklement0
  • 382,024
  • 64
  • 607
  • 775