Compare-Object different property names - powershell

Is there a way to use Compare-Object -property, for comparing 2 properties with different names? I have something like this:
$ComparisonProperty= $ComparisonProperty | Add-Member -MemberType AliasProperty -Name Url -Value ITEM_TARGET_URI -PassThru
Compare-Object $FirstFile $SecondFile -Property Url -PassThru | Where-Object{$_.SideIndicator -eq "<="} | Out-file .\result.txt
But this gives an error:
Cannot bind argument to parameter 'InputObject' because it is null
At $ComparisonProperty
//Edit
Sample data are 2 csv files with many headers, 1 with Url header in it, another with ITEM_TARGET_URI in it. Result should be strings from file 1 that do not exist in file 2. Comparison works if i provide them with the same property names, but the whole point is to force it to compare 2 properties with different names.

To answer the actual question calculated properties or property aliases would get you what you need. You have been trying to use the latter but you need to add the property to the file object itself. Consider the two file examples
id,first_name
1,Elia
2,Nikolos
3,Bert
4,Sharleen
5,Bill
id,beginning_name
1,Elia
2,Nikolos
3,Bert
4,Mildrid
5,Bill
Notice that the headers are different. So now lets try and create the property alias. Assume that I have already imported these files as CSV's
$file2 | Add-Member -MemberType AliasProperty -Name first_name -Value beginning_name
compare-object $file1 $file2 -Property first_name
That will give you the results you were expecting. I added an alias to the second file object
You could just work with column data
Another approach to this is to drop the properties and just work with string arrays. Since I know the headers I want I can just get those columns themselves as well
$namesfromfile1 = $file1 | Select-Object -ExpandProperty first_name
$namesfromfile2 = $file2 | Select-Object -ExpandProperty beginning_name
Compare-Object $urls1 $urls2
Depending on your PS version this can also be shortened. Whether or not its simplified is up to the reader.
Compare-Object ($file1).first_name ($file2).beginning_name

Related

Powershell eq operator saying hashes are different, while Write-Host is showing the opposite

I have a script that periodically generates a list of all files in a directory, and then writes a text file of the results to a different directory.
I'd like to change this so it checks the newest text file in the output directory, and only makes a new one if there's differences. It seemed simple enough.
Here's what I tried:
First I get the most recent file in the directory, grab the hash, and write my variable values to the console:
$lastFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$oldHash = Get-FileHash $lastFile | Select-Object Hash
Write-Host 'lastFile = '$lastFile
Write-Host 'oldHash = '$oldHash
Output:
lastFile = C:\ReportOutputDir\test1.txt
oldHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Then I do the exact same gci on the FileList dir, and create a new file (new_test.txt), then grab the hash of this file:
gci -Path C:\FileLists -File -Recurse -Name -Depth 2 | Sort-Object | out-file C:\ReportOutputDir\new_test.txt
$newFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$newHash = Get-FileHash $newFile | Select-Object Hash
Write-Host 'newFile = '$newFile
Write-Host 'newHash = '$newHash
Output:
newFile = C:\ReportOutputDir\new_test.txt
newHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Finally, I attempt my -eq operator where I'd usually simply remove the newFile if it's equal. For now, I'm just doing a simple :
if ($newHash -eq $oldHash){
'files are equal'
}
else {'files are not equal'}
And somehow, I'm getting
files are not equal
What gives? Also, for the record I was originally trying to save the gci output to a variable and comparing the contents of the last file to the gci output, but was also having trouble with the -eq operator. Fairly new to powershell stuff so I'm sure I'm doing something wrong here.
Select-Object Hash creates an object with a .Hash property and it is that property that contains the hash string.
The object returned is of type [pscustomobject], and two instances of this type never compare as equal - even if all their property names and values are equal:
The reason is that reference equality is tested, because [pscustomobject] is a .NET reference type that doesn't define custom equality-testing logic.
Testing reference equality means that only two references to the very same instance compare as equal.
A quick example:
PS> [pscustomobject] #{ foo = 1 } -eq [pscustomobject] #{ foo = 1 }
False # !! Two distinct instances aren't equal, no matter what they contain.
You have two options:
Compare the .Hash property values, not the objects as a whole:
if ($newHash.Hash -eq $oldHash.Hash) { # ...
If you don't need a [pscustomobject] wrapper for the hash strings, use Select-Object's -ExpandProperty parameter instead of the (possibly positionally implied) -Property parameter:
Select-Object -ExpandProperty Hash
As for why the Write-Host output matched:
When you force objects to be converted to string representations - essentially, Write-Host calls .ToString() on its arguments - the string representations of distinct [pscustomobject] instances that have the same properties and values will be the same:
PS> "$([pscustomobject] #{ foo = 1 })" -eq "$([pscustomobject] #{ foo = 1 })"
True # Same as: '#{foo=1}' -eq '#{foo=1}'
However, you should not rely on these hashtable-like string representations to determine equality of [pscustomobject]s as a whole, because of the inherent limitations of these representations, which can easily yield false positives.
This answer shows how to compare [pscustomobject] instances as a whole, by comparing all of their property values, by passing all property names to Compare-Object -Property - but note that this assumes that all property values are either strings or instances of .NET value types or corresponding properties must again either reference the very same instance of a .NET reference type or be of a type that implements custom equality-comparison logic.

Possible to sort object by property (descending) and still display corresponding values?

$procs = Get-Process
$procs[0] | GM -MemberType Property
This gives all the properties in ascending order. Now, I'd like to display the properties in descending order but with it's values. It's easy to sort the properties themselves...
$procs[0] | GM -MemberType Property | Select-Object Name | Sort-Object -Descending -Property Name
...however this does not display the corresponding values.
I'm also able to sort values of one property in a descending manner but not sort by properties themselves.
EDIT: Here's a screenshot to better illustrate what I'd like to achieve:
What you can see here is the sample output of Get-Process | Where-Object { $_.Name -like "a*" } | Export-Csv -Path "$env:USERPROFILE\Desktop\example.csv"
As you can see the properties are not sorted. I would like to be able to output exactly what the screenshot shows but with the property columns and their corresponding values sorted in descending order, i.e. WS, VM, SI, PM, NPM, Name, Handles.
I hope I was able to clearly explain it now.
Thanks for your help.
It happens because, after all in your pipeline result is Hashtable with strings.
Save result in variable and give it into pipe like this,it's reveal each property in detail by putting Name of property after variable In "dot notation"
$propertys = (get-process)[0] | GM -MemberType Property | Select-Object Name | Sort-Object -Descending -Property Name
($propertys.NAME) | %{(get-process)[0].$_}
or use short presentation of properties by using FL
(get-process)[0] | fl $($propertys.Name[0..$propertys.Count])

Edit an object non-destructively in PowerShell

I am trying to format the resulting object without destroying it. But all my efforts and research has failed me. Any tips are welcome.
My code looks like this:
Set-Location 'C:\Temp'
$Files = Get-ChildItem -File | Select-Object FullName, Length
And what I get, is this:
FullName Length
-------- ------
C:\Temp\CleanupScript.txt 10600
C:\Temp\Columns.csv 4214
C:\Temp\Content.html 271034
C:\Temp\Content.txt 271034
C:\Temp\DirSizes.csv 78
What I want is this:
FullName Length
-------- ------
Temp\CleanupScript.txt 10600
Temp\Columns.csv 4214
Temp\Content.html 271034
Temp\Content.txt 271034
Temp\DirSizes.csv 78
When I tried this:
$Files = Get-ChildItem -File | Select-Object FullName, Length | % { $_.FullName.Remove(0, 3) }
I got the right result, but I lost the Length column.
PS C:\Temp> $Files
Temp\CleanupScript.txt
Temp\Columns.csv
Temp\Content.html
Temp\Content.txt
Temp\DirSizes.csv
Please help.
Big thanks
Patrik
The easiest way to do this is to construct the property you want in the Select command, such as:
$Files = Get-ChildItem -File | Select #{l='FullName';e={$_.FullName.Substring(3)}},Length
The format for this is a hashtable with two entries. The keys are lable (or name), and expression. You can shorten them to l (or n), and e. The label entry defines the name of the property you are constructing, and the expression defines the value.
If you want to retain all of the original methods and properties of the objects you should add a property to them rather than using calculated properties. You can do that with Add-Member as such:
$Files = GCI -File | %{Add-Member -inputobject $_ -notepropertyname 'ShortPath' -notepropertyvalue $_.FullName.Substring(3) -PassThru}
Then you can use that property by name like $Files | FT ShortPath,Length -Auto, while still retaining the ability to use the file's methods like Copy() and what not.
I would recommend using a calculated property and Split-Path -NoQualifier; e.g.:
Get-ChildItem -File | Select-Object `
#{Name = "NameNoQualifier"; Expression = {Split-Path $_.FullName -NoQualifier}},
Length
For help on calculated properties, see the help for Select-Object.
(Aside: To correct your terminology a bit, this is not modifying objects non-destructively but rather outputting new objects containing the properties you want formatted how you want them.)

How To Get The Value Of Header In CSV

In PowerShell I want to pass the name of a header in a CSV file into another function in the PowerShell script.
How can I retrieve the value-text of a header name into a variable in CSV?
e.g. if I have the following CSV data:
ID Name Country
-- ---- -------
1 John United States
2 Beatrice Germany
3 Jouni Finland
4 Marcel France
In the above example how can I retrieve the Country column value text as "Country" text into a variable in my script?
(Note: I am familiar with the notation $_.Country to retrieve the value of, for example, "Germany" from a row by importing the CSV in Powershell)
My specific issue is that currently I have the following function in my script:
function GetItemIdFromTitle([string]$LookupTitle, [ref]$LookupId)
{
$LookupField = $LookupList.Fields["DEPTCATEGORY"]
$LookupItem = $LookupList.Items | where {$_['DEPTCATEGORY'] -like "*$LookupTitle*"}
$LookupId.Value = $LookupItem.ID
}
This currently takes a string value -> $LookupTitle and uses that to find an item in a SharePoint list. As you can see in the script I am hard-coding in the column name as "DEPTCATEGORY". This is the column name that will be looked up to in the SharePoint list.
Instead of hard-coding the column name I want to pass in the name of the column for the corresponding $LookupTitle value and replace the hard-coded "DEPTCATEGORY".
I am calling the above function as follows:
#GET THE LOOKUP COLUMN ID
GetItemIdFromTitle $_.DEPTCAT ([ref]$LookupIdValue)
( $_.DEPTCAT is the value from the row in the CSV column. )
Can I do something like
$myCountryColumnName = $_.Country.Property.Title
or
$myCategoryColumnName = $_.DEPTCAT.Property.Name
to get the column name from the CSV?
If you have an object in $obj, you could list all the property headers like this:
$obj | Get-member -MemberType 'NoteProperty' | Select-Object -ExpandProperty 'Name'
This is an array, so you can reference them individually like this:
($obj | Get-member -MemberType 'NoteProperty' | Select-Object -ExpandProperty 'Name')[0]
This would just give you the name of the first property for instance.
Assuming that you have already read in the CSV using Import-CSV you can examine the property of the resulting object. Similar to this answer but maintains column order in the resulting array
Examining the first row / element
$data = import-csv $path
$data[0].psobject.properties.name
So the second line will return a string array of the properties.
To get the column name from the csv, first, put the names of the column headers into an array (this will also allow you to loop through each column, if needed) ...
$inFilePath = "C:\path\to\file.csv"
$csvColumnNames = (Get-Content $inFilePath | Select-Object -First 1).Split(",")
... , secondly, index into the array by column position (index starts at 0). Given your original example it would be;
$myCountryColumnName = $csvColumnNames[2]
This is more of a general comment than an answer.
I needed to pull the first column header name from CSVs and I started with selecting the NoteProperty fields from Get-Member. This doesn't work because the order of the NoteProperty column header names might not match the order of the column headers in the CSV file.
futureSPQR's method will work every time because the text won't get reordered on you. Below is my one-liner version of his method to get the first column header name.
((Get-Content filename.csv)[0] -split(','))[0]
$f = Import-Csv "...csv"
Get-Member -InputObject $f[0] |
Where-Object {$_.MemberType -eq "NoteProperty"} |
select-object Name
If you're looking to check if a header name exists in the array, use below code
$Array | Get-member -MemberType 'NoteProperty'| where {$_.Name -eq "ColumnName"}

Is there a way to force powershell -Export-CSV cmdlet to maintain a specific column order?

I'm new to powershell, so the script is a Frankenstein of various examples of from sites.
My question is how can I make sure that a csv file I am creating for a DataTable keeps the column order I specify?
My script does this to populate the csv headers and values like so:
...snip...
$dataTable | ForEach-Object {
$csv+=New-Object PSObject -Property #{
program_code=$_.ProgramCode;
first_name=$_.FirstName;
last_name=$_.LastName;
email=$_.email;
phone=$_.phone;
phone2=$_.otherphone;
address=$_.addr1;
address2=$_.addr2;
city=$_.city;
state=$_.state;
postal_code=$_.Zip;
country=$_.Country;
grad_year=$_.HsGradDate;
lead_date=$_.LeadDate;
lead_source=$_.LeadSource;
school_status=$_.SchoolStatus;
}
}
$csv | Export-CSV C:\scripts\NewLeads$(Get-Date -Format yyyyMMdd).csv -notype -Append
...snip...
I want the file to have to columns in the order that I specify in the script, but when I open it in notepad or excel the columns appear in a seemingly random order. Keyword being seemingly since they may have some method of ordering themselves.
In PowerShell V3, instead of:
$csv+=New-Object PSObject -Property #{
I would use:
$csv+=[pscustomobject]#{
The PowerShell V3 parser will preserve the order of the keys when you cast a hash literal to either [ordered] or [pscustomobject]. A small side benefit to this approach - it will also be faster.
If you are using V2, you'll need to skip the -Property parameter to New-Object and instead use multiple calls to Add-Member. It will look something like:
$csv+=New-Object PSObject |
Add-Member -Name program_code -Value $_.ProgramCode -MemberType NoteProperty -PassThru |
Add-Member -Name first_name -Value $_.FirstName -MemberType NoteProperty -PassThru |
...
Select the fields in the order required, then export.
$csv | select-object -property program_code,first_name,last_name,email,phone,phone2,address,address2,city,state,psotal_code,country,grad_year,lead_date,lead_source,school_status |
Export-CSV C:\scripts\NewLeads$(Get-Date -Format yyyyMMdd).csv -notype -Append
However, you may be able to short-circuit this a little. Depending on what $dataTable really is, you may (should, in most cases) be able to select directly from that object and bypass creating the collection of PSObjects. But if you need the custom headers, you'll need to use expressions in select-object (linebreaks for readability).
$dataTable| select-object #{Name="program_code";Expression={$_.ProgramCode}},`
#{Name="first_name";Expression={$_.firstname}},`
#{Name="last_name";Expression={$_.lastname}},email,phone,`
#{Name="phone2";Expression={$_.otherphone}},`
#{Name="addr1";Expression={$_.address}},`
#{Name="addr2";Expression={$_.address2}},city,state,`
#{Name="postal_code";Expression={$_.zip}},country,`
#{Name="grad_year";Expression={$_.hsgraddate}},`
#{Name="lead_date";Expression={$_.leaddate}},`
#{Name="lead_source";Expression={$_.leadsource}},`
#{Name="school_status ";Expression={$_.schoolstatus }}|
Export-CSV C:\scripts\NewLeads$(Get-Date -Format yyyyMMdd).csv -notype -Append