Training
Get a free hour of SANS training

Experience SANS training through course previews.

Learn More
Learning Paths
Can't find what you are looking for?

Let us help.

Contact us
Resources
Join the SANS Community

Become a member for instant access to our free resources.

Sign Up
For Organizations
Interested in developing a training plan to fit your organization’s needs?

We're here to help.

Contact Us
Talk with an expert

Month of PowerShell: Merging Two Files (Understanding ForEach)

A routine task (merging two files) leads us down the path of developing a better understanding of the ForEach command in PowerShell.

Authored byJoshua Wright
Joshua Wright

#monthofpowershell

Let's say we have two files: firstnames.txt and lastnames.txt. You need to merge the two files together to create a list of email addresses for a target organization as part of a password spray attack. Maybe you want to use MSOLSpray (orig) and FireProx to discover login names.

This is a task for the foreach statement (or so I thought).

Let's look at the files. I've created firstnames.txt and lastnames.txt from the Name Census website data (first names, last names):

PS C:\temp> Get-Content .\firstnames.txt
andrea
barbara
bari
biddy
david
edward
elizabeth
heather
james
jennifer
john
jon
latisha
linda
maddie
mary
michael
patricia
robert
william
PS C:\temp> Get-Content .\lastnames.txt
allen
brown
davis
garcia
gray
harris
hernandez
johnson
jones
keely
kembrey
lopez
lulham
marfell
martinez
merckle
miller
rodriguez
smith
williams

Next, I'll read the files into variables $firstnames and $lastnames:

PS C:\temp> $firstnames = Get-Content .\firstnames.txt
PS C:\temp> $lastnames = Get-Content .\lastnames.txt
PS C:\temp>

Using two foreach loops, we can iterate on the $firstnames and $lastnames lists, creating the variables $first and $last for each name in both loops. Then we use Write-Host with variable expansion to produce the email address First.Last@falsimentis.com:

PS C:\temp> foreach ($first in $firstnames) { foreach ($last in $lastnames) { Write-Host "$first.$last@falsimentis.com" } }
andrea.allen@falsimentis.com
andrea.brown@falsimentis.com
andrea.davis@falsimentis.com
andrea.garcia@falsimentis.com
andrea.gray@falsimentis.com
andrea.harris@falsimentis.com
andrea.hernandez@falsimentis.com
andrea.johnson@falsimentis.com
andrea.jones@falsimentis.com
andrea.keely@falsimentis.com
andrea.kembrey@falsimentis.com
andrea.lopez@falsimentis.com
andrea.lulham@falsimentis.com
andrea.marfell@falsimentis.com
andrea.martinez@falsimentis.com
andrea.merckle@falsimentis.com
andrea.miller@falsimentis.com
andrea.rodriguez@falsimentis.com
andrea.smith@falsimentis.com
andrea.williams@falsimentis.com
barbara.allen@falsimentis.com
barbara.brown@falsimentis.com
barbara.davis@falsimentis.com
...

Fantastic! Intuitive, straightforward code to solve a problem. Problem solving with PowerShell! Success!

Except for one last part: this writes the output to the screen, but that's not what we want. We want the output to go to a file. So, let's add to the pipeline with Out-File to save the email addresses to a file.

Here's where it all started to go downhill.

This doesn't work:

PS C:\temp> foreach ($first in $firstnames) { foreach ($last in $lastnames) { Write-Host "$first.$last@falsimentis.com" } } | Out-File "falsimentis-email-guesses.txt"
At line:1 char:113
+ ... $lastnames) { Write-Host "$first.$last@falsimentis.com" } } | Out-Fil ...
+                                                                 ~
An empty pipe element is not allowed.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : EmptyPipeElement

The problem is this: ForEach doesn't output to the pipeline when it is used as a statement. But, ForEach does output to the pipeline when it is used as an alias for the ForEach-Object cmdlet.

If you're confused about that, you're not alone.

Let's take a look at how this PowerShell loop construct can have significantly different use cases. First, foreach is a PowerShell statement, meaning it can be used like this:

foreach ($currentobject in $listofobjects) {
    Write-Host $currentobject
}

In this use case, foreach does not output to the pipeline, preventing us from using it with later cmdlets, like Out-File. Here, foreach is a PowerShell statement; it is not the ForEach alias for ForEach-Object. Using ForEach-Object or the ForEach alias would require an object to precede it, like this:

$listofobjects | ForEach { 
    Write-Host $_
}

Here's the bottom line: When you use foreach as a statement, you can't leverage the output in the pipeline normally; when you use ForEach as an alias for ForEach-Object in the pipeline, you can.

Confounded by this, I asked some colleagues to come up with solutions to the challenge of merging two files and writing the output to a new file.

Solution 1: Mixed ForEach

This is the solution from the amazing Jon Gorenflo:

PS C:\temp> $firstnames = Get-Content .\firstnames.txt
PS C:\temp> $lastnames = Get-Content .\lastnames.txt
PS C:\temp> $firstnames | ForEach { foreach ($lastname in $lastnames) { "$_.$lastname@falsimentis.com" } } | Out-File "falsimentis-email-guesses.txt"
PS C:\temp> gci .\falsimentis-email-guesses.txt


    Directory: C:\temp


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         6/21/2022  11:28 AM          24962 falsimentis-email-guesses.txt


PS C:\temp> Get-Content .\falsimentis-email-guesses.txt | Select-Object -First 3
andrea.allen@falsimentis.com
andrea.brown@falsimentis.com
andrea.davis@falsimentis.com

Jon's solution is interesting because it uses a mix of ForEach as an alias for the ForEach-Object cmdlet and foreach as a statement (respectively; I've capitalized them to match in the preceding example). From what I understand, because the pipeline is created using $firstnames | ForEach, then subsequent foreach statements will continue to output to the pipeline.

WiredTired
It's ninja to use both variations of ForEach in one command.It's syntactically awkward to use different syntax for ForEach. Using a mix of $_ and $lastname variables seems inconsistent.

Solution 2: Mixed ForEach

This is the solution from my #MOPS collaborator Mick Douglas:

PS C:\temp> $firstnames = Get-Content .\firstnames.txt
PS C:\temp> $lastnames = Get-Content .\lastnames.txt
PS C:\temp> foreach ($first in $firstnames) { foreach ($last in $lastnames) { "$first.$last@falsimentis.com" | Add-Content -Path ".\falsimentis-email-guesses.txt" } }
PS C:\temp> Get-Content .\falsimentis-email-guesses.txt | Select-Object -First 3
andrea.allen@falsimentis.com
andrea.brown@falsimentis.com
andrea.davis@falsimentis.com

NOTE: I've changed Mick's original solution to use variable expansion in the string for consistency with the other solutions; Mick originally wrote this using string concatenation.

Mick's solution uses the foreach statement consistently for both loops. If we were to add the Out-File pipeline at the end of the 2nd block, we'd get the An empty pipe element is not allowed error message. However, Mick uses the pipeline within the 2nd foreach block, adding one email address at a time using Add-Content. This works because we're not trying to pipeline the foreach statement, we're just using the pipeline for the string variable expansion for "$first.$last@falsimentis.com" and multiple Add-Content file writes.

WiredTired
Consistent use of foreach. Intuitive, and easy to read.Each loop iteration writes to the file using Add-Content; this is OK for small files, but inefficient for large data sets since it is a lot of small writes instead of a small number of big writes.

Solution 3: Subexpressions

This is the solution I came up with:

PS C:\temp> $firstnames = Get-Content .\firstnames.txt
PS C:\temp> $lastnames = Get-Content .\lastnames.txt
PS C:\temp> $(foreach ($first in $firstnames) { foreach ($last in $lastnames) { "$first.$last@falsimentis.com" } }) | Out-File "falsimentis-email-guesses.txt"
PS C:\temp> Get-Content .\falsimentis-email-guesses.txt | Select-Object -First 3
andrea.allen@falsimentis.com
andrea.brown@falsimentis.com
andrea.davis@falsimentis.com

My initial attempt looks similar, where the email addresses are formed and we use the pipeline to write the results to a file. Here though, the foreach loops are embedded in a subexpression using PowerShell's $() operator.

By enclosing the original PowerShell in the subexpression operator, we get an object of type System.Array as the output:

PS C:\temp> $(foreach ($first in $firstnames) { foreach ($last in $lastnames) { "$first.$last@falsimentis.com" } }).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

The array is a collection of strings for each email address generated in the 2nd foreach block. Since it's an array, we can use the pipeline and Out-File to write it to a file.

WiredTired
Uses the pipeline in a consistent manner with common pipeline use. Reduces the number of write operations (faster).The closing }}) syntax makes me wonder if I should just write Async JavaScript and get it over with.

Closing Thoughts

PowerShell's immediate differentiator from other scripting languages is the power of the pipeline. However, this isn't always as consistently accessible as we might like, and it requires a deeper understanding of PowerShell functionality to leverage well. Knowing the differences between the foreach statement and the ForEach alias for ForEach-Object is important, and helps us to know why a simple statement doesn't work the way you might expect.

Special thanks to Kirk Munro for his articles Essential PowerShell: Understanding foreach and Essential PowerShell: Understanding foreach (Addendum) that were instrumental in helping me understand the nuances of foreach.

-Joshua Wright

Return to Getting Started With PowerShell


Joshua Wright is the author of SANS SEC504: Hacker Tools, Techniques, and Incident Handling, a faculty fellow for the SANS Institute, and a senior technical director at Counter Hack.

Month of PowerShell: Merging Two Files (Understanding ForEach) | SANS Institute