2

Here's something I don't understand.

When I define a variable:

$v = [byte]2, [byte]3

and check its type:

$v.getType().name

I get

Object[]

I then format $v:

'{0} {1}' -f $v

which prints

2 3

Now, if I get a file's first two bytes:

$f = (get-content 'xyz.txt' -encoding byte -readCount 2 -totalCount 2)

and check its type:

$f.getType().name

I get the same type as before: Object[].

However, unlike with $v, I cannot format $f:

'{0} {1}' -f $f

I get the error message Error formatting a string: Index (zero based) must be greater than or equal to zero and less than the size of the, although the length of the array is 2:

$f.length

returns

2

I don't understand why this is and would appreciate an explanation.

mklement0
  • 382,024
  • 64
  • 607
  • 775
René Nyffenegger
  • 39,402
  • 33
  • 158
  • 293

1 Answers1

4
  • The behavior should be considered a bug in the -f operator; it is present as of v7.1 and reported in GitHub issue #14355; it does not affect other operators with array operands, such as -split or -in.

  • The workaround is to cast $f to [array] or, if creating a copy of the array is acceptable, @($f):

'abc' > xyz.txt

$f = get-content 'xyz.txt' -encoding byte -readCount 2 -totalCount 2

'{0} {1}' -f ([array] $f)

Note: Using @(), the array-subexpression operator - ... - @($f) - as Mathias R. Jessen notes - is the even simpler option, but do note that using @() involves cloning (creating a shallow copy of) the array, whereas the [array] cast in this case does not.

The alternative is to apply the [array] cast as a type constraint (by placing it to the left of the $f = ... assignment):

'abc' > xyz.txt

[array] $f = (get-content 'xyz.txt' -encoding byte -readCount 2 -totalCount 2)

'{0} {1}' -f $f

Note:

  • In PowerShell [Core] v6+, you must use -AsByteStream in lieu of -Encoding Byte.

  • The problem can also be avoided if -ReadCount 2 is omitted, but note that that decreases the performance of the command, because the bytes are then emitted one by one; that is, with -ReadCount 2 -TotalCount 2 a single object is emitted that is a 2-byte array as a whole, whereas just -TotalCount 2 emits the individual bytes, one by one to the pipeline, in which case it is then the PowerShell engine itself that collects these bytes in an [object[]] array for the assignment.

  • Note that applying @() directly to the command - @(get-content ...) - would not work in this case, because @(), due to parameter combination -ReadCount 2 -TotalCount 2, receives a single output object that happens to be an array as a whole and therefore wraps that single object in another array. This results in a single-element array whose element is the original 2-element array of bytes; for more information about how @(...) works, see this answer.


Background information:

The problem is an invisible [psobject] wrapper around each array returned by Get-Content -ReadCount (just one in this case), which unexpectedly causes the $f array passed to -f not to be recognized as such.

Note that PowerShell's other array-based operators, such as -in and -replace, are not affected.

The wrapper can be bypassed in two ways:

  • $f.psobject.BaseObject

  • casting to [array], as shown at the top.

Note:

  • Generally, output objects produced by cmdlets - as opposed to output produced by PowerShell code - have generally invisible [psobject] wrappers; mostly, they are benign, because PowerShell usually just cares about the .NET object being wrapped, not about the wrapper, but on occasion problems arise, such as in this case - see GitHub issue #5579 for a discussion of the problem and other contexts in which it manifests.

  • In order to test if a given object has a [psobject] wrapper, use -is [psobject]; e.g.:

$var = 1
$var -is [psobject] # -> $false

$var = Write-Output 1
$var -is [psobject] # -> $true, due to use of a cmdlet.

# You can also test command output directly.
(Write-Output 1) -is [psobject]  # -> $true
mklement0
  • 382,024
  • 64
  • 607
  • 775