Piping an array into a function has Foreach and Foreach-Object only dealing with

I’m piping an array of PSCustomObjects into a function but when I try to iterate over the array, the function only deals with the last item of the array.

Tested both foreach and ForEach-Object but the results are the same.

Function Get-MailInfo {
	$MailInfoArray = @()

	$ReturnArray | ForEach-Object {
		$User = Get-ADUser -Server $server -Identity $_.UserName -Properties Manager
		if ((Get-ADUser -Server $server -Identity $_.UserName -Properties Manager).Manager -ne $null) {
			$Manager = Get-ADUser -Server $server -Identity $User.Manager -ErrorAction Stop
		}
		else {
			$Manager = Get-ADUser -Server $server -Identity $_.UserName
		}
		$PSObject = [PSCustomObject]@{
			User = $_.UserName
			GivenName = $User.GivenName
			SurName = $User.Surname
			To = ($User.UserPrincipalName).ToLower()
			Cc = ($Manager.UserPrincipalName).ToLower()
			Computer = $_.ComputerName
		} 
		$MailInfoArray += $PSObject
	}
	return $MailInfoArray
}

$MailInfoArray
$MailInfoArray.Length

function Send-MailInfo {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [array]$MailInfoArray
    )
	# I've inc
    foreach ($obj in $MailInfoArray) {
        Add-Content -Path ".\$($obj.User)-fe.txt" -Value "$($obj.GivenName)" -Encoding UTF8
    }
	
	$MailInfoArray | ForEach-Object {
		Add-Content -Path ".\$($_.User)-fo.txt" -Value "$($_.GivenName)" -Encoding UTF8
	}
}

$MailInfoArray | Send-MailInfo

If I call the the last function like this however Send-MailInfo (Get-MailInfo) it works as intended. Does the pipeline not support arrays naturally or is there something else I should be adding in the parameter block?

Change line 32 from

    [array]$MailInfoArray

to

    [PSCustomObject[]]$MailInfoArray

Instead of telling the function simply that it gets an array without specifying an array of what, you tell it that it gets a number of PSCustomObjects in an array?
Makes sense, however it seems that I’m still missing something somewhere.

This is another example, where I’ve changed to from [array] to [PSCustomObject] but piping it still only returns the last object in the array.

function Get-ComputerUserArray {
  [CmdletBinding()]
  param (
    [parameter()]
    [string]$Path
  )
  $reg = '(?<cname>[A-Z]{2}\d{4}).* ADM\\(?<uname>[a-z]{2}\d{4})'

  $ReturnArray = Switch -Regex -File $Path {
    { $_ -cmatch $reg } {
      $_ -cmatch $reg | Out-Null
      [PSCustomObject]@{
        ComputerName  = $Matches['cname']
        UserName      = $Matches['uname']
      }
    }
  }
  $ReturnArray.Length
  return $ReturnArray
}

function Show-ReturnArray {
  [CmdletBinding]
  param (
    [Parameter(ValueFromPipeLine = $true)]
    [PSCustomObject[]]$ReturnArray
  )
  $ReturnArray.Length
  $ReturnArray | ForEach-Object {
    $_.ComputerName
    $_.UserName
  }
}

# This does not work - only returns the last item in the array!
Get-ComputerUserArray -Path ".\TestFile.txt" | Show-ReturnArray

# This works - returns all the items in the array!
Show-ReturnArray -ReturnArray (Get-ComputerUserArray -Path ".\TestFile.txt")

It seems like you are missing your processing structure for pipeline input. I don’t see your begin, process, and end blocks.

IE

#Sample Objects for piping
$customObjects = 0..5 |
ForEach-Object {
    $props = @{
        Name = "Object$_"
        Path = "C:\User\Object$_"
    }
    New-Object -TypeName PSObject -Property $props
}

#Sample function for processing pipeline input
Function Test-Piping {
    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline)]$data
    )

    Begin {
        Write-Host "Beginning pipe to function" -ForegroundColor Green
    }

    Process {
        Write-Host "Processing for $($data.Name) on $($data.path)" -ForegroundColor Cyan
    }

    End {
        Write-Host "Ending pipe to function" -ForegroundColor Red
    }

}

#Sample piping into Sample function
$customObjects | Test-Piping

Results:

Beginning pipe to function
Processing for Object0 on C:\User\Object0
Processing for Object1 on C:\User\Object1
Processing for Object2 on C:\User\Object2
Processing for Object3 on C:\User\Object3
Processing for Object4 on C:\User\Object4
Processing for Object5 on C:\User\Object5
Ending pipe to function

Thank you for your input, and I’m sorry it took me so long to reply.
Been busy prepping my home office for a week of working from home.

I’m getting closer, but I’m still missing something about the processing of the array.

I set up the function with begin/process/end blocks and mostly the processing of the objects in the piped in array seems to work as it should.
However I don’t understand why the array reports a lenght of 0 or 1 objects rather than the full lenght of the array.

My intention is to ask the user to confirm that they want to send the mail to the number of objects in the array, but if I place $MailInfoArray.Length in the begin block it reports 0 objects.
If on the other hand I place it in the process-block it reports 1 object, and it seems to iterate over the array though $MailInfoArray.Length is placed outside of the ForEach-Object loop.

Truncated input:

User      : un1234
GivenName : User1
SurName   : Name
To        : user1.name@email.address
Cc        : manager.name@email.address
Computer  : CN1234
[...]

Output:

1
user1.name@email.address
manager.name@email.address
Computer: CN1234
1
user2.name@email.address
manager.name@email.address
Utbyte av dator: CN2345
Mail sent!

Function:

function Send-ReturnMail {
  [CmdletBinding()]
  param (
    [Parameter(ValueFromPipeLine = $true)]
    [PSCustomObject[]]$MailInfoArray
  )
  begin {
    $credential =  Get-Credential 
  }

  process {
    # $Send = Read-Host -Prompt "You will be mailing $($MailInfoArray.Length) users. rnGo ahead? [Y/n] "

    $MailSubject = "Computer: $($_.Computer)"
    $MailBody = @"
    
  
  
	[...]
  

"@

	$mailParams = @{
      Credential  = $credential
      SmtpServer  = 'smtp.mail.address'
      Port        = '587'
      UseSSL      = $true
      Encoding    = 'UTF8'
      From        = 'itsupport <it.support@email.address>'
      BodyAsHtml  = $true
      Body        = $MailBody
      Subject     = $MailSubject # $($_.Computer)  
      To          = $($_.To)
      Cc          = $($_.Cc)
      # ReplyTo     = 'itsupport@email.address'
    }

    $MailInfoArray.Length
    # if ((($Send).ToLower() -eq 'y') -or (($Send).ToLower() -eq 'yes') -or (($Send).ToLower() -eq '')) {
      $MailInfoArray | ForEach-Object {
        # Send-MailMessage @mailParams -WhatIf
        $mailParams.To
        $mailParams.Cc
        $mailParams.Subject
        #$mailParams.Body
      }
    #}
  }

  end {
    "Mail sent!"
  }
}

When an array of objects is passed over the pipeline to a function like yours, each element in the array is processed one at a time like a loop, so when calling your function over the pipeline, the process block $MailInfoArray is a single element each iteration and not the entire array. Try calling your function without using the pipeline i.e.

Send-ReturnMail -MailInfoArray <array argument>

Do you see a difference? Here’s a simplified example:

function test-piping {
[cmdletbinding()]
Param(
[parameter(ValueFromPipeline)][string[]]$array
)
$array.count
}

(1..10) | test-piping 
test-piping -array (1..10)


1
10

Thank you.

While testing I had a previous version of the calling script invoking this script with “regular” parameter which worked as intended and I didn’t understand why, when the pipeline didn’t. But that of course explains it.

When I think about it I know that the pipe sends discrete items when doing for instance Get-ChildItem *.txt | Get-Content, I just imagined that it would send an array as a single unit rather than expand it.