Powershell: weird behavior with && and array concatenation

Created on 19 Feb 2020  路  9Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

function read_services {
    $entry
    $entries = @()

    foreach ($line in get-content /windows/system32/drivers/etc/services) {
        ($name, $port, $description) = $line -split ' +'

        $entry && ($entries += $entry)

        $entry = [ordered]@{
            'name'    = $name;
            'port'    = $port;
            'comment' = $description;
        };
    }
}

$services = read_services

write-host $services.count

Output:

41903

With the && construct changed to an if statement, it works correctly:

function read_services {
    $entry
    $entries = @()

    foreach ($line in get-content /windows/system32/drivers/etc/services) {
        ($name, $port, $description) = $line -split ' +'

        if ($entry) { $entries += $entry }

        $entry = [ordered]@{
            'name'    = $name;
            'port'    = $port;
            'comment' = $description;
        };
    }

    return $entries
}

$services = read_services

write-host $services.count

Output:

287

Environment data

Name                           Value
----                           -----
PSVersion                      7.0.0-rc.2
PSEdition                      Core
GitCommitId                    7.0.0-rc.2
OS                             Microsoft Windows 10.0.19551
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Issue-Question Resolution-Answered

Most helpful comment

Yes $x && $y is not the same as $x; if ($x) { $y }. It's instead the same as $x; if ($?) { $y }.

&& and || are designed to encode command success, not expression value. In that case, you should use if or a ternary (or add your 馃憤 to this proposal).

All 9 comments

Using an _expression_ such as $entry as the LHS of && is pointless, because && and || act on the _success status_ of the LHS, and an expression's success status is _always_ $true (unless a statement-terminating error occurs, in which case the entire statement is terminated).

PS> $null && 'yes'
yes

Use && and || only with _commands_, where the implied $? value - not the command's _output_ - determines the success status.

I think @rjmholt was the one doing the work with && and || if I'm not mistaken? 馃 (Feel free to correct me if my memory is incorrect!)

Essentially, && in PS does _not_ work like it does in C#. It instead behaves somewhat more closely to how it behaves in Bash and Bash-like shells, where it acts on the _perceived success or failure of the prior command_.

&& only triggers the following statement if the prior statement succeeds. || is the exact opposite. Whether or not a statement _succeeds_ is typically down to whether or not it generates an error of some kind, and nothing more.

You can do something similar to what you seem to be looking to do with PS's logical -and operator to a similar effect:

$entry -and ($entries += $entry)

However, do note that -and is a logical expression and will always return true or false. If you want this to behave like the if statement, you'll need to also capture or redirect the output:

$entry -and ($entries += $entry) > $null

# or

$null = $entry -and ($entries += $entry)

A few asides, @rkitover:

  • $entry by itself doesn't declare a variable; it outputs the value of a non-existent $entry variable, which defaults to $null.

  • No need for (...) around destructuring assingments.

  • _Unary_ -split splits by nonempty runs of whitespace (trimming and leading whitespace too)

Thus:

$name, $port, $description = -split $line
  • Building arrays with += is inefficient (a _new_ array is created in each iteration) and verbose; simply treat the entire foreach loop as an expression and let PowerShell collect the results in an array for you, which bypasses your initial problem:
[array] $entries  = foreach ($line in get-content /windows/system32/drivers/etc/services) {
       $name, $port, $description = -split $line
       # Construct and output 
       [ordered] @{
            name   = $name
            port   = $port
            comment = $description
        }
    }
}

@vexx32

Whether or not a statement succeeds is typically down to whether or not it generates an error of some kind, and nothing more.

Essentially, it comes down to the value of $? after execution of the _command_ (or, pointlessly, _expression_; due to the current grammar it can't be a whole _statement_), which means that $? is $false if:

  • with a PowerShell command: if it writes to the error stream, which means emitting at least one _non-terminating error_.

  • with an external program: if it reports a _nonzero exit code_ (as reflected in $LASTEXITCODE).

    • Regrettably, due to #10512, a 2> redirection incorrectly sets $? to $false in the presence of stderr output even if $LASTEXITCODE is 0.

with PS's logical -and operator

I suggest not recommending the use of -and, because it serves a fundamentally different purpose.

An if statement or ternary conditional is the appropriate construct to use for _expressions_ and also Test-* commands (which signal their success by _outputting_ a Boolean).

Yes $x && $y is not the same as $x; if ($x) { $y }. It's instead the same as $x; if ($?) { $y }.

&& and || are designed to encode command success, not expression value. In that case, you should use if or a ternary (or add your 馃憤 to this proposal).

I'm sure eventually you guys will make all of this more intuitive, thank you for all the great info! I'm very new to powershell.

The thing that actually confused me here is that powershell can return data from multiple places and it will all be streamed together. I have never seen anything like that before. That's actually pretty awesome.

When I see differences from UNIX for some things in powershell, usually powershell makes the correct design choice. Not to say that everything is not controversial.

If it helps any @rkitover, you might want to check out https://aka.ms/pskoans to get familiar with some of the nuance that can trip you up from time to time in PS 馃檪

Also, I highly recommend reading through the about_* help docs in PS -- they're all available from Get-Help or the online documentation. There's a _ton_ of good info in those.

GitHub
A simple, fun, and interactive way to learn the PowerShell language through Pester unit testing. - vexx32/PSKoans

A final note on the pipeline-chain operators, && and || (see about_Pipeline_Chain_Operators): They were borrowed from the world of POSIX-like shells (e.g., Bash) and are primarily useful for chaining _external programs_ (e.g., git) that reliably reflect their overall success or failure though their _process exit code_.

The thing that actually confused me here is that powershell can return data from multiple places and it will all be streamed together. I have never seen anything like that before. That's actually pretty awesome.

It is indeed awesome if you learn to harness it to your advantage.

PowerShell balances many worlds deftly (mostly - interaction with external programs is a perennial trouble spot), but the challenge is that if you come from only _one_ similar world, such as Unix shells or traditional programming languages, and expect everything to work the same, pain ensues.

While you can limp along with only ever using return and echo 'foo', ..., it really pays to familiarize yourself with the fundamentals of PowerShell, which is what @vexx32's PS Koans project helps with (plus the about_* topics he mentions).

Part of learning the fundamentals is learning how to use the help system (Get-Help) effectively, so you can perform targeted lookups not just for whole commands, but also for examples and individual parameters.

There is no support (yet) for looking up _operators_ directly via Get-Help - see the suggestion in #11339, which also points to a helper command, Show-OperatorHelp, which would allow you to run
Show-OperatorHelp '&&', for instance.

This issue has been marked as answered and has not had any activity for 1 day. It has been closed for housekeeping purposes.

Was this page helpful?
0 / 5 - 0 ratings