1

I am trying to modify a variable within Invoke-Command in order to get out of a loop, however I'm having trouble doing that.

In the sample script below, I'm connecting to a host, grabbing information from NICs that are Up and saving the output to a file (Baseline). Then on my next iteration I will keep grabbing the same info and then compare Test file to Baseline file.

From a different shell, I've connected to the same server and disabled one of the NICs to force Compare-Object to find a difference.

Once a difference is found, I need to get out of the loop, however I cannot find a way to update the local variable $test_condition. I've tried multiple things, from Break, Return, $variable:global, $variable:script, but nothing worked so far.

$hostname = "server1"
$test_condition = $false

    do {
        Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
            
            $path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"

            if ($path -eq $false) {
                Get-NetAdapter | Where-Object Status -EQ "Up"  | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)

            } else {
                Get-NetAdapter | Where-Object Status -EQ "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
                $objects = @{
                    ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
                    DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
                    }
                $test_condition = (Compare-Object @objects).SideIndicator -ccontains "<="
                $test_condition #this is returning True <-----

            }

        }
    } until ($test_condition -eq $true)

Any tips? What am I doing wrong?

TIA, ftex

Steven
  • 6,817
  • 1
  • 14
  • 14
fabianotex
  • 15
  • 5

1 Answers1

3

You can pass variables into a remote script block with the $Using:VarName scope modifier, but you can't use typical $Global: or $Script to modify anything in the calling scope. In this scenario the calling scope isn't the parent scope. The code is technically running in a new session on the remote system and $Global: would refer to that session's global scope.

For example:

$var = "something"
Invoke-Command -ComputerName MyComuter -ScriptBlock { $Global:var = "else"; $var}

The remote session will output "else". However, after return in the calling session $var will output "something" remaining unchanged despite the assignment in the remote session.

Based on @SantiagoSquarzon's comment place the assignment inside the Do loop with a few other modifications:

$hostname = "server1"

do {
    $test_condition = $false
    $test_condition = 
    Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
        
        $path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"

        if ($path -eq $false) {
            Get-NetAdapter | Where-Object Status -eq "Up"  | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)

        } else {
            Get-NetAdapter | Where-Object Status -eq "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
            $objects = @{
                ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
                DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
                }
            
            (Compare-Object @objects).SideIndicator -contains "<=" # this is returning True <-----
        }

    }
} until ($test_condition -eq $true)

I don't know why you were using -ccontains considering "<=" has no casing implications. Also it's very unusual to capitalize operators.

Notice there's no explicit return or assignment. PowerShell will emit the Boolean result of the comparison and that will be returned from the remote session and end up assigned to the $test_condition variable.

An aside:

I'm not sure why we want to use -contains at all. Admittedly it'll work fine in this case, however, it may lead you astray elsewhere. -contains is a collection containment operator and not really meant for testing the presence of one string within another. The literal meaning of "contains" makes for an implicitly attractive hazard, as demonstrated in this recent question.

In short it's easy to confuse the meaning, purpose and behavior on -contains.

This "<=" -contains "<=" will return "true" as expected, however "<==" -contains "<=" will return "false" even though the left string literally does contain the right string.

The answer, to the aforementioned question says much the same. My addendum answer offers a some additional insight for the particular problem and how different operators can be circumstantially applied.

So, as a matter of practice for this case wrap the Compare-Object command in the array sub-expression operator like:

@( (Compare-Object @objects).SideIndicator ) -contains "<="

Given the particulars, this strikes me as the least intrusive way to implement such a loosely stated best practice.

Steven
  • 6,817
  • 1
  • 14
  • 14
  • 2
    Yep, great answer as always. I'm so not used to `Compare-Object` honestly, never find a good use case for it vs a more classic approach, maybe i'm too old lol. I would recommend OP using a `PSSession` for this use case too and `-Session` instead of `-ComputerName` on `icm`. – Santiago Squarzon May 11 '21 at 23:43
  • Thanks! Yes now that you mention it, as written amd considering it's a loop we should indeed use a PSSession. Moreover, we should probably introduce a `Start-Sleep` command. However, It doesn't look like there's a good reason to outside `Invoke-Command`, instead we could probably move it in to the script block. That would diminish the case for PSSessions, not having to setup/teardown repeatedly. I think I'll wait for comment before amending. – Steven May 12 '21 at 01:03
  • Nicely done; note that you never need `@(...)` in order to use `-contains` - even a _scalar_ LHS will do; e.g.; `1 -contains 1` works just fine. – mklement0 May 12 '21 at 01:04
  • @mklement0 Thanks. I edited the answer, hopefully to better convey the intended message. I included a recent question that shows what I think is a common point of confusion with the `-contains` operator. In any event, your feedback is always welcomed; do let me know what you think. – Steven May 12 '21 at 14:22
  • Thanks for updating, Steven (you already had my +1). I assume you're saying that use of `@(...)` with `-contains` isn't a _technical_ necessity but _signals to the reader_ that the LHS of `-contains` is a _collection_ operator. However, even `-match` and `-like` can situationally act like collection operators, namely if the LHS is a collection, whereas `-contains` always acts _always_ treats its LHS as a collection. To me, the `@(...)` therefore suggests the opposite of what you're trying to convey, namely that _situationally_ `@(...)` is necessary - which it isn't. – mklement0 May 12 '21 at 15:32
  • Also note that with _expression_ output such as `(Compare-Object @objects).SideIndicator` (member enumeration on a command's outptut), `@(...)` results in (in this case unnecessary) construction of an _additional_, always `[object[]]`-typed array. Try `@([int[]] (1..5)).GetType().Name`, for instance. – mklement0 May 12 '21 at 15:35
  • Your interpretation of my intent is correct. However, now I'm genuinely concerned I may be sending the wrong message. I did talk about the situational behavior of other operators in the the referenced answer. And, I struggled with if I should expound on them here as well. Note: I had already double checked the enumeration, and `(Compare-Object $array $array2).SideIndicator.GetType()` where `$array` has exactly 1 element more `$array2` returns a string not `[Object[]]`. This was consistent across 5.1 & 7.1.3... – Steven May 12 '21 at 15:58
  • Only just happened to see this, @Steven. On a meta note: you as the post owner are automatically notified of follow-up comments, but in the absence of @-mentioning commenters, they aren't notified of your responses. The exact notification rules are complex, and I wish that the site showed you _who will be notified_ before submitting a comment (there is a hard limit on only being able to @-mention _one_ user). – mklement0 May 12 '21 at 21:21
  • Yes, the `.GetType()` behavior is as expected, but my point was that `@() -contains ` and ` -contains ` are _equivalent_, so that bringing `@(...)` into the mix may cause confusion. – mklement0 May 12 '21 at 21:24
  • A follow-up to the meta conversation: A who-will-be-notified preview feature was requested in 2011(!) in https://meta.stackexchange.com/questions/99261/tell-the-user-who-will-be-notified-of-a-comment, but the feature never materialized. – mklement0 May 12 '21 at 21:49
  • Thanks @Steven for your help and everyone involved in this thread. I do appreciate it. PS: The -ccontains was a mistake of mine, part of a tab complete. I was looking for -contains only. – fabianotex May 13 '21 at 14:15