break a foreach loop, automatic variable woes

This topic contains 4 replies, has 2 voices, and was last updated by Profile photo of Dave Wyatt Dave Wyatt 2 years, 4 months ago.

  • Author
    Posts
  • #17602
    Profile photo of TJ Anon
    TJ Anon
    Participant

    Well, I got this script to work, so there is no emergency here. But I don't understand why I needed to go to extraordinary lengths to make it work. It is a script that we put on our image. The first time the freshly imaged machine boots and a user logs on, the script changes the time zone, renames the machine, and joins the domain. Then the script deletes any traces that it existed on that machine. (We run it through the runonce registry key.) The script you are seeing has been trimmed and fixed for public viewing.

    The first issue I had was trying to break out of the foreach loop. I ended up fixing it by using a label for that loop and breaking on the label name. Before I started using a label, the break command would exit the entire script.

    Second issue is that the automatic variables that are produced when the loop reads my csv file, are empty. This happened after I labeled the foreach loop. Before labeling the automatic variables were working fine. So I ended up typing in the full name of the variables. I.e., $machine.MacAddr instead of $_.MacAddr. That made it work again. Is that a bug in the software?

    Third issue is one that has bothered me for a 2-3 years. Every time I create automatic variables by reading a file with a foreach loop, I have to put those variables into a regular variable before I can use them. I can't run AD cmdlets against them, for instance. Ran into this problem way back in the beginning of my powershell career (3 years ago) and once I figured out how to make it work, I just forgot about it. I'd really like to know why I have trouble using automatic variables without plugging them into a real variable.

    Thanks for your help. I really want to understand powershell. Since I started learning it, my job changed so that I code 90% of the time. 🙂 Makes me AND my boss very happy.

    $MachInfo =  "c:\PostImg\MachInfotest.csv" 
    
    
    Function Join-Domain ($compname, $room, $seat)
    {
           
    write-host "`ncompname = " $compname
    write-host "room = " $room
    write-host "seat = " $seat
    
    
    
    } # End Function Join-Domain
    
    #######################
    # Main Driver Program #
    #######################
    $today = (get-date -format g)
    
    #########################
    # Get local MAC address #
    #########################
    $localnetwork = get-ciminstance `
       -class "Win32_NetworkAdapterConfiguration" `
       -filter "DHCPEnabled = 'true' AND Ipenabled='true'"
    [string]$localMac = $localnetwork.MACAddress
    
    #######################################################################
    # Below looks for the local MAC addresses on the machinelist.csv file                                     #
    # so the script will know how to rename the machine.                                                                  #
    #######################################################################
    $MachList = Import-Csv $MachInfo 
    :MacFound foreach ($machine in $MachList) {
       $Name = $_.MachName
       [string]$Mac = $_.MacAddr
       [string]$Room = $_.Room
       [string]$Seat = $_.Seat
    
    #   $Name = $machine.MachName
    #   [string]$Mac = $machine.MacAddr
    #   [string]$Room = $machine.Room
    #   [string]$Seat = $machine.Seat
    write-host "automatic variable _.machname = " $_.machname
       if ($Mac -eq $localMac)
          {
              break MacFound                # Jump out of the foreach loop. No need to keep looking if you found it.
          } # End if mac = localmac
    
    } # End foreach statement 
       if ($Mac -eq $localMac)
          {
    #      "Sending $Name machine to join-domain function" 
          Join-Domain -compname $Name `
             -room $room `
             -seat $seat 
    
          } # End if mac = localmac
       else 
          {
             "Local PC Mac address could not be found in machinfo.csv list: $localmac" 
          }
    
    #Remove-Item "c:\PostImg\*" -force  # Removes all files in the tjtest folder  
    
  • #17604
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    Are you sure that's the correct code? Because here:

    :MacFound foreach ($machine in $MachList) {
       $Name = $_.MachName
       [string]$Mac = $_.MacAddr
       [string]$Room = $_.Room
       [string]$Seat = $_.Seat
    

    You've got evidence of both types of ForEach loops; the foreach statement (as written), and a pipeline with ForEach-Object (the $_ variable used inside the body). If you're using a foreach ($thing in $things) style loop, you don't need the label. However, the break statement does NOT get you out of a pipeline that's using ForEach-Object. (Or rather, it does, but it looks for the end of a block that does work with the break statement, and if none were found, it exits the whole script, which is what you were describing.)

    When you're in a pipeline using ForEach-Object, you can emulate the "continue" keyword with a return statement. What you're really doing is the equivalent of returning from the Process block of an advanced function, there; it'll be executed again for the next input object. If you need to emulate "break" in a pipeline that's using ForEach-Object, you have a few options that I can think of, off the top of my head:

    [ul]
    [li]Throw a terminating error. The pipeline will be aborted, but the calling code would need to handle that properly.[/li]
    [li]Pipe the ForEach-Object cmdlet to Select-Object -First 1, and only output something when you've found what you were looking for. Select-Object -First 1 (in PowerShell 3.0 and later) also produces a terminating error to abort the pipeline, but one that the PowerShell engine handles quietly behind the scenes.[/li]
    [li]Set some sort of flag variable which tells your ForEach-Object body to stop doing stuff. It would still get executed for all of the rest of the input objects, but wouldn't actually do much. This is less efficient than the first two options.[/li]
    [/ul]

    Generally, I try to use the second option and pipe to Select-Object -First 1, if possible (or just use a foreach loop instead of ForEach-Object in a pipeline.)

    Edit:

    For clarification, here's the difference between the two types of "foreach" loops:

    foreach ($thing in Get-Things)
    {
        # foreach followed by the ($thing in Get-Things) syntax
        # Cannot receive pipeline input
        # Cannot be directly piped to anything else, without help from some other wrapping syntax
        # Does not use the $_ variable inside the loop body
        # Requires the $things variable to either be an enumerator, or a collection already held in memory.  (Does not stream objects the way the pipeline does.)
        # Supports the continue and break keywords
    }
    
    Get-Things | foreach {
        # here, foreach is an alias to the ForEach-Object cmdlet.
        # No ($thing in $things) syntax after the foreach token
        # Opening brace must be on the same line as the ForEach-Object cmdlet.
        # Input object is stored in the $_ automatic variable at the start of the body.
        # Streams input objects one at a time; the entire output of Get-Things doesn't have to be stored in memory at any point.
        # Does not use the continue or break keywords.
        # Generally much slower than the foreach loop; you gain memory efficiency at the expense of execution time.
    }
    
  • #17605
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    Regarding your other question, there are several types of code that can set the automatic $_ variable. Catch blocks set $_ to the terminating error that was caught, ForEach-Object blocks set $_ to the input object, Select-Object uses $_ in constructed properties, and so on. It's a good practice to save the value that's in $_ at the beginning of a ForEach-Object body (or even at the beginning of a Catch block, if it's long), just in case that variable gets overwritten with something else later on. Even if nothing in your original code is doing that, someone may add a new line later which introduces a problem, and if you had saved the value of $_ in a different variable from the outset, that bug would never have happened.

  • #17646
    Profile photo of TJ Anon
    TJ Anon
    Participant

    Thanks for the detailed explanation, Dave. For some reason I thought automatic variables would work in any kind of looping structure. I've never used the "Get-Things | foreach" construct. So I must've had this problem before and just never investigated. It was the right code. I just wrote the loop in such a way that automatic variables wouldn't work. I was a tad confused when you kept using both the terms: Foreach-Object and foreach. So used get-alias to figure out they're the same command. I guess it just depends on the context, what the command needs in order to operate correctly. When you refer to the Foreach-Object command, I think you are referring to the "get-Things | foreach" construct, correct? Appreciate your taking time to answer. Thank you.

  • #17647
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    Correct, and that's why you'll never see "| foreach" in a script that I write. When I'm using the cmdlet, I always spell out ForEach-Object with the capitalization, to make it easy to see the difference. Using "foreach" to mean two different things depending on where you find it in a command is confusing to me, so I avoid it.

You must be logged in to reply to this topic.