user10070807
user10070807

Reputation:

Powershell - looping through an array

I'm looking to search the C and E drives of all Windows servers in Active Directory for any existing copies of putty.exe and their version. The output needs to have the server name, full path to the executable, and the file version. So far I have the following code (which right now is only using two servers for testing:

$ComputerName = Get-ADComputer -filter "name -like 'computer01' -or name `
-like 'server01'" | select -ExpandProperty name

$OutputArr = @()

$findFiles = foreach($computer in $computername){

    $file = Invoke-Command -computername $computer { Get-ChildItem -Path `
    c:\, e:\ -Recurse | where-object{(!$_.psiscontainer -eq $true) -and `
    ($_.name -like "putty.exe")} | ForEach-Object -process {$_.fullname} }


    $output = $OutputObj = New-Object -TypeName PSobject  
    $OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
    $OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file

    $OutputArr += $OutputObj
    Write-Verbose $OutputObj
}

$OutputArr | fl

The above code outputs the following array:

ComputerName : COMPUTER01
FilePath     : {C:\Program Files\PuTTY\putty.exe, C:\Program Files (x86)\PuTTY\PUTTY.EXE}

ComputerName : SERVER01
FilePath     : {C:\Program Files (x86)\putty\putty.exe, C:\Users\testuser\Desktop\Public Desktop\putty.exe}

This produces the correct data, but now I need to run another snippet of code against each separate filepath under computername, but am not sure how to accomplish this, as it is taking the full filepath with multiple entries.

Essentially, I need to separate each ComputerName in the array into multiple lines:

COMPUTER01,C:\Program Files\PuTTY\putty.exe
COMPUTER01,C:\Program Files (x86)\PuTTY\PUTTY.EXE
SERVER01,C:\Program Files (x86)\putty\putty.exe

Etc...

Is an array not the correct way to do it?

Upvotes: 4

Views: 11841

Answers (3)

Patrick Mcvay
Patrick Mcvay

Reputation: 2281

I am not sure what exactly you are wanting to do, but this should work for iterating through your custom object. Your Invoke-Command can be simplified also.

$file = Invoke-Command -computername $computer { Get-ChildItem -Path "C:\", "E:\" -Recurse -File -Filter "putty.exe" | Select -Property VersionInfo }

$OutputObj = New-Object -TypeName PSobject  
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $env:COMPUTERNAME
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file

$OutputArr += $OutputObj

foreach ($item in $OutputArr)
{
    for ($i = 0; $i -lt $item.FilePath.Count; $i++)
    { 
         Write-Output ([string]::Join(', ', $item.ComputerName, $item.FilePath[$i].VersionInfo.FileName, $item.FilePath[$i].VersionInfo.FileVersion))
    }       
}

Upvotes: 0

AdminOfThings
AdminOfThings

Reputation: 25031

If you are working strictly with what you already have stored in $OutputArr, the following will work:

$out = foreach ($line in $OutputArr) {
   if ($line.filepath.count -gt 1) {
     foreach ($fp in $line.FilePath) {
       [pscustomobject][ordered]@{ComputerName = $line.ComputerName; FilePath = $fp}
     }
   } 
   else {
     $line
   }
 } 

$out | ConvertTo-Csv -NoTypeInformation

The foreach loop creates new objects with properties ComputerName and FilePath and stores them in $out as an array of objects.

If you do not care about properties and just want a comma-delimited list, you can use the following:

foreach ($line in $OutputArr) {
  if ($line.filepath.count -gt 1) {
    foreach ($fp in $line.FilePath) {
      "{0},{1}" -f $line.ComputerName,$fp 
    }
  }
  else {
      "{0},{1}" -f $line.ComputerName,$line.FilePath
  }
 } 

This does the same looping as the first solution but instead uses the format operator (-f) to format the output. Piping to ConvertTo-Csv formats the output to be comma-delimited with the properties as headers.

You could move your desired functionality into your code before you even store anything in $OutputArr. I feel like doing all this after all of the other looping to create $OutputArr is just adding inefficiency.

Upvotes: 3

Mike Veazie - MSFT
Mike Veazie - MSFT

Reputation: 916

PowerShell can get tricky when doing remote sessions. The below script should be a good starting point for you. Here are some other areas for improvement:

  1. Doing Get-ChildItem -Recurse at the root of a drive will use an inordinate amount of memory and you could cause unintentional page file expansion or even make a server unresponsive due to 100% memory usage. In my snippet below I am using a list of well known paths. If you need to to identify if putty.exe is started on additional machines, your monitoring solution hopefully has process performance data and you can search for putty.exe there.
  2. Speaking of memory management, remote shells have limitations of how much memory they can use. If you run winrm get winrm/config/winrs you will see the upper limit.
  3. If you are going to authenticate to additional resources from within your remote script blocks, you will need to set up authentication that supports double hop scenarios (CredSSP or Kerberos)
$computerNames = @('computer1','computer2')

foreach($computer in $computerNames)
{
    <# 
        First Script Block checks well known paths for putty.exe
    #>
    $puttyResults = Invoke-Command -ComputerName $computer -ScriptBlock {

        $wellKnownPaths = @()
        $wellKnownPaths += Join-Path $env:USERPROFILE -ChildPath "Desktop"
        $wellKnownPaths += "D:\tools\"
        $wellKnownPaths += $env:Path.Split(';')

        $puttyPaths = @()
        foreach($path in $wellKnownPaths)
        {
            $puttyPaths += Get-ChildItem $path -Filter "putty.exe" -Recurse
        }

        if($puttyPaths.Count -gt 0)
        {
            $resultsArray = @()
            foreach($path in $puttyPaths)
            {
                $resultsArray += [PSCustomObject]@{
                    ComputerName = $env:COMPUTERNAME
                    PuttyPath = $path.FullName
                }
            }

            return $resultsArray
        }

        return $null
    }

    if($puttyResults -ne $null)
    {
        foreach($result in $puttyResults)
        {
            <#
                Second script block takes action against putty.exe
            #>
            $puttyExists = Invoke-Command -ComputerName $computer -ArgumentList @($result.PuttyPath) -ScriptBlock {
                Param(
                    $PuttyPath
                )

                return (Test-Path $PuttyPath)
            }

            if($puttyExists)
            {
                $msg = "Putty exists on '{0}', at '{1}'" -f $result.ComputerName, $result.PuttyPath
                Write-Host $msg -ForegroundColor:Yellow
            }
        }        
    }
}

Upvotes: 0

Related Questions