In PowerShell I have an array of string objects, and I have an object that contains string objects. In Java you can do a .equals(aObject) to test if the string values match, whereas doing a == test if the two objects refer to the same location in memory.
How do I run an equivalent .equals(aObject) in powershell?
I'm doing this:
$arrayOfStrings[0].Title -matches $myObject.item(0).Title
These both have the exact same string values, but I get a return value of false. Any suggestions?
You want to do $arrayOfString[0].Title -eq $myPbiject.item(0).Title
-match is for regex matching ( the second argument is a regex )
You can do it in two different ways.
Option 1: The -eq operator
>$a = "is"
>$b = "fission"
>$c = "is"
>$a -eq $c
True
>$a -eq $b
False
Option 2: The .Equals() method of the string object. Because strings in PowerShell are .Net System.String objects, any method of that object can be called directly.
>$a.equals($b)
False
>$a.equals($c)
True
>$a|get-member -membertype method
List of System.String methods follows.
Related
I wrote the following function to split a string into an array of strings. There are circumstances when the input is $null, in which case the function should return $null, or when the input already is a string array, in which case the input should be returned as-is.
function Split-Tests($tests)
{
if ($tests -eq $null)
{
return $tests
}
if (($tests.GetType() -eq [string[]]) -and $tests.Count -ne 1)
{
return $tests
}
return ([string]$tests).Split(",")
}
The function should return either $null, or a string array. However, when I call this function like Split-Tests "1,2,3", the returned value has type object[] even though the string.Split function returns string[].
I tried an explicit cast to string[] (return [string[]](([string]$tests).Split(","))) instead, and I tried the [OutputType([string[]])] attribute, but the return type remained at object[].
As a final resort, I cast the result of the function invocation to [string[]]. That works, but I would rather define the return type within the function that outside it. Can you help me?
Edit: I found this answer indicating that I could add a comma between "return" and the return value. Unfortunately, in my case it didn't help. Not even "Write-Output" mentioned in this reply made a change.
Edit again: The comma trick did it, I must've done something wrong in my first attempt.
This is the normal behaviour. Unlike other programming languages, PowerShell unrolls arrays and outputs them element-by-element to the pipeline, as a stream. Even the return statement in PowerShell doesn't actually return the given object as-is, but outputs to the pipeline as well.
I. e.
return ([string]$tests).Split(",")
is just a shortcut for:
([string]$tests).Split(",") # Output to the pipeline
return # Return from the function
When the output gets captured into a variable, PowerShell just sees the individual elements as passed to the pipeline. It doesn't know about the original array type. As the values from the pipeline could be of different types, it can only create a generic object[] array, which accepts any element type.
Function Fun { return 1,2,3,'a','b','c' }
$x = Fun # Now x contains 3 ints and 3 strings.
To force output of a string[] array, you can use the unary form of the comma-operator in front of the array to prevent enumeration:
function Split-Tests( $tests)
{
if ($tests -eq $null)
{
return $tests
}
if (($tests.GetType() -eq [string[]]) -and $tests.Count -ne 1)
{
return ,$tests
}
,([string]$tests).Split(",") # No return statement needed here
}
$a = Split-Tests "1,2,3"
$a.GetType().Name # Outputs "String[]"
I have a Hash Table like this:
$Path = #{
"BM" = "\\srv\xy"
"BB4-L" = "\\srv\xy"
"BB4-R" = "\\srv\xy"
"HSB" = "\\srv\xy"
"IB" = "\\srv\xy"
"LM1" = "\\srv\xy"
"LM2" = "\\srv\xy"
"sis" = "\\srv\xy"
}
my $env:username is sis. Why does .contains() and -contains something different?
PS Z:\Powershell-Scripts\Functions> $Path -contains $env:username
False
PS Z:\Powershell-Scripts\Functions> $Path.contains($env:username)
True
I always like to go with the PowerShell Syntax if possible, but I can't in this case, since -contains would return false.
How are .contains() and -contains different?
$Path is a System.Collections.Hashtable. You can also read in documentation that:
When the test value is a collection, the Contains operator uses
reference equality. It returns TRUE only when one of the reference
values is the same instance of the test value object.
Each item in hashtable is System.Collections.DictionaryEntry. You are comparing it to string. Since types do not match, references do not match as well. Contains(System.Object key) and ContainsKey(System.Object key) use keys to test. To be consistent in comparisons you should write:
$Path.Keys -contains $env:username
From MS documentation:
-Contains
Description: Containment operator. Tells whether a collection of reference
values includes a single test value. Always returns a Boolean value. Returns TRUE
only when the test value exactly matches at least one of the reference values.
.Contains() Method is one of the methods of a String object that supports substring hence why you get
True when you run $Path.Contains($env:username)
I got the code to work by changing from hashtable.containskey to -contains. In the following code snippit, i commented our the line containing .containsKey($UserIDFromAD) and added the line containing $UserIDsFromFile.keys -contains $UserIDFromAD. The code now works as expected.
So, whats the difference between .containskey and -contains?
#if ($UserIDsFromFile.containsKey($UserIDFromAD)){ #If the UserID is in Yesterdays list ignore it.
if ($UserIDsFromFile.keys -contains $UserIDFromAD){ #If the UserID is in Yesterdays list ignore it.
An interesting and weird thing I noticed writing PowerShell classes lines:
class A {
[object] WhereObject(){
return #(1,2) | Where-Object {$_ -gt 2}
}
[object] Where(){
return #(1,2).Where( {$_ -gt 2})
}
}
$a = new-object A
$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.
$a.Where() # Works well
It looks like it is by design. Why does it work so?
Workaround
Function which explicitly convert "empty" value to $null:
function Get-NullIfEmpty {
param(
[Parameter(ValueFromPipeline=$true)][array] $CollectionOrEmtpy
)
begin { $output = $null }
process
{
if($output -eq $null -and $CollectionOrEmtpy -ne $null){
$output = #()
}
foreach ($element in $CollectionOrEmtpy)
{
$output += $element
}
}
end { return $output }
}
In this case, the method will look like:
[object] WhereObject() {
return #(1,2) | Where-Object {$_ -gt 2} | Get-NullIfEmpty
}
I tried to return an empty array from the class method, but it is also tricky because for a regular function an empty array means "nothing" as well. If you have a call chain like method1 -> function -> method2 - method1 throw the same exception. Because the function converts an empty array to nothing.
So converting to $null is optimal in my case :)
The (PowerShell v4+) .Where() method, which is evaluated in expression mode, always returns an instance of [System.Collections.ObjectModel.Collection[psobject]]:
If no input objects match, that instance is simply empty (it has no elements and its .Count property returns 0).
By contrast, the Where-Object cmdlet uses pipeline semantics, which implies the following output behavior:
If nothing is output (if nothing matches the filter script block), the return value is a "null collection", which is technically the [System.Management.Automation.Internal.AutomationNull]::Value singleton.
If a single item matches, that item is output as-is.
If multiple items match and they are collected in a variable / evaluated as part of an expression, they are collected in an [object[]] array.
As for the specific symptom - which Bruce Payette's answer has since confirmed to be a bug.
Update: The bug is fixed since at least v7; returning "nothing" (AutomationNull) is now coerced to $null; see the original bug report on GitHub.
An internal [List[object]] instance is used to collect the method call's output, executed via an internal pipeline. If that internal pipeline outputs "nothing" - i.e., [System.Management.Automation.Internal.AutomationNull]::Value - no object is added to the list. However, subsequent code assumes that there is at least one object in the list and blindly accesses index 0, causing the error at hand.
A simpler reproduction of the problem:
class A {
# Try to return [System.Management.Automation.Internal.AutomationNull]::Value
# (which is what `& {}` produces).
[object] WhereObject(){ return & {} }
}
$a = new-object A
$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.
As for the desirable behavior:
It seems that the fix will result in $null getting output if the method's code returns the "null collection", using C#'s default-value feature - see this comment.
The .Where() operator always returns a Collection<PSObject>. The pipeline case however, returns nothing. This is a problem because the code that invokes the scriptblock expects there to be an object in the result List i.e. result.Count == 1. There are no objects in the pipeline case so you get an index-out-of-range error. So this is a bug. We should still generate an error but it should be "non-void methods must return a value" or some such. BTW - the code in question is here.
how does putting a comma before a list affect its type?
Have a look at the following code:
function StartProgram
{
$firstList = getListMethodOne
Write-Host "firstList is of type $($firstList.gettype())"
$secondList = getListMethodTwo
Write-Host "secondList is of type $($secondList.gettype())"
}
function getListMethodOne
{
$list = new-object system.collections.generic.list[string]
$list.Add("foo") #If there is one element, $list is of type String
$list.Add("bar") #If there is more than one element, $list is of type System.Object[]
return $list
}
function getListMethodTwo
{
$list = new-object system.collections.generic.list[string]
$list.Add("foo")
$list.Add("bar")
return ,$list #This is always of type List[string]
}
StartProgram
Why is it, if you don't use a comma before returning $list in getListMethodOne it returns as type System.Object[], whereas if you do use a comma as in getListMethodTwo, it is of type List[string] as expected?
PS: I'm using PSVersion 4.0
When you return collection, than PowerShell is kind enough to unravel it for you.
Unary comma creates collection with single element, so "external" collection gets unraveled and collection you want to return is kept.
I blogged about it a while ago.
Two more things:
return is used in PowerShell to leave function early, it's not needed to return something from function (any not captured output is returned)
in PowerShell 4.0 you can use Write-Output -NoEnumerate $collection to prevent unraveling your collection.
I don't have a full answer, but I would bet this relates to PowerShell's 'flattening' behaviour.
By using the unary operator ',' you are creating a new collection wrapper around the $list object. After PowerShell 'flattens' this, you see the object that was inside the wrapper.
A fuller explaination here: http://rkeithhill.wordpress.com/2007/09/24/effective-powershell-item-8-output-cardinality-scalars-collections-and-empty-sets-oh-my/
I want to write a cmdlet that reads multiple records from a database and puts them onto the pipeline.
I think I can do either a single WriteObject(Enumerable<rec>, true) or I can loop myself and call WriteObject multiple times.
What's the difference between these two?
Here is the documentation: Cmdlet.WriteObject Method (Object, Boolean)
And here is the example:
# Writes objects one by one
function Test1
{
[CmdletBinding()]param()
$data | %{ $PSCmdlet.WriteObject($_) }
}
# Writes the collection and allows the core to enumerate it one level.
# Numbers of written objects is the collection item count.
function Test2
{
[CmdletBinding()]param()
$PSCmdlet.WriteObject($data, $true)
}
# Writes the collection as a single object.
# Numbers of written objects is 1.
function Test3
{
[CmdletBinding()]param()
$PSCmdlet.WriteObject($data, $false)
}
function Test
{
(Test1).GetType().Name
(Test2).GetType().Name
(Test3).GetType().Name
}
$data = New-Object System.Collections.ArrayList
Write-Host "1 item"
$null = $data.Add('hello')
Test
Write-Host "2+ items"
$null = $data.Add('world')
Test
Output:
1 item
String
String
ArrayList
2+ items
Object[]
Object[]
ArrayList
Thus, calling WriteObject(item) for each item in a collection is basically the same as WriteObject(items, true); in both cases the collection itself has gone.
WriteObject(items, false) is different; it returns a reference to the collection and the caller can use that effectively depending on a scenario. For example, if a collection is a DataTable object (not unrolled set of DataRow items) then a caller can operate on DataTable members of the returned object.
Well, WriteObject(Object, boolean) will allow you to output a collection and have it stay intact (if called with "false" for the second argument). Normally PowerShell will enumerate any collections that get put on the pipeline.
So you could output a string array and the result would be of type [String[]]. While if you let PowerShell unwrap it, it will be an array of strings in a [Object[]].
You can also call that overload with "true" and it will be just like a loop calling WriteObject(Object).