简体   繁体   中英

PowerShell - If/Else Statement Doesn't Work Properly

First off I apologize for the extremely long, wordy post. It's an interesting issue and I wanted to be as detailed as possible. I've tried looking through any related PowerShell posts on the site but I couldn't find anything that helped me with troubleshooting this problem.

I've been working on a PowerShell script with a team that can send Wake-On-Lan packets to a group of computers. It works by reading a .csv file that has the hostnames and MAC's in two columns, then it creates the WOL packets for each computer and broadcasts them out on the network. After the WOL packets are sent, it waits a minute and then pings the computers to verify they are online, and if any don't respond it will display a window with what machines didn't respond to a ping. Up until the final If/Else statement works fine, so I won't be going into too much detail on that part of the script (but of course if you want/need further details please feel free to ask).

The problem I'm having is with the final If/Else statement. The way the script is supposed to work is that in the ForEach loop in the middle of the script, the value of variable $PingResult is true or false depending on whether or not the computer responds to a ping. If the ping fails, $PingResult is $false, and then it adds the hostname to the $PingResult2 variable.

In theory if all of the machines respond, the If statement fires and the message box displays that it was a success and then the script stops. If any machines failed to respond, the Else statement runs and it joins all of the items together from the $PingResult2 variable and displays the list in a window. What actually happens is that even if all of the machines respond to a ping, the If statement is completely skipped and the Else statement runs instead. However, at that point the $PingResult2 variable is blank and hence it doesn't display any computer names of machines that failed to respond. In my testing I've never seen a case where the script fails to wake a computer up (assuming it's plugged in, etc.), but the Else statement still runs regardless. In situations where the Else statement runs, I've checked the value of the $PingResult2 variable and confirmed that it is blank, and typing $PingResult2 –eq “” returns $true.

To add another wrinkle to the problem, I want to return to the $PingResult2 variable. I had to create the variable as a generic list so that it would support the Add method to allow the variable to grow as needed. As a test, we modified the script to concatenate the results together by using the += operator instead of making $PingResult2 a list, and while that didn't give a very readable visual result in the final display window if machines failed, it did actually work properly occasionally. If all of the computers responded successfully the If statement would run as expected and display the success message. Like I said, it would sometimes work and sometimes not, with no other changes making a difference in the results. One other thing that we tried was taking out all of the references to the Visual Basic assembly and other GUI elements (besides the Out-GridView window) and that didn't work either.

Any idea of what could be causing this problem? Me and my team are completely tapped out of ideas at this point and we'd love to figure out what's causing the issue. We've tried it on Windows 7, 8.1, and the latest preview release of Windows 10 with no success. Thanks in advance for any assistance.

PS Extra brownie points if you can explain what the regular expression on line 29 is called and how it exactly works. I found out about it on a web posting that resolved the issue of adding a colon between every two characters, but the posting didn't explain what it was called. (Original link http://powershell.org/wp/forums/topic/add-colon-between-every-2-characters/ )

Original WOL Script we built the rest of the script around was by John Savill (link http://windowsitpro.com/networking/q-how-can-i-easily-send-magic-packet-wake-machine-my-subnet )

Script

Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms

$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.ShowDialog() | Out-Null

$FileVerify = Get-Content -Path $OpenFileDialog.FileName -TotalCount 1
$FileVerify = ($FileVerify -split ',')

If($FileVerify[0] -ne "Machine Name" -or $FileVerify[1] -ne "MAC")

{

    $MsgBox = [System.Windows.Forms.MessageBox]::Show("The CSV File's headers must be Machine Name and MAC.",'Invalid CSV File headers!',0,48)
    Break

}

$ComputerList = Import-Csv -Path $OpenFileDialog.FileName |
Out-GridView -PassThru -Title "Select Computers to Wake up"

ForEach($Computer in $ComputerList)

    {

        If($Computer.'MAC' -notmatch '([:]|[-])')

            {

                $Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'

            }

        $MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
        $UDPclient = new-Object System.Net.Sockets.UdpClient
        $UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
        $packet = [byte[]](,0xFF * 6)
        $packet += $MACAddr * 16
        [void] $UDPclient.Send($packet, $packet.Length)
        write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"

    }

Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60

$PingResult2 = New-Object System.Collections.Generic.List[System.String]

ForEach($Computer in $ComputerList)

    {

        Write-Host "Pinging $($Computer.'Machine Name')"
        $PingResult = Test-Connection -ComputerName $Computer.'Machine Name' -Quiet

        If ($PingResult -eq $false)

            {

                $PingResult2.Add($Computer.'Machine Name')

            }

    }

If($PingResult2 -eq "")

    {

        [System.Windows.Forms.MessageBox]::Show("All machines selected are online.",'Success',0,48)
        Break

    }

Else

    {

        $PingResult2 = ($PingResult2 -join ', ')
        [System.Windows.Forms.MessageBox]::Show("The following machines did not respond to a ping: $PingResult2",'Unreachable Machines',0,48)

    }

The comparison in your If statement is incorrect because you are comparing $PingResult2 , a List<string> , to a string. Instead, try

If ($PingResult2.Count -eq 0)
{
    # Show the message box
}
Else
{
    # Show the other message box
}

or one of countless other variations on this theme.

The regular expression in question uses a backreference to replace exactly two characters with the same two characters plus a colon character. I am unsure what exactly you are attempting to "define," though.

You are checking if a list has a value of a null string, rather than checking the number of items in the list.

If you change the if statement to the following it should work fine:

If($PingResult2.count -eq 0)

I'm guessing the regex is trying to insert a colon between every two characters of a string to represent 0123456789ab as 01:23:45:67:89:ab .

The code means if there is no hyphen or colon in the MAC, put in a colon every the characters, then split the address using colon as delimiter then represent each as a byte:

If($Computer.'MAC' -notmatch '([:]|[-])')

        {

            $Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'

        }

    $MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }

The other answer have explained quite well why your code does not work. I'm not going there. Instead I'll give some suggestions that I think would improve your script, and explain why I think so. Let's start with functions. Some of the things you do are functions I keep on hand because, well, they work well and are used often enough that I like having them handy.

First, your dialog to get the CSV file path. It works, don't get me wrong, but it could probably be better... As it is you pop up an Open File dialog with no parameters. This function allows you to use a few different parameters as wanted, or none for a very generic Open File dialog, but I think it's a slight improvement here:

Function Get-FilePath{
[CmdletBinding()]
Param(
    [String]$Filter = "|*.*",
    [String]$InitialDirectory = "C:\")

    [void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
    $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $OpenFileDialog.initialDirectory = $InitialDirectory
    $OpenFileDialog.filter = $Filter
    [void]$OpenFileDialog.ShowDialog()
    $OpenFileDialog.filename
}

Then just call it as such:

$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"

That opens the dialog filtering for only CSV files, and starts them looking at their desktop (I find that a lot of people save things to their desktop). That only gets the path, so you would run your validation like you were. Actually, not like you were. You really seem to have over complicated that whole bit. Bit I'll get to that in a moment, first, another function! You call message boxes fairly often, and type out a bunch of options, and call the type, and everything every single time. If you're going to do it more than once, make it easy on yourself, make a function. Here, check this out:

Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}

Then you can specify as much or as little as you want for it. Plus it uses Type'd parameters, so tab completion works, or in the ISE (if that's where you're writing your script, like I do) it will pop up valid options and you just pick from a list for the buttons or icon to show. Plus it doesn't return anything if it's a simple 'OK' response, to keep things clean, but will return Yes/No/Cancel or whatever other option you choose for buttons.

Ok, that's the functions, let's get to the meat of the script. Your file validation... Ok, you pull the first line of the file, so that should just be a string, I'm not sure why you're splitting it and verifying each header individually. Just match the string as a whole. I would suggest doing it case insensitive, since we don't really care about case here. Also, depending on how the CSV file was generated, there could be quotes around headers, which you may want to account for. Using -Match will perform a RegEx match that is a bit more forgiving.

If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
    Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
    break
}

So now we have two functions, and 5 lines of code. Yes, the functions take up more space than what you previously had, but they're friendlier to work with, and IMO more functional. Your MAC address correction, and WOL sending part are all aces so far as I'm concerned. There's no reason to change that part. Now, for validating that computers came back up... here we could use some improvement. Instead of making a [List] just add a member to each object, then filter against that below. The script as a whole would be a little longer, but better off for it I think.

Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms

Function Get-FilePath{
[CmdletBinding()]
Param(
    [String]$Filter = "|*.*",
    [String]$InitialDirectory = "C:\")

    [void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
    $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $OpenFileDialog.initialDirectory = $InitialDirectory
    $OpenFileDialog.filter = $Filter
    [void]$OpenFileDialog.ShowDialog()
    $OpenFileDialog.filename
}

Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}

#Get File Path
$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"

#Validate Header
If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
    Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
    break
}

$ComputerList = Import-Csv -Path $CSVFile |
Out-GridView -PassThru -Title "Select Computers to Wake up"

ForEach($Computer in $ComputerList)

    {

        If($Computer.'MAC' -notmatch '([:]|[-])')

            {

                $Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'

            }

        $MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
        $UDPclient = new-Object System.Net.Sockets.UdpClient
        $UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
        $packet = [byte[]](,0xFF * 6)
        $packet += $MACAddr * 16
        [void] $UDPclient.Send($packet, $packet.Length)
        write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"

    }

Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60

$ComputerList|ForEach

    {

        Write-Host "Pinging $($_.'Machine Name')"
        Add-Member -InputObject $_ -NotePropertyName "PingResult" -NotePropertyValue (Test-Connection -ComputerName $Computer.'Machine Name' -Quiet)

    }

If(($ComputerList|Where{!($_.PingResult)}).Count -gt 0)

    {

        Show-MsgBox "All machines selected are online." 'Success'

    }

Else

    {

        Show-MsgBox "The following machines did not respond to a ping: $(($ComputerList|?{!($_.PingResult)}) -join ", ")" 'Unreachable Machines' -Icon Asterisk

    }

Ok, I'm going to get off my soap box and go home, my shift's over and it's time for a cold one.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM