PowerShell list inherited methods with Get-Member - powershell

I am exploring what pipeline functionality is also available as methods. For example...
#('1', '2', '3', '2') | where {$_ -in #('2')} will return the two '2'. But I can also do...
#('1', '2', '3', '2').Where({$_ -in #('2')}) and get the same result.
I also want to Group, and while
#('1', '2', '3', '2') | group {$_.Count -gt 1} doesn't do the actual grouping I want, it does... something. But
#('1', '2', '3', '2').Group({$_.Count -gt 1}) fails with Method invocation failed because [System.String] does not contain a method named 'Group'.
So that got me looking for what IS available as intrinsic methods.
#('1') | Get-Member -MemberType Method -Force | Format-Table doesn't even include Where and yet the Method is there. So I assume it is inheriting that method. But I don't know how to include inherited Methods. I though -Force would do it but it doesn't, nor does -View provide any more detail.
Is there a way to explore these intrinsic methods such as .Where()? And perhaps tangentially, is there a way to Group as a method rather than with the pipeline?

Dabombber's answer is helpful, but let me try to give a systematic overview:
The .Where() and .ForEach() array methods are instances of what are known as intrinsic members; no other such intrinsic array-processing (collection-processing) methods exist as of PowerShell 7.1
Intrinsic members are members (properties and methods) that the PowerShell engine exposes on objects of any type - except if the object has a native .NET type member of the same name, which takes precedence.
Discovery limitations, as of PowerShell 7.1 / late 2020:
Intrinsic members cannot be discovered via Get-Member
GitHub proposal #11798 aims to introduce this ability.
Tab completion works only for .Where() and .ForEach() (on collection values), but not for other intrinsic members, such as the .psobject property (see below).
However, they are documented in the conceptual about_Intrinsic_Members help topic.
In short: as of this writing, you cannot discover intrinsic members programmatically.
List of intrinsic members:
Intrinsic members that enable unified treatment of collections (arrays) and scalars:
PowerShell adds .Count and .Length properties (aliases of each other) even to scalars (assuming they don't have type-native properties of the same name), with an non-null scalar sensibly reporting 1 (e.g., (42).Count), and $null reporting 0 ($null.Count)
Pitfalls:
Accessing these properties while Set-StrictMode -Version 2 or higher is in effect unexpectedly causes statement-terminating errors, because the engine treats them as non-existent; this long-standing bug is discussed in GitHub issue #2798.
IEnumerable instances such as returned by LINQ methods aren't collections per se, and calling .Count on them triggers enumeration and returns a .Count property value from each enumerated element instead (which defaults to 1); e.g., [Linq.Enumerable]::Range(1,3).Count returns array 1, 1, 1 instead of 3.
There is a bug in Windows PowerShell (as of the latest and final version, 5.1), which has since been corrected in PowerShell (Core): [pscustomobject] instances unexpectedly do not have .Count and .Length properties (see GitHub issue #3671 for the original bug report) - see this answer.
Similarly, PowerShell allows you to index even into a scalar (again, unless preempted by a type-native indexer, such as that of XmlElement); e.g., (42)[0] and (42)[-1] both return 42, i.e. the scalar itself.
Collection-processing intrinsic members:
These are (only) the .Where() and .ForEach() methods discussed above.
Note that, in the the interest of unified treatment of collections and scalars, these methods also work on scalars; e.g., (42).ForEach({ $_ + 1 }) yields 43.
Pitfalls:
In Windows PowerShell (since fixed in PowerShell [Core] 7+), some types of scalars - notably [pscustomobject] and [xml] - do not provide these methods, which should be considered a bug.
The System.Collections.Generic.List<T> collection type has its own .ForEach() method that therefore shadows PowerShell's; this type-native method neither supports the use of $_ nor producing output.
Intrinsic members for reflection (not available on $null):
The .psobject property is a rich source of reflection on any object, such as its list of properties; e.g., (Get-Date).psobject.Properties lists metadata about all public properties of instances of System.DateTime.
.pstypenames (also available as .psobject.TypeNames) lists all ETS (Extended Type System) type names associated with an instance; by default, the property contains the full names of the object's .NET type and its base type(s).
The .psbase, .psextended, and .psadapted properties return categorized subsets of type members, namely .NET-native, ETS-added, and adapted members.
Adapted members are members that surface information from a different data representation as if they were native type members, notably in the context of CIM (WMI) and PowerShell's adaptation of the XML DOM.
For instance, [xml] (System.Xml.XmlDocument) instances have members in all three categories; try $xml = ([xml] '<someElement>some text</someElement>'); '-- .psbase:'; $xml.psbase; '-- .psextended'; $xml.psextended; '-- .psadapted'; $xml.psadapted

Your arrays are getting unrolled through the pipeline, so #('1') | Get-Member -MemberType Method -Force is showing members for a string.
You can bypass that by sending it as the second object of an array
,#('1') | Get-Member -MemberType Method
outputting it without enumeration
Write-Output -NoEnumerate #('1') | Get-Member -MemberType Method
or passing it as a parameter instead of through the pipeline.
Get-Member -InputObject #('1') -MemberType Method
Some of the methods may also be static, so you could use the -Static switch for Get-Member.
There is a list of array methods here, although I have no idea why not all of them show up, perhaps it's an intentional part of the Get-Member cmdlet or maybe they're part of the PowerShell language itself rather than proper methods.

Related

powershell, how to safely store variable type for future use

I load some config from a JSON file and I want to be able to check what I've just loaded and potentially set some default values.
for now, I create a hash table containing info then load and check this way
$config_info=#{
foo=#{default='' ;type=[string]}
bar=#{default=#();type=[array] }
}
$config = Get-Content $configPath -Raw | ConvertFrom-Json
if(-not [Bool]$config.PSObject.Properties['foo']) {
$config | Add-Member -MemberType NoteProperty -Name 'foo' -Value $config_info.foo.default
}
if($config.foo -isnot $config_info.foo.type) { ... }
but I'm wondering how safe is the notation type=[string] and if I should rather use type=''.getType() witch looks less natural.
Update 1
in the past I used Export-Clixml/Import-Clixml for my config files but the resulting files are hardly human readable/ editable.
I also sometimes used dot-sourcing PS1 files but for the current project, config has to be maintain by non-powershell people.
What I'm wondering is, is type=[string] a safe/ lasting syntax to store ''.getType() in a variable, or not.
What I'm wondering is, is type=[string] a safe/ lasting syntax to store ''.getType() in a variable, or not.
Yes, assuming that the .NET [string] type (System.String) isn't being shadowed by a custom class definition of the same name (which would be unwise to do).
[string] is a PowerShell type literal, and referring to a .NET type this way works predictably, assuming the type has been loaded into the session, which is by definition true for built-in types such as System.String. You can refer to a .NET type:
by its accelerator name, if defined; such as [string] or [regex]; use the .FullName property to see the type's full name; e.g. [regex].FullName
by its full name, though note that you're free to omit the System. part of the namespace, e.g., you can refer to System.Text.Encoding as [System.Text.Encoding] or [Text.Encoding]
by its assembly-qualified name; e.g. (obtained with
[string].AssemblyQualifiedName):
[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]`
However:
This doesn't work across PowerShell editions, at least not for built-in types.
It is rarely necessary and can give the false impression that a given assembly version is being referenced (.NET seemingly loads whatever version is available)
However, in .NET (Core) / PowerShell (Core) only, you may omit the Version field; similarly, Culture and PublicKeyToken are optional.
While I doubt the need will arise, especially with respect to built-in types, at least hypothetically you can then disambiguate types with the same full name by their hosting assembly; e.g.:
# PS Core (v7+) only; the PublicKeyToken field may be omitted.
[System.String, System.Private.CoreLib, PublicKeyToken=7cec85d7bea7798e]
A simpler PowerShell (Core) 7+ solution:
In PowerShell (Core) 7+, ConvertFrom-Json has an -AsHashtable switch returns the parsing results as hashtables rather than as [pscustomobject] instances.
Given that you can cast from a hashtable to a custom class type literal, you can use strongly typed properties with default values that correspond to the properties in the input JSON data.
# Custom class that describes the structure of the config data,
# with strongly typed properties and default values.
class Config {
[string] $foo = '(none)'
[int[]] $bar = #()
}
# Sample JSON input; parse it into a hashtable.
$ht = #'
{
"bar": [1, 2]
}
'# | ConvertFrom-Json -AsHashtable
# Construct a [Config] instance from the values in the hashtable,
# enforcing data types, with on-demand conversion.
[Config] $ht
Output (note how foo has its default value):
foo bar
--- ---
(none) {1, 2}
Note:
PowerShell's flexible automatic type conversions are used when constructing the Config instance, and given that PowerShell's allows any data type to be converted to [string], non-string input for [string]-typed properties is still accepted (resulting in stringification).
When a value is of the wrong type and cannot be converted (e.g., a value of "abc" for an [int]-typed property), the [Config] cast results in a statement-terminating error.
If you need more stringent type-checking, consider using validation by a JSON schema, which Test-Json supports via the -Schema and -SchemaFile parameters, in PowerShell (Core) only, as zett42 suggests.

Select elements from powershell objects

Hi everybody!
Short question:
Why does this work:
(Get-CimClass Win32_Share).CimClassMethods['Create'].Parameters
But this doesn’t:
(Get-CimClass Win32_Share).CimClassMethods.Create.Parameters
The same result can be achieved by:
((Get-CimClass Win32_Share).CimClassMethods|where-object {$_.name -match 'create'}).parameters
With the same logic I would expect that the same applies for:
(Get-command *help*)['get-help']
Or
(Get-command *help*)['cmdlet']
Or
(Get-childitem)['test.txt']
But it doesn’t. Only the method with where-object is possible here
Other considerations:
I know that this is should be the default way to retrive items from an hashtable and not pscustomobject but I also would like to better understand where else I can use this method. I searched the whole day on google but didn’t find anything.
Thanks in advance
Franco
The string-based indexing (['Create']) works, because the Microsoft.Management.Infrastructure.Internal.Data.CimMethodDeclarationCollection type that it is applied to implements a parameterized Item property that has a [string]-typed parameter (abstracted; verify with Get-Member Item -InputObject Get-CimClass Win32_Share).CimClassMethods):
# {get;} indicates that the property is read-only.
Item ParameterizedProperty <T> Item(string key) {get;}
Note: Strictly speaking, for something like ['Create'] to be accepted, it is sufficient for the Item property parameter to be [object]-typed, which is the case for the non-generic [hashtable] type, for instance.
For types with such a method, PowerShell, like C#, supports syntax ['Create'] as syntactic sugar for .Item('Create'), and the implication is that a key-based item lookup is performed.
Typically, such types also implement the IDictionary interface (e.g., [hasthable]), but that isn't the case here.
CimMethodDeclarationCollection behaves similarly to an [ordered] hashtable, in that it supports both positional (e.g, [0]) and key-based indexing (e.g., ['Create']),[1] but with one crucial difference:
PowerShell does not enumerate IDictionary-implementing types in the pipeline (you'll have to call .GetEnumerator() to achieve that), whereas CimMethodDeclarationCollection - due to not implementing IDictionary - is enumerated, like other collections such as arrays; that is, its elements / entries are sent one by one through the pipeline.
As for why something like the following doesn't work:
(Get-command help)['get-help']
Get-command *help* outputs multiple objects, which (...) automatically collects in a regular PowerShell array, of type [object[]].
[object[]] (based on System.Array), does not have the requisite parameterized property that would support ['get-help'] syntax - arrays support only positional indexing (e.g. [0] to refer to the first element), based on a parameterized Item property whose parameter is [int]-typed, implemented as part of the IList interface (abstracted; verify with Get-Member Item -InputObject #()):
# Note: IList.Item indicates that the property is implemented
# as part of the IList interface.
# {get;set;} indicates that the property is read-write.
Item ParameterizedProperty <T> IList.Item(int index) {get;set;}
However, given PowerShell's member-access enumeration feature it is reasonable to expect ['get-help'] to then be applied to the individual elements of the array, given that's how it works with ., the member-access operator (e.g., (Get-Command *help*).Name).
As of PowerShell 7.2.4, however, only ., not also ["someKey"] performs this enumeration; this surprising asymmetry is the subject of GitHub issue #17514.
[1] An [int]-typed key doesn't necessarily imply positional semantics, but does in the context of implementing the IList or ICollection interfaces (or their generic equivalent).

Why can't I call IEnumerable.Sum() on an array in PowerShell? [duplicate]

I am trying to use LINQ in PowerShell. It seems like this should be entirely possible since PowerShell is built on top of the .NET Framework, but I cannot get it to work. For example, when I try the following (contrived) code:
$data = 0..10
[System.Linq.Enumerable]::Where($data, { param($x) $x -gt 5 })
I get the following error:
Cannot find an overload for "Where" and the argument count: "2".
Never mind the fact that this could be accomplished with Where-Object. The point of this question is not to find an idiomatic way of doing this one operation in PowerShell. Some tasks would be light-years easier to do in PowerShell if I could use LINQ.
The problem with your code is that PowerShell cannot decide to which specific delegate type the ScriptBlock instance ({ ... }) should be cast.
So it isn't able to choose a type-concrete delegate instantiation for the generic 2nd parameter of the Where method. And it also does't have syntax to specify a generic parameter explicitly. To resolve this problem, you need to cast the ScriptBlock instance to the right delegate type yourself:
$data = 0..10
[System.Linq.Enumerable]::Where($data, [Func[object,bool]]{ param($x) $x -gt 5 })
Why does [Func[object, bool]] work, but [Func[int, bool]] does not?
Because your $data is [object[]], not [int[]], given that PowerShell creates [object[]] arrays by default; you can, however, construct [int[]] instances explicitly:
$intdata = [int[]]$data
[System.Linq.Enumerable]::Where($intdata, [Func[int,bool]]{ param($x) $x -gt 5 })
To complement PetSerAl's helpful answer with a broader answer to match the question's generic title:
Note: The following applies up to at least PowerShell 7.2. Direct support for LINQ - with syntax comparable to the one in C# - is being discussed for a future version of PowerShell Core in GitHub issue #2226.
Using LINQ in PowerShell:
You need PowerShell v3 or higher.
You cannot call the LINQ extension methods directly on collection instances and instead must invoke the LINQ methods as static methods of the [System.Linq.Enumerable] type to which you pass the input collection as the first argument.
Having to do so takes away the fluidity of the LINQ API, because method chaining is no longer an option. Instead, you must nest static calls, in reverse order.
E.g., instead of $inputCollection.Where(...).OrderBy(...) you must write [Linq.Enumerable]::OrderBy([Linq.Enumerable]::Where($inputCollection, ...), ...)
Helper functions and classes:
Some methods, such as .Select(), have parameters that accept generic Func<> delegates (e.g, Func<T,TResult> can be created using PowerShell code, via a cast applied to a script block; e.g.:
[Func[object, bool]] { $Args[0].ToString() -eq 'foo' }
The first generic type parameter of Func<> delegates must match the type of the elements of the input collection; keep in mind that PowerShell creates [object[]] arrays by default.
Some methods, such as .Contains() and .OrderBy have parameters that accept objects that implement specific interfaces, such as IEqualityComparer<T> and IComparer<T>; additionally, input types may need to implement IEquatable<T> in order for comparisons to work as intended, such as with .Distinct(); all these require compiled classes written, typically, in C# (though you can create them from PowerShell by passing a string with embedded C# code to the Add-Type cmdlet); in PSv5+, however, you may also use custom PowerShell classes, with some limitations.
Generic methods:
Some LINQ methods themselves are generic and therefore require one or more type arguments.
In PowerShell (Core) 7.2- and Windows PowerShell, PowerShell cannot directly call such methods and must use reflection instead, because it only supports inferring type arguments, which cannot be done in this case; e.g.:
# Obtain a [string]-instantiated method of OfType<T>.
$ofTypeString = [Linq.Enumerable].GetMethod("OfType").MakeGenericMethod([string])
# Output only [string] elements in the collection.
# Note how the array must be nested for the method signature to be recognized.
PS> $ofTypeString.Invoke($null, (, ('abc', 12, 'def')))
abc
def
For a more elaborate example, see this answer.
In PowerShell (Core) 7.3+, you now have the option of specifying type arguments explicitly (see the conceptual about_Calling_Generic_Methods help topic); e.g.:
# Output only [string] elements in the collection.
# Note the need to enclose the input array in (...)
# -> 'abc', 'def'
[Linq.Enumerable]::OfType[string](('abc', 12, 'def'))
The LINQ methods return a lazy enumerable rather than an actual collection; that is, what is returned isn't the actual data yet, but something that will produce the data when enumerated.
In contexts where enumeration is automatically performed, notably in the pipeline, you'll be able to use the enumerable as if it were a collection.
However, since the enumerable isn't itself a collection, you cannot get the result count by invoking .Count nor can you index into the iterator; however, you can use member-access enumeration (extracting the values of a property of the objects being enumerated).
If you do need the results as a static array to get the usual collection behavior, wrap the invocation in [Linq.Enumerable]::ToArray(...).
Similar methods that return different data structures exist, such as ::ToList().
For an advanced example, see this answer.
For an overview of all LINQ methods including examples, see this great article.
In short: using LINQ from PowerShell is cumbersome and is only worth the effort if any of the following apply:
you need advanced query features that PowerShell's cmdlets cannot provide.
performance is paramount - see this article.
If you want to achieve LINQ like functionality then PowerShell has some cmdlets and functions, for instance: Select-Object, Where-Object, Sort-Object, Group-Object. It has cmdlets for most of LINQ features like Projection, Restriction, Ordering, Grouping, Partitioning, etc.
See Powershell One-Liners: Collections and LINQ.
For more details on using Linq and possibly how to make it easier, the article LINQ Through Powershell may be helpful.
I ran accross LINQ, when wanting to have a stable sort in PowerShell (stable: if property to sort by has the same value on two (or more) elements: preserve their order). Sort-Object has a -Stable-Switch, but only in PS 6.1+. Also, the Sort()-Implementations in the Generic Collections in .NET are not stable, so I came accross LINQ, where documentation says it's stable.
Here's my (Test-)Code:
# Getting a stable sort in PowerShell, using LINQs OrderBy
# Testdata
# Generate List to Order and insert Data there. o will be sequential Number (original Index), i will be Property to sort for (with duplicates)
$list = [System.Collections.Generic.List[object]]::new()
foreach($i in 1..10000){
$list.Add([PSCustomObject]#{o=$i;i=$i % 50})
}
# Sort Data
# Order Object by using LINQ. Note that OrderBy does not sort. It's using Delayed Evaluation, so it will sort only when GetEnumerator is called.
$propertyToSortBy = "i" # if wanting to sort by another property, set its name here
$scriptBlock = [Scriptblock]::Create("param(`$x) `$x.$propertyToSortBy")
$resInter = [System.Linq.Enumerable]::OrderBy($list, [Func[object,object]]$scriptBlock )
# $resInter.GetEnumerator() | Out-Null
# $resInter is of Type System.Linq.OrderedEnumerable<...>. We'll copy results to a new Generic List
$res = [System.Collections.Generic.List[object]]::new()
foreach($elem in $resInter.GetEnumerator()){
$res.Add($elem)
}
# Validation
# Check Results. If PropertyToSort is the same as in previous record, but previous sequence-number is higher, than the Sort has not been stable
$propertyToSortBy = "i" ; $originalOrderProp = "o"
for($i = 1; $i -lt $res.Count ; $i++){
if(($res[$i-1].$propertyToSortBy -eq $res[$i].$propertyToSortBy) -and ($res[$i-1].$originalOrderProp -gt $res[$i].$originalOrderProp)){
Write-host "Error on line $i - Sort is not Stable! $($res[$i]), Previous: $($res[$i-1])"
}
}
There is a simple way to make Linq chaining fluent, by setting a using statement to the Linq namespace, Then you can call the where function directly, no need to call the static Where function.
using namespace System.Linq
$b.Where({$_ -gt 0})
$b is an array of bytes, and I want to get all bytes that are greater than 0.
Works perfect.

Get-ItemProperty with Registry, returned object type

I can use Get-Item with folders, files and registry keys, and the type of the object I get back will make sense; [System.IO.DirectoryInfo], [System.IO.FileInfo] or [Microsoft.Win32.RegistryKey].
But with registry properties, what using Get-ItemProperty returns is a [System.Management.Automation.PSCustomObject]. Is this because there is no dedicated type for registry property? That seems odd. But my Google-Fu is not turning anything up.
My use case is this, I am doing a series of Copy and Move tasks, with all four item types potentially getting copied or moved, and I want to implement an option to rename an existing destination rather than overwriting or failing. And exactly what the rename options are depends on the object type. And from a readability standpoint, PSCustom Object or a simple else for RegistryProperty is a bit ugly. So, looking for a way to get the property back as a type with a more obvious name, so when I look at the code again in 12 months it makes some sense.
Get-ItemProperty returns what is conceptually a registry value object: a property of a registry key that has a name and a - uh... - value (the named value object's data).
The .NET registry API has no type to represent such a value object - instead, it allows access via the registry key type's .GetValue($valueName) (to get a specific value object's data[1]) and .GetValueNames() methods (to get the list of all value names).
The PowerShell implementers apparently chose not to implement their own .NET type, and chose to use PowerShell's general-purpose, dynamic "property-bag" type, [pscustomobject][2] to model these value objects.
If you want to avoid the [pscustomobject] instances that Get-ItemProperty returns, you can use Get-Item instead, which returns a Microsoft.Win32.RegistryKey instance, i.e. an instance of the .NET type representing a key, on which you can invoke the methods mentioned above.
As an aside: If you're just interested in a given value object's data, you can use the PSv5+
Get-ItemPropertyValue cmdlet (e.g.
Get-ItemPropertyValue HKCU:\Console -Name LineWrap directly returns the [int] data of the targeted value).
[1] Additionally, as js2010's answer shows, the .GetValueKind() method returns an enum value that identifies a given value object's registry-specific data type. These types imply what .NET types are used to represent them, as returned by .GetValue(), but in some cases have no direct equivalent (ExpandString, MultiString, Unknown) and require additional work to interpret them correctly.
[2] It is possible - but wasn't done in this case - to assign (one or more) self-chosen type names to [pscustomobject] instances, which PowerShell reflects as the type name in Get-Member output (only the first, if there are multiple) and which it respects for ETS type definitions and format-data definitions. However, such pseudo types are not accessible as type literals; e.g.: $obj = [pscustomobject] #{ PSTypeName = 'MyType'; prop = 'foo' } allows you test for this type name with $obj.pstypenames -contains 'MyType', but not with $obj -is [MyType]. That said, you can base parameter declarations on them, via the [PSTypeName()] attribute.
There is a way to get the type of the properties:
$key = get-item hkcu:\key1
$key.GetValueKind('value1')
DWord

Compare-Object not working if I don't list the properties

I have 2 Excel spreadsheets I am trying to compare:
$OleDbAdapter = New-Object System.Data.OleDb.OleDbDataAdapter “Select * from [Report$]“,”Provider=Microsoft.ACE.OLEDB.12.0;Data Source=S:\FIS-BIC Reporting\Report Output Files\Product-Marketing\TEST_XI\ECM - Pipeline by LOB_04182013_040544.xls;Extended Properties=”"Excel 12.0 Xml;HDR=YES”";”
$RowsReturned = $OleDbAdapter.Fill($DataTable)
$OleDbAdapter2 = New-Object System.Data.OleDb.OleDbDataAdapter “Select * from [Report$]“,”Provider=Microsoft.ACE.OLEDB.12.0;Data Source=S:\FIS-BIC Reporting\Report Output Files\Product-Marketing\ECM - Pipeline by LOB_04182013_074004.xls;Extended Properties=”"Excel 12.0 Xml;HDR=YES”";”
$RowsReturned2 = $OleDbAdapter2.Fill($DataTable2)
Compare-Object $DataTable $DataTable2
It returns nothing. I know that in the 6th column, they are different. If I specify "-property F6", it does return the difference. Any idea why it doesn't unless I specify the property? The number of columns can vary (though will be the same for each of the files in the comparison), so specifying the properties specifically won't work.
If you don't specify the -Property parameter, Compare-Object doesn't compare all properties, it compares the results of invoking the .ToString() method on both objects. So, Compare-Object $DataTable $DataTable2 compares $DataTable1.ToString() with $DataTable1.ToString(). The .ToString() method returns an empty string when invoked on a DataTable object, so there is no difference to report.
For example:
$file1 = Get-Item somefilename
$file1 = Get-Item anotherfilename
Compare-Object $file1 $file2
This will return the difference between the full paths of the two files, like this:
InputObject SideIndicator
----------- -------------
<path>\anotherfilename =>
<path>\somefilename <=
That's because invoking .ToString() on a FileInfo object returns its FullName property, so you're comparing the files' full path names.
Although the -Property parameter accepts multiple properties, listing all the properties is not the solution. Aside from being very tedious, it will not give you the results you want. If you list multiple properties, Compare-Object compares the combination of all the properties, and if any one of the listed properties is different, it returns a result showing all the listed properties (both ones that are the same and ones that are different) as a single difference.
What you need to do is iterate over a list of properties, and invoke Compare-Object once for each property:
$properties = ($DataTable | Get-Member -MemberType Property | Select-Object -ExpandProperty Name)
foreach ($property in $properties) {
Compare-Object $DataTable $DataTable2 -Property "$property" | Format-Table -AutoSize
}
In most cases, when comparing all properties of two objects, you'd want to use Get-Member -MemberType Properties, in order to get cover all property types. However, if you're comparing DataTable objects, you're better off using Get-Member -MemberType Property so that you're comparing only the properties corresponding to data fields, not other properties of the DataTable objects that have nothing to do with the data.
This is written assuming that the number of columns is the same, as you stated, or at least that the number of columns in $DataTable2 doesn't exceed the number of columns in $DataTable.
If you can't reliably assume that, derive the $properties array from whichever one has more columns, by comparing ($DataTable | Get-Member -MemberType Property).Count with ($DataTable2 | Get-Member -MemberType Property).Count and using the properties from whichever is greater.
Using Format-Table is important, it's not just there to make things look pretty. If you list multiple objects of the same type (in this case, arrays), PowerShell remembers the format of the first object, and uses it for all subsequent objects, unless you explicitly specify a format. Since the name of the first column will be different for each property (i.e., each column from the spreadsheet), the first column will be empty for all but the first difference encountered.
The -AutoSize switch is optional. That is there just to make things look pretty. But you must pipe the results to a formatting filter. You can also use Format-List if you prefer.
Adi Inbar's helpful answer contains good background information about how Compare-Object works.
However, there is a way to use -Property generically to compare all column values - assuming that both input tables have the same column structure, or that the the tables should only be compared by the first table's columns:
# The original collection.
$coll1 = [pscustomobject] #{ one = '1a'; two = '2a'; three = '3a' },
[pscustomobject] #{ one = "1b"; two = "2b"; three = '3b' }
# The other collection to compare the original to.
# Note the difference in the 2nd object in column 'two'
$coll2 = [pscustomobject] #{ one = '1a'; two = '2a'; three = '3a' },
[pscustomobject] #{ one = "1b"; two = "2b!"; three = '3b' }
# PSv3+: Get the array of all property names to compare
# from the original collection.
# Note:
# * The assumption is that both collections have the same set of
# properties (or that the collections should only be compared by
# the *first* collection's properties).
# * In PSv2-, use the following instead:
# $propsToCompare = $coll1[0].psobject.properties | % { $_.name }
$propsToCompare = $coll1[0].psobject.properties.name
# Compare the 2 collections by all property values.
# -PassThru means that any input object in which a difference is found
# is passed through as-is.
Compare-Object $coll1 $coll2 -Property $propsToCompare -PassThru
The above yields:
one two three SideIndicator
--- --- ----- -------------
1b 2b! 3b =>
1b 2b 3b <=
Note how => tells you that the selected object is exclusive to the right side and vice versa for <=
A caveat is that Compare-Object is slow, because it cannot make assumptions about input data being sorted and therefore has to read and compare both input collections in full.
With sorted input, you can use -SyncWindow <Int32> to speed things up, but that requires advance knowledge of how many items at most can differ between the two input collections following each difference found, and if the -SyncWindow value is too small, spurious differences will be reported.
If you compare objects as a whole (no -Property argument), PowerShell uses the following comparison method:
Note:
Below, LHS refers to an object from the reference collection (-ReferenceObject) and RHS to an object from the difference collection (-DifferenceObject)
The short of it is: Unless the types involved implement IComparable (which is true for strings and all primitive .NET types (the CLR's number types and [char])) or have custom .ToString() implementations with instance-specific return values from which equality can be inferred, whole-object comparison will not be meaningful, and objects will be treated as equal even when they aren't.
The following logic is implemented in the TryCompare() engine method, which is used whenever two values must be compared, irrespective of context.
If the LHS is a string, string comparison is performed, which is case-insensitive and culture-invariant by default; the -CaseSensitive and -Culture parameters allow you to change that.
If both the LHS and the RHS are numbers of (potentially different) .NET primitive types, numeric comparison is performed.
Otherwise, an attempt is made to convert the RHS is to the type of the LHS and, if the type supports System.IComparable, its .CompareTo() method is called to determine equality.
Otherwise, the LHS and RHS are compared via the Object.Equals() method that all objects inherit or implement or, if the type implements interface IEquatable<T>, IEquatable<T>.Equals().
If System.Object.Equals() is called, and a given type doesn't override it, only value types (structs) composed only of other value-type instances will compare meaningfully; for reference types, only two references to the very same object instance are considered equal.
Caveat: As of PowerShell Core 7.1.0-preview.2, only if the .Equals() call returns true is the result used. The reason is that the comparison code is also used for ordering (sorting) values, where determining equality alone is not enough. In the context of Compare-Object, not using a false result is actually inappropriate for types that implement IEquatable<T> (and don't also implement IComparable) - see this GitHub issue.
The remaining logic comes from ObjectCommandComparer.Compare() (which is also used by Get-Unique and Select-Object -Unique):
If none of the above methods apply, the objects are compared by their .ToString() representations (with the same string-comparison specifics described above).
Note that for many types these will not result in meaningful comparison, because the default .ToString() implementation simply returns the full type name (e.g., 'System.IO.FileInfo'), so that all instances of that type compare the same.