PowerShell icon indicating copy to clipboard operation
PowerShell copied to clipboard

Set-Strictmode should not complain about COUNT & LENGTH properties on elements

Open jpsnover opened this issue 8 years ago • 15 comments

One of the big problems we have had in the language has been not knowing whether the following expression was going to return a single element or an array:

$x = gps $name

We fixed this longstanding problem by masking over the issue allowing elements to be treated as arrays. e.g.

(gps -id 0).count (gps -id 0).length

When you enable strictmode - it complains about these and it shouldn't. I'm about to enable strictmode on a large collection of scripts and I'm sure that is going to find lots of these issue and modifying the code is not going to make the code any better - it will just cause a lot of work and reduce my reliability.

jpsnover avatar Nov 29 '16 00:11 jpsnover

Bumping this because this is something I would also like to see

jcotton42 avatar Aug 08 '17 18:08 jcotton42

Just to provide some additional info:

  • The problem occurs with Set-StrictMode -Version 2 or higher, where attempts to access non-existing properties result in a statement-terminating error (e.g., The property 'Count' cannot be found on this object. Verify that the property exists.).

    • Basically, PowerShell unexpectedly treats properties .Count and .Length as non-existent, even though it normally adds them itself.
  • Fortunately, the other feature related to unifying scalars and collections - indexing - works irrespective of what strict mode is in effect (e.g., (gps -id 0)[0]).

mklement0 avatar Nov 03 '17 03:11 mklement0

Me too! I just added strict mode to my script to help me not call functions incorrectly and now I've lost the cool automatic Count property. Please fix this!

nebosite avatar Jan 05 '18 17:01 nebosite

Any update on this? Just noticed, that it also applies for explicitly defined arrays, which is quite irritating:

Set-StrictMode -Version 7
[pscustomobject[]]$a = $null # e.g. returned by something like 1..5 | Where-Object {$false}
$a.Count

It is especially cumbersome, as checking for the count of elements cannot be done in the same manner as if it was 1 or 2 elements, so an if statement would become like this:

if (-not [bool](Get-Member -InputObject $a -Name "Count" -ErrorAction SilentlyContinue)) {
  # Zero Elements
} elseif ($a.Count -eq 1) {
  # One Element
} else {
  # More than one element
}

It just throws the property not found exception, despite being an array and therefore having that attribute:

The property 'Count' cannot be found on this object. Verify that the property exists.
At line:1 char:1
+ $a.Count
+ ~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], PropertyNotFoundException
    + FullyQualifiedErrorId : PropertyNotFoundStrict

Edit: Also these forms work:

Set-StrictMode -Version 7
$myArray = @()
$myArray.Count
$myArray2 = New-Object int[] 0
$myArray2.Count
$list = New-Object Collections.Generic.List[Int]
$list.Count

But unintuitively this one doesn't

Set-StrictMode -Version 7
$a = 1,2,3,4 | Where-Object {$_ -eq 5}
$a.Count
# And even more confusingly:
function a {
  [OutputType([Int64])]param($a)
  return $a.Where{$_ -gt 5}
}
[Int64[]]$notWorking = 1..5 | a
$notWorking.Count
[pscustomobject[]]$butWorks = 1..5 | a
$butWorks.Count

For the last two it gets even stranger:


# And the really confusing part:
PS C:\> [pscustomobject[]]

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     PSObject[]                               System.Array

PS C:\> [Int64[]]

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int64[]                                  System.Array

Therefore both variables should be of that type and behave as such and not the same as $null.

agowa avatar Aug 20 '20 02:08 agowa

PowerShell lets you assign $null or ([System.Management.Automation.Internal.AutomationNull]::Value, which is what commands that produce no output technically return) to variables that are constrained to:

  • a reference type, which includes arrays and collection types, in which case these values are stored as-is (e.g., [int[]] $a = $null stores $null in $a)
  • a value type, if these values can be converted to that type (e.g, [int] $i = $null, which stores 0 in $a)

Operating on a reference-type-constrained variable set to $null or [System.Management.Automation.Internal.AutomationNull]::Value then operates on these values, which by definition have no properties - other than the engine-intrinsic .Count (and its .Length alias). (Yes, actual array instances have a .Count property via the ICollection interface, but no instance exists in this case).

With strict mode off or at -Version 1, $null.Count and $null.Length do work and return 0, as expected. With -Version 2 and above, we're again seeing the problem described in the OP:

PS> Set-StrictMode -Off; [int[]] $a = $null;  $a.Count, $a.Length
0
0

PS> Set-StrictMode -Version 2; [int[]] $a = $null;  $a.Count, $a.Length
PropertyNotFoundException: The property 'Count' cannot be found on this object. Verify that the property exists.

mklement0 avatar Aug 20 '20 03:08 mklement0

You can also do this now:

[int[]] $a = $null
$itemCount = ($a)?.Count ?? 0

SeeminglyScience avatar Aug 20 '20 14:08 SeeminglyScience

Is that still experimental? 7.0.3 returns only this: You must provide a value expression following the '?' operator.

Edit: Yes it is and it's called PSNullConditionalOperators

agowa avatar Aug 20 '20 21:08 agowa

Indeed, @agowa338; it's still an experimental feature in the current preview (7.1.0-preview.6.) as well.

To refocus on the original problem:

  • It is baffling that the PowerShell engine does not to recognize the properties it itself adds to all objects (that don't already have them) as available properties. True, these engine-added properties (termed intrinsic members) aren't bona fide properties of an object itself (in a .NET type sense), but what matters is that they are usable and have meaningful values.

  • Not doing so severely diminishes the utility of Set-StrictMode -Version 2 and above; personally, Set-StrictMode -Version 1 is the highest version I ever use.

mklement0 avatar Aug 20 '20 22:08 mklement0

I just encountered this problem and came across this issue - +1. I'm using the following workaround for .Count property for now:

$oCount = if ($o -is [array]) { ([array]$o).Count } else { 1 }

vatsan-madhavan avatar Mar 17 '21 00:03 vatsan-madhavan

You can also do this now:

[int[]] $a = $null
$itemCount = ($a)?.Count ?? 0

This will not help if it's not $null

$Var = 'Foo'
${Var}?.Count

Please fix, I was hoping to use StrictMode but with this it's not worth it

ili101 avatar Nov 22 '22 14:11 ili101

I just recently started using strictmode to validate my scripts and was getting some weird failures that turned out to be because of this problem. I (and many others) need .Count to tell us how many elements are in get-childitem, even when we have strictmode enable.

charlesjpalmer avatar Feb 23 '23 19:02 charlesjpalmer

The best workaround I found so far is using Measure-Object. That way, it'll also work with older versions of PowerShell (mainly the windows one, for cases where it is needed to use the native modules that are still incompatible with the newer versions...). That should work in all versions, even in Version 1.0

  • (Measure-Object -InputObject $a).Count
  • ($a | Measure-Object).Count

Maybe that should even be the preferred solution, banning these dynamic count and length properties in strict mode entirely, as they're not sure to be there, or an object could have overwritten them with its custom Count property.

I came to that conclusion after having had to deal with countless issues because of the automatic unwrapping of arrays with only one element in PowerShell, which sometimes is handled very poorly, esp. by 3rd party modules... So if there is only one element, you get a string returned. But if there are more, you get an array of strings.

And then you may end up with stuff like (within the module, where you cannot quickly fix it by e.g., add a [pscustomobject[]]):

$queue = Get-3rdPartyQueue
if ($queue.Length -gt 20) {
  Do-Stuff
} else {
  Wait-ForMoreStuff
}

And now, if only one element is returned by Get-3rdPartyQueue PowerShell will unwrap it, and the .Length will not be called on an array but on the object itself (that should have been the array's element). And assuming that object has a .Length like, e.g., String does it'll cause logic errors...

agowa avatar Feb 24 '23 09:02 agowa

Is this still open? Just hit that bug again in 7.3.7, but in a kinda "new and interesting way".

What works:

Set-StrictMode -Version latest
[string[]]$a = @()
$a.Count

What doesn't:

Set-StrictMode -Version latest
[string[]]$a = @() | Select-Object
$a.Count
Set-StrictMode -Version latest
[string[]]$a = @()
($a | Select-Object).Count

Something is very strange here, for some reason the Select-Object is changing the type...

agowa avatar Oct 13 '23 14:10 agowa

I have closed the linked issue #16427 (Resolution-No Activity), assuming that this one stays open as it still exists in:

$PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.3.9
PSEdition                      Core
GitCommitId                    7.3.9
OS                             Microsoft Windows 10.0.19045
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

iRon7 avatar Nov 15 '23 09:11 iRon7

Thanks @iRon7, yea the Resolution-No Activity thingy of the bot is annoying. I just wasted valuable time c'n'p-ing a dummy message into a lot of tickets here. If I have to do that again I'll probably just create a throwaway github account and a bot that does it for me...

agowa avatar Nov 16 '23 14:11 agowa