How do I iterate through JSON array in powershell - powershell

How do I iterate through JSON array which is converted to PSCustomObject with ConvertFrom-JSON? Using foreach does not work.
$jsonArray ='[{"privateKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\key.pem"},
{"publicKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\cert.pem"},
{"publicKeyCALocation" : "C:\\ProgramData\\docker\\certs.d\\ca.pem"}]'
$json = convertfrom-json $jsonArray
$json | foreach {$_}
Returns
privateKeyLocation
------------------
C:\ProgramData\docker\certs.d\key.pem
Enumerator though says there are 3 members of array
>$json.Count
3

The problem that you are having is not specific to it being a JSON array, it has to do with how custom objects in an array are displayed by default. The simplest answer is to pipe it to Format-List (or FL for short).
PS C:\Users\TMTech> $JSON|FL
privateKeyLocation : C:\ProgramData\docker\certs.d\key.pem
publicKeyLocation : C:\ProgramData\docker\certs.d\cert.pem
publicKeyCALocation : C:\ProgramData\docker\certs.d\ca.pem
Aside from that, when PowerShell outputs an array of objects it bases the columns that it displays upon the properties of the first object in the array. In your case that object has one property named 'privateKeyLocation', so that is the only column that appears, and since the other two objects do not have that property it does not display anything for them. If you want to keep it as a table you could gather all potential properties, and add them to the first item with null values, and that would allow you to display it as a table, but it still wouldn't look very good:
$json|%{$_.psobject.properties.name}|select -Unique|?{$_ -notin $json[0].psobject.Properties.Name}|%{Add-Member -InputObject $JSON[0] -NotePropertyName $_ -NotePropertyValue $null}
Then you can output as a table and get everything:
PS C:\Users\TMTech> $json
privateKeyLocation publicKeyLocation publicKeyCALocation
------------------ ----------------- -------------------
C:\ProgramData\docker\certs.d\key.pem
C:\ProgramData\docker\certs.d\cert.pem
C:\ProgramData\docker\certs.d\ca.pem
Edit: To get the value of each object in this case is tricky, because the property that you want to expand keeps changing for each object. There's two ways to do this that I can think of, what I would consider the right way, and then there's the easy way. The right way to do it would be to determine the property that you want to expand, and then reference that property directly:
$JSON |%{
$PropName = $_.PSObject.Properties.Name
$_.$PropName
}
That'll do what you want, but I think easier would be to pipe to Format-List, then Out-String, wrap the whole thing in parenthesis, split on new lines and replace everything up to : which should just leave you with the paths you want.
($JSON|FL|Out-String) -split '[\r\n]+' -replace '(?m)^.+ : '|?{$_}

Interesting enough. I responded to this exact question from the same OP on another forum. Though my response was just RegEx and be done with it, with no additional conversion.
Of course there are several ways to do this. The below is just what I came up with.
$jsonArray = '[{"privateKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\key.pem"},
{"publicKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\cert.pem"},
{"publicKeyCALocation" : "C:\\ProgramData\\docker\\certs.d\\ca.pem"}]'
([regex]::Matches($jsonArray,'(?<=\").:\\[^\"]+(?=\")').Value) -replace '\\\\','\' `
| ForEach {
If (Test-Path -Path $_)
{"path $_ found"}
Else {Write-Warning "Path $_ not found"}
}
WARNING: Path C:\ProgramData\docker\certs.d\key.pem not found
WARNING: Path C:\ProgramData\docker\certs.d\cert.pem not found
WARNING: Path C:\ProgramData\docker\certs.d\ca.pem not found
So, maybe not as elegant as what was posted here, but it would get the OP where they wanted to be.
So, consolidating everything TheMadTechnician gave and what the OP is after, and attempting to make it as concise as possible, would give the OP the below (I added a element to show a positive response):
Clear-Host
($jsonArray = #'
[{"privateKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\key.pem"},
{"publicKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\cert.pem"},
{"publicKeyCALocation" : "C:\\ProgramData\\docker\\certs.d\\ca.pem"},
{"publicKeyTestFileLocation" : "D:\\Temp\\test.txt"}]
'# | ConvertFrom-Json | Format-List | Out-String) -split '[\r\n]+' -replace '(?m)^.+ : '`
| Where-Object {$_} | ForEach {
If(Test-Path -Path $_){"The path $_ was found"}
Else{Write-Warning -Message "The path $_ was not found}"}
}
WARNING: The path C:\ProgramData\docker\certs.d\key.pem was not found}
WARNING: The path C:\ProgramData\docker\certs.d\cert.pem was not found}
WARNING: The path C:\ProgramData\docker\certs.d\ca.pem was not found}
The path D:\Temp\test.txt was found
Which one is more to his liking is a matter of the OP choice of course.
The performance between the two varied on each test run, but the fastest time using the straight RegEx approach was:
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 43
Ticks : 439652
TotalDays : 5.08856481481481E-07
TotalHours : 1.22125555555556E-05
TotalMinutes : 0.000732753333333333
TotalSeconds : 0.0439652
TotalMilliseconds : 43.9652
and the fastest on the consolidated version here was:
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 54
Ticks : 547810
TotalDays : 6.34039351851852E-07
TotalHours : 1.52169444444444E-05
TotalMinutes : 0.000913016666666667
TotalSeconds : 0.054781
TotalMilliseconds : 54.781
Updating to add iRon's take on this topic
So this...
$jsonArray ='[{"privateKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\key.pem"},
{"publicKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\cert.pem"},
{"publicKeyCALocation" : "C:\\ProgramData\\docker\\certs.d\\ca.pem"}]'
$json = convertfrom-json $jsonArray
$json | ForEach {
$Key = $_.psobject.properties.name;
"Testing for key " + $_.$Key
Test-Path -Path $_.$Key
}
Testing for key C:\ProgramData\docker\certs.d\key.pem
False
Testing for key C:\ProgramData\docker\certs.d\cert.pem
False
Testing for key C:\ProgramData\docker\certs.d\ca.pem
False
... and this:
('[{"privateKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\key.pem"},
{"publicKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\cert.pem"},
{"publicKeyCALocation" : "C:\\ProgramData\\docker\\certs.d\\ca.pem"}]' `
| convertfrom-json) | ForEach {
$Key = $_.psobject.properties.name;
"Testing for key " + $_.$Key
Test-Path -Path $_.$Key
}
Testing for key C:\ProgramData\docker\certs.d\key.pem
False
Testing for key C:\ProgramData\docker\certs.d\cert.pem
False
Testing for key C:\ProgramData\docker\certs.d\ca.pem
False

Most simple way, should be like this
$ret ='[your json]'
$ret | ConvertFrom-Json
$data = $ret | ConvertFrom-Json
foreach($data in $ret | ConvertFrom-Json) {
Write-Host $data;
}

You can index into the array. Check out $json.GetType()
$jsonArray ='[{"privateKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\key.pem"},
{"publicKeyLocation" : "C:\\ProgramData\\docker\\certs.d\\cert.pem"},
{"publicKeyCALocation" : "C:\\ProgramData\\docker\\certs.d\\ca.pem"}]'
$json = convertfrom-json $jsonArray
foreach($i in 0..($json.Count-1)){
$json[$i] | out-host
$i++
}

You can use ForEach-Object i.e:
$json | ForEach-Object -Process { Write-Hoste $_; }
That's I believe the simplest way and gives you easy access to properties if array contains objects with other properties.

Related

Format-Table for array of hashtables

I am trying to use the Format-Table command to output an array of hash tables of all files checked out from our TFS repo.
My code thus far:
$arr = #();
#Take the string from the tf command, parse it and build an array of hash tables
(tf stat /recursive /user:* /format:detailed | Select-String -Pattern '^\$' -NotMatch | Select -SkipLast 3 | Out-String) -split '(\r\n){2}' | ForEach-Object {
$ht = #{};
if ($_ -ne '') {
$str = $_ | Out-String;
$str -split '\r?\n'| ForEach-Object {
$key, $value = $_ -split '\s*:\s*';
#Write-Host $key, $Value;
try {
$ht.Add($key, $value);
} catch [ArgumentException] {
Write-Host "Caught exception";
}
}
$arr += ($ht);
}
}
Edit
Looks like I'm erroring out here.
$arr.ForEach({[PSCustomObject]$_}) | Format-Table -AutoSize
Full Error:
Cannot convert value "System.Collections.Hashtable" to type
"System.Management.Automation.LanguagePrimitives+InternalPSCustomObject". Error: "Cannot process argument because the
value of argument "name" is not valid. Change the value of the "name" argument and run the operation again."
At C:\Dev\Tools\powershell\Convert-TfsOutput.ps1:21 char:15
+ $arr.ForEach({[PSCustomObject]$_}) | Format-Table -AutoSize
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastConstructorException
Edit2
Here is sample output when i replace the above line with:
$arr.ForEach({ $_ | Out-String })
Name Value
---- -----
Workspace work1
Date {Wednesday, September 5, 2018 1, 38, 48 PM}
Local item file1
File type Windows-1252
User user1
Lock none
Change edit
Name Value
---- -----
Workspace work2
Date {Monday, September 10, 2018 12, 14, 56 PM}
Local item file2
User user2
Lock none
Change edit
Edit 3
Output of the below command
Write-Host $str;
User : User1
Date : Wednesday, September 5, 2018 1:38:48 PM
Lock : none
Change : edit
Workspace : Work1
Local item : File1
File type : Windows-1252
User : User2
Date : Monday, September 10, 2018 12:14:56 PM
Lock : none
Change : edit
Workspace : Work2
Local item : File2
Would like the output in a tabular format with rows below the column names:
Workspace | Date | Local item | File type | User | Lock | Change
Tried to use the code in another answer but it does not output correctly.
Format-Table on Array of Hash Tables
Convert your hashtables to custom objects before passing them to Format-Table.
... | Where-Object { $_ } | ForEach-Object {
$ht = #{};
($_ | Out-String) -split '\r?\n'| ForEach-Object {
...
}
New-Object -Type PSObject -Property $ht
} | Format-Table
Edit: Looks like your input data has blank lines which lead to keys with empty strings in your hashtables, which then cause the error you observed, because objects can't have a property with an empty string for a name.
Change your hashtable/object creation to something like this:
... | Where-Object { $_ } | ForEach-Object {
$ht = ($_ | Out-String).Trim() -replace '\s+:\s+', '=' |
ConvertFrom-StringData
New-Object -Type PSObject -Property $ht
} | Format-Table

Adding Left-Padding to PowerShell Out-String

I'm trying to making a small library of PowerShell functions to make things easier for myself and my team. For example:
$toReturn=Get-ADUser $name -Properties msRTCSIP-UserEnabled| Select msRTCSIP-UserEnabled | Format-List | Out-String
Where the output is like:
msRTCSIP-UserEnabled : True
Now, all I want to do is add a few empty spaces before the output (msRTCSIP-UserEnabled).
Basically, I want to add a certain amount of spaces to any Out-String.
Note: I'm not pulling just one value, its more like a bunch of them, so the output looks more like:
msRTCSIP-UserEnabled : True
msRTCSIP-DeploymentLocator : SRV:
msRTCSIP-PrimaryUserAddress : SIP:XXXX#XX.com
userPrincipalName : XXXX#XX.com
msRTCSIP-InternetAccessEnabled : True
msRTCSIP-FederationEnabled : True
Terminal Output
I'd like to indent the whole output a little more to the right in the terminal.
There are multiple ways of doing what you want. For example:
$data = #(
"msRTCSIP-UserEnabled : True",
"msRTCSIP-DeploymentLocator : SRV:",
"msRTCSIP-PrimaryUserAddress : SIP:XXXX#XX.com",
"userPrincipalName : XXXX#XX.com",
"msRTCSIP-InternetAccessEnabled : True",
"msRTCSIP-FederationEnabled : True"
)
$data | %{ "`t" + $_ } | Out-String
$data | %{ " {0}" -f $_} | Out-String
$data | %{ [string]::Format(" {0}", $_) }

Get hashtable value by key of group object under strict mode version 2?

The following script, which pivot the array list by x and y, doesn't work. ($hashvariable.x not working). How to rewrite it? It seems it's not easy to simple get a value by key in a hashtable under strict mode.
Set-StrictMode -version 2 # change 2 to 1 will work
$a = #('a','b','x',10),
#('a','b','y',20),
#('c','e','x',50),
#('c','e','y',30)
$a | %{
new-object PsObject -prop #{"label" = "'$($_[0])','$($_[1])'"; value=#{ $_[2]=$_[3]}}
} |
group label | % {
"$($_.Name), $($_.Group.value.x), $($_.Group.value.y)" # error
#"$($_.Name), $($_.Group.value['x']), $($_.Group.value['y'])" # empty for x,y
}
Expected result.
'a','b', 10, 20
'c','e', 50, 30
Error:
Property 'x' cannot be found on this object. Make sure that it exists.
At line:6 char:35
+ "[$(#($_.Name -split ",") + #($_.Group.value.x, $_.Group.value.y))]"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], PropertyNotFoundException
+ FullyQualifiedErrorId : PropertyNotFoundStrict
Not sure what you really want, but this is my best guess. You should accumulate all objects in one group in single Hashtable object instead of creating separate Hashtable for each input object:
$a = ('a','b','x',10),
('a','b','y',20),
('c','e','x',50),
('c','e','y',30)
$a |
Group-Object {$_[0]},{$_[1]} |
Select-Object Values,
#{
Name='Group'
Expression={
$_.Group |
ForEach-Object {$t=#{}}{$t.Add($_[2],$_[3])}{$t}
}
} |
ForEach-Object {
'''{0}'',''{1}'', {2}, {3}'-f#($_.Values;$_.Group['x','y'])
}
It is difficult to understand what, in fact, this script does, anyway, the problem is that only first element in a collection $_.Group.Value has the property 'x', at the same time, strict mode prohibits references to non-existent properties of an object, so you receive this error.
Here is a solution, hope I've understood right what you want:
$a = #('a','b','x',10),
#('a','b','y',20),
#('c','e','x',50),
#('c','e','y',30)
$hashtable = #{}
$a |%{
$hashtable["$($_[0]) $($_[1])"] +=
[hashtable]#{$_[2] = $_[3]}
}
$hashtable.GetEnumerator() | %{
"$($_.Key) $($_.Value['x']) $($_.Value['y'])"
}

Cover flat file using mapping to table (eg. csv)

I have following several hundred entries in text file like this:
DisplayName : John, Smith
UPN : MY3043241#domain.local
Status : DeviceOk
DeviceID : ApplC39HJ3JPDTF9
DeviceEnableOutboundSMS : False
DeviceMobileOperator :
DeviceAccessState : Allowed
DeviceAccessStateReason : Global
DeviceAccessControlRule :
DeviceType : iPhone
DeviceUserAgent : Apple-iPhone4C1/902.206
DeviceModel : iPhone
... about 1500 entries of the above with blank line between each.
I'm looking to create a table with following headers from the above:
DisplayName,UPN,Status,DeviceID,DeviceEnableOutboundSMS,DeviceMobileOperator,DeviceAccessState,DeviceAccessStateReason,DeviceAccessControlRule,DeviceType,DeviceUserAgent,DeviceModel
The question is, is there a tool or some easy way to do this in excel or other application. I know it is easy task to write a simple algorithm, unfortunately I cannot go that route. Powershell would be an option but I'm not good at so if you have any tips on how to approach this that route please, let me know.
Although I am a big fan on Powershell one-liners, it wouldn't be of much help to someone trying to learn or start out with it. More so getting buy-in, in a corporate setting.
I have written a cmdlet, documentation included, to get you started.
function Import-UserDevice {
Param
(
# Path of the text file we are importing records from.
[string] $Path
)
if (-not (Test-Path -Path $Path)) { throw "Data file not found: $Path" }
# Use the StreamReader for efficiency.
$reader = [System.IO.File]::OpenText($Path)
# Create the initial record.
$entry = New-Object -TypeName psobject
while(-not $reader.EndOfStream) {
# Trimming is necessary to remove empty spaces.
$line = $reader.ReadLine().Trim()
# An empty line would indicate we need to start a new record.
if ($line.Length -le 0 -and -not $reader.EndOfStream) {
# Output the completed record and prepare a new record.
$entry
$entry = New-Object -TypeName psobject
continue
}
# Split the line through ':' to get properties names and values.
$entry | Add-Member -MemberType NoteProperty -Name $line.Split(':')[0].Trim() -Value $line.Split(':')[1].Trim()
}
# Output the residual record.
$entry
# Close the file.
$reader.Close()
}
Here's an example of how you could use it to export records to CSV.
Import-UserDevice -Path C:\temp\data.txt | Export-Csv C:\TEMP\report.csv -NoTypeInformation
Powershell answer here... I used a test file C:\Temp\test.txt which contains:
DisplayName : John, Smith
UPN : MY3043241#domain.local
Status : DeviceOk
DeviceID : ApplC39HJ3JPDTF9
DeviceEnableOutboundSMS : False
DeviceMobileOperator :
DeviceAccessState : Allowed
DeviceAccessStateReason : Global
DeviceAccessControlRule :
DeviceType : iPhone
DeviceUserAgent : Apple-iPhone4C1/902.206
DeviceModel : iPhone
DisplayName : Mary, Anderson
UPN : AR456789#domain.local
Status : DeviceOk
DeviceID : ApplC39HJ3JPDTF8
DeviceEnableOutboundSMS : False
DeviceMobileOperator :
DeviceAccessState : Allowed
DeviceAccessStateReason : Global
DeviceAccessControlRule :
DeviceType : iPhone
DeviceUserAgent : Apple-iPhone4C1/902.206
DeviceModel : iPhone
So that I could have multiple records to parse. Then I ran it against this script which creates an empty array $users, gets the content of that file 13 lines at a time (12 fields + the empty line). Then it creates a custom object with no properties. Then for each of the 13 lines, if the line is not empty it creates a new property for that object we just created, where the name is everything before the : and the value is everything after it (with spaces removed from the end of the name and value). Then it adds that object to the array.
$users=#()
gc c:\temp\test.log -ReadCount 13|%{
$User = new-object psobject
$_|?{!([string]::IsNullOrEmpty($_))}|%{
Add-Member -InputObject $User -MemberType NoteProperty -Name ($_.Split(":")[0].TrimEnd(" ")) -Value ($_.Split(":")[1].TrimEnd(" "))
}
$users+=$User
}
Once you have the array $Users filled you could do something like:
$Users | Export-CSV C:\Temp\NewFile.csv -notypeinfo
That gives you a CSV that you would expect it to.
For an input file with just a couple hundred records I'd probably read the entire file, split it at empty lines, split the text blocks at line breaks, and the lines at colons. Somewhat like this:
$infile = 'C:\path\to\input.txt'
$outfile = 'C:\path\to\output.csv'
[IO.File]::ReadAllText($infile).Trim() -split "`r`n`r`n" | % {
$o = New-Object -Type PSObject
$_.Trim() -split "`r`n" | % {
$a = "$_ :" -split '\s*:\s*'
$o | Add-Member -Type NoteProperty -Name $a[0] -Value $a[1]
}
$o
} | Export-Csv $outfile -NoType

Cannot execute same powershell function twice, gives error

I'm fairly new to powershell and I'm basically writing a script which performs a join on several .csv files based on a primary column. I am using the Join-Collections script from here: http://poshcode.org/1461
As I need to combine 5 .csv files, I need to run this function 4 times.
On the first run, it works fine, but then trying to run the function again gives 'No object specified to the cmd-let' errors.
In trying to debug, I've literally copy-and-pasted the line and only changed the variable name to make a new variable.
I must be doing something fundamentally wrong...
$SumFile = "VMSummary.csv"
$MemFile = "VMMemory.csv"
$ProcFile = "VMProcessor.csv"
$OSFile = "VMOS.csv"
$NicFile = "VMNics.csv"
$SumFileCSV = Import-Csv $SumFile | Select VMElementName,GuestOS,Heartbeat,MemoryUsage,IpAddress
$MemFileCSV = Import-Csv $MemFile | Select VMElementName,Reservation
$ProcFileCSV = Import-Csv $ProcFile
$OSFileCSV = Import-Csv $OSFile
$NicFileCSV = Import-Csv $NicFile
$JoinColumn = "VMElementName"
function Join-Collections {
PARAM(
$FirstCollection
, [string]$FirstJoinColumn
, $SecondCollection
, [string]$SecondJoinColumn=$FirstJoinColumn
)
PROCESS {
$ErrorActionPreference = "Inquire"
foreach($first in $FirstCollection) {
$SecondCollection | Where{ $_."$SecondJoinColumn" -eq $first."$FirstJoinColumn" } | Join-Object $first
}
}
BEGIN {
function Join-Object {
Param(
[Parameter(Position=0)]
$First
,
[Parameter(ValueFromPipeline=$true)]
$Second
)
BEGIN {
[string[]] $p1 = $First | gm -type Properties | select -expand Name
}
Process {
$Output = $First | Select $p1
foreach($p in $Second | gm -type Properties | Where { $p1 -notcontains $_.Name } | select -expand Name) {
Add-Member -in $Output -type NoteProperty -name $p -value $Second."$p"
}
$Output
}
}
}
}
$Temp = Join-Collections $SumFileCSV $JoinColumn $MemFileCSV $JoinColumn
$Temp
##BREAKS HERE
$Temp2 = Join-Collections $SumFileCSV $JoinColumn $MemFileCSV $JoinColumn
UPDATE
It gives the following error:
No object has been specified to the get-member cmdlet
+ foreach($p) in $Second | gm <<<< -type Properties | Where { $p1 -notcontains $_.Name } | select -expand Name)
The csv data is pretty straight forward. When I print out $Temp just before it breaks, it spits out:
GuestOS : Windows Server (R) 2008 Standard
Heartbeat : OK
IpAddress : 192.168.48.92
MemoryUsage : 1024
VMElementName : VM015
Reservation : 1024
GuestOS : Windows Server (R) 2008 Standard
Heartbeat : OK
IpAddress : 192.168.48.151
MemoryUsage : 1028
VMElementName : VM053
Reservation : 1028
GuestOS : Windows Server (R) 2008 Standard
Heartbeat : OK
IpAddress : 192.168.48.214
MemoryUsage : 3084
VMElementName : VM065
Reservation : 3084
GuestOS :
Heartbeat :
IpAddress :
MemoryUsage :
VMElementName : VM074
Reservation : 1024
GuestOS : Windows Server 2008 R2 Standard
Heartbeat : OK
IpAddress : 192.168.48.32
MemoryUsage : 3072
VMElementName : VM088
Reservation : 3072
GuestOS : Windows Server (R) 2008 Enterprise
Heartbeat : OK
IpAddress : 192.168.48.81
MemoryUsage : 3084
VMElementName : VM090
Reservation : 3084
GuestOS : Windows Server 2008 R2 Enterprise
Heartbeat : OK
IpAddress : 192.168.48.82
MemoryUsage : 5120
VMElementName : VM106
Reservation : 5120
The rest of the .csv data is the same sort of stuff - just stats on different servers.
Ideally what I want to do is this :
$Temp = Join-Collections $SumFileCSV $JoinColumn $MemFileCSV $JoinColumn
$Temp = Join-Collections $Temp $JoinColumn $ProcFileCSV $JoinColumn
$Temp = Join-Collections $Temp $JoinColumn $OSFileCSV $JoinColumn
$Temp = Join-Collections $Temp $JoinColumn $NicFileCSV $JoinColumn | Export-Csv "VMJoined.csv" -NoTypeInformation -UseCulture
This code works fine on Powershell v3 CTP 2 (which is probably what #manojlds is using). In Powershell V2 however the parameter $second of the Join-Object function is not bound when invoking Join-Collections the second time. This can be easily verified by adding the following line to the process block inside the Join-Object function:
$psboundparameters | out-host
You will notice that when invoking Join-Collections for the first time both parameters (of Join-Object are bound, however the second time $second is no longer bound.
It is unclear what is causing this behaviour, but since it seems to be working in Powershell V3 I'm guessing it's a bug.
To make the function work in Powershell V2 one could explicitly bind the parameters by replacing this line:
$SecondCollection | Where{ $_."$SecondJoinColumn" -eq $first."$FirstJoinColumn" } | Join-Object $first
by this line:
$SecondCollection | Where{ $_."$SecondJoinColumn" -eq $first."$FirstJoinColumn" } | %{Join-Object -first $first -second $_}