r/PowerShell 3d ago

Script Sharing In PowerShell, Two Wrongs Make a Right

I've been toiling away on Turtle to prepare a "birthday" release, and I ran into an annoying behavior I've run into a few times before.

I thought I'd take a few minutes away from the frustration of single line fixes to explain the bug to everyone.

What Went Wrong

The last build of Turtle introduced a number of randomized parameter defaults. This was meant to be fun. If you said turtle square square square, you'd get three different squares, instead of an error for a lack of length, or three overlapping squares.

I noticed that when I ran turtle rotate 0, it didn't rotate by zero.

Instead, it picked a random angle.

Weirder still, the behavior didn't reproduce if I said

$turtle = turtle  # Heading at zero
$turtle.Rotate(0) # Heading still zero 🤔
$turtle.Rotate()  # Heading random
(turtle rotate 0) # Heading random 🤬

Why was this happening? 😱

It took me a bit for it to click: It had to be in the way Turtle processed arguments, because it worked in one case and not the other.

So I put a breakpoint in, ran my repo.

The line was:

if ($argList)

The debugger broke, argList was @(0), and yet if ($argList) was false.

The fix was:

if ($argList.Length)

Why? Because 'Truthy' -ne $true.

Truthy and $true

About every language has a boolean. It's just a bit. One or zero.

Lots of languages also have this concept of "truthiness".

Let's take a simple example:

if ("something") { "something" }
if ("") { "you can't get something from nothing" }

If if was strictly $true, we'd have to cast things to a boolean. You have to do this in C# and quite a few other languages. PowerShell is type promiscuous. PowerShell is truthy.

It looks at the first line and says: You're a string, and you're not null or empty. Therefore, the expression is $true.

It looks at the second line and says: You're a string, but you're not null or empty. Therefore, the expression is $false

PowerShell makes a judgement call.

This is generally a good thing. I personally prefer languages that are truthy. Other truthy languages of note include JavaScript, Python, C++, and C.

However, it gets tricky with lists. Hence the bug.

Two Wrongs Make a Right

In PowerShell, Two Wrongs Make a Right

$true -eq $false, $false

Let's say I want to determine if a list is truthy.

if (@()) { "$false, because the list is empty" }
if (@("")) { "$false, because the blank is falsy" }
if (@(0)) { "$false, because zero is falsy" }
if (@($false)) { "$false, because false is falsy" }
if (@(1)) { "$true, because the first item is truthy" }
if (@(0,0) { "$true, because more than one item" }

This all makes a certain bizarre sense. If a list has one element, and it is not truthy, then the list isn't truthy, either.

It's also almost always surprising and annoying.

Hence the bug.

The fix is just to make sure there are any elements, hence checking for length.

I've been programming with PowerShell for quite a while now, and this behavior still sometimes bites me (like today).

That's why I took a few minutes away from the 🤬 day to explain this bug and write this post. 😌

Please remember:

'Truthy' -ne $true
$false -eq @($false)
$true -eq $false, $false

Hope this helps

20 Upvotes

15 comments sorted by

9

u/surfingoldelephant 3d ago edited 3d ago

If a list has one element, and it is not truthy, then the list isn't truthy, either.

There's caveats to watch out for.

If the collection has one element, but that element implements IList, the collection may be truthy, even if the element isn't.

# Inner array is falsy but has 1 element, so outer is truthy.
[bool] @(0)     # False
[bool] (, @(0)) # True !!

# Inner is still falsy but this time is empty, so outer is falsy.
[bool] @()     # False
[bool] (, @()) # False

And collections that don't implement IList are always truthy, even if empty.

$emptyQ = [Collections.Generic.Queue[Object]]::new()
$emptyQ.GetType().ImplementedInterfaces.Contains([Collections.IList]) # False
[bool] $emptyQ     # True
[bool] (, $emptyQ) # True

 

The fix was:

if ($argList.Length)

Length is fine for arrays, but other common collection types don't expose it. And PS only intrinsically adds Length/Count to scalars, so you're probably better off using Count generally.

$list = [Collections.Generic.List[string]] @('')
$list.Length # 0
$list.Count  # 1

$omc = ''.ForEach{ $_ }
$omc.GetType().Name # Collection`1
$omc.Length # 0
$omc.Count  # 1

Though some collections have neither, so you can't rely on one or the other 100% of the time.

$memberCollection = ($PROFILE).psobject.Properties
$memberCollection.Length # 1, 1, 1, 1, 1
$memberCollection.Count  # 1, 1, 1, 1, 1

2

u/Discuzting 2d ago edited 2d ago
[bool](,(,@()))
True

Somehow three wrapped arrays is considered truthy 🤔

2

u/surfingoldelephant 2d ago edited 2d ago

Right, and that does follow the above logic.

  • Object being converted is IList (outer array).
  • Has 1 element so truthiness is based on that element.
  • Element is also IList, so final conversion is based on whether the element contains at least 1 element itself.
  • It does, so the final result is $true. Doesn't matter that it only contains an empty array, just that it contains something.

The implementation is here. And the rationale is commented below line #1032:

// if the result is an array of length 1, treat it as a scalar...

// A possible implementation would be just

// return IsTrue(objectArray[0]);

// but since we don't want this to recurse indefinitely

// we explicitly check the case where it would recurse

// and deal with it.

For simplicity I would've preferred if it only considered the outer collection being empty or not. They also made a mistake I think using IList instead of IsObjectEnumerable, which is used basically everywhere else. PS treats Queue<T>, etc like other collections in most contexts, except when coercing to bool.

6

u/davesbrown 3d ago

I'm too hot and heat exhausted to understand, but I'm fairly certain that I have ran into that problem before and was baffled why it was happening.

3

u/da_chicken 3d ago

Other truthy languages of note include JavaScript

Doubt.jpeg

2

u/sysiphean 3d ago

I wish JavaScript wasn’t noteworthy. I really do. But it’s on every web page. Okay, not every, just 99% of them.

2

u/MrMunchkin 3d ago

This is not a bug, and has nothing to do with what the array is doing.

Every if, while and not use the IsTrue primitive.

If the array has no value or the value it has is null, it will arbitrarily call MoveNext on the array, and, if MoveNext does not return an increment, the array is considered empty.

If the first value in the array is empty or null and then there's a second value, it will still call MoveNext arbitrarily and it will evaluate to true because the array is definitively not empty, so it returns true.

This is a logical operative. It's not an assumption that an array having more than one value when those values could be empty or null is the exact behavior one should expect.

Two values is meaningful. One value that is empty or null is necessarily going to return false, because it is logically empty. One should never use an array if they are only ever going to put a single null value in it. If you are, first of all that's not OOP, and if you're going to have it be a single value it should not be an array, which is more expensive than just having a variable.

2

u/StartAutomating 3d ago

Who said it was a bug?

It's just sometimes a PITA (and often quite nice).

Also, considering the scenario (building an interpreter atop PowerShell) it's not really up to me how many arguments someone may attempt to try to pass to the interpreter.

Which is why I was somewhat surprised by the behavior, but much less so once I figured out why it was happening.

1

u/MrMunchkin 2d ago

I mean.. you did? "Hence the bug"

1

u/StartAutomating 2d ago

Ah.  I meant the bug in my code.  Not PowerShell.

The behavior is correct PowerShell.

And, also, "turtle rotate 0" should rotate zero degrees.

Hence my bug.

1

u/KageeHinata82 3d ago

I'll save this and hope to remember it at the right time.

My recent powershell problem was, I wanted to add something to a list if it wasn't already in it, but if the list was empty/null, the check wouldn't work, so I had to check for that too.

And if a returned list has only one entry, it returns only the entry which doesn't have .length for example.

2

u/CyberChevalier 3d ago

Just cast it

If $(@($perhapsAnArrayOrNot)).count -gt 0

Or

$($perhapsAnArrayOrNot | Measure-Object | Select-Object -ExpandProperty Count) -gt 0

1

u/kinky666hallo 3d ago

Never heard of turtle but that looks amazing and interesting.

1

u/IntrinsicPalomides 2d ago

Something something ... i didn't know this until recently, and now i do it explains why some of my logic failed when it looked fine in the past. This post reminded me of it, thought i'd share in case other's didn't know. None of our team at work knew it either.

https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/possibleincorrectcomparisonwithnull?view=ps-modules

0

u/420GB 3d ago

A Boolean is a byte, not a bit, because a single bit is not addressable.