I have a foreach loop that currently puts three entries in my hashtable:
$result = foreach($key in $serverSpace.Keys){
if($serverSpace[$key] -lt 80){
[pscustomobject]#{
Server = $key
Space = $serverSpace[$key]}}}
When I use
$result.count
I get 3 as expected.
I changed the foreach loop to exlude the entries less than or equal to one using
$result = foreach($key in $serverSpace.Keys){
if($serverSpace[$key] -lt 80 -and $serverSpace[$key] -gt 1){
[pscustomobject]#{
Server = $key
Space = $serverSpace[$key]}}}
$result.count should have 1 as its output but it doesn't recognize .count as a suggested command and $result.count doesn't output anything anymore. I'm assuming when theres only one entry in the hash table it won't allow a count? Not sure whats going on but my conditions for my script are dependent on the count of $result. Any help would be appreciated.
$result is not a hashtable so I prefixed it with #($result).count. Thank you to #Theo and #Lee_Dailey
What you're seeing is a bug in Windows PowerShell (as of the latest and final version, 5.1), which has since been corrected in PowerShell (Core) - see GitHub issue #3671 for the original bug report.
That is, since v3 all objects should have an intrinsic .Count property , not just collections, in the interest of unified treatment of scalars and collections - see this answer for more information.
The workaround for Windows PowerShell is indeed to force a value to be an array via #(...), the array-subexpression operator, which is guaranteed to have a .Count property, as shown in your answer, but it shouldn't be necessary and indeed isn't anymore in PowerShell (Core, v6+)
# !! Due to a BUG, this outputs $null in *Windows PowerShell*,
# !! but correctly outputs 1 in PowerShell (Core).
([pscustomobject] #{}).Count
# Workaround for Windows PowerShell that is effective in *both* editions,
# though potentially wasteful in PowerShell (Core):
#([pscustomobject] #{}).Count
Related
I've made a most unfortunate typo costing me quite some precious time:
$errors.Count
This returns "0", even if there are errors, because the variable name should be singular. This does work:
$error.clear() # To ensure a correct repro
Copy-Item asdf fdsa # Will show an error
$error.Count # Will output "1"
However, I now want to know why $errors.Count gave me anything at all, and why it gave me "0". So I went on to do some testing, and got the following results:
$asdf.Count # Will output "0"
$nrOfEinsteinsInSpace.Count # Will output "0"
$a = 0; $a.Count; # Will output "1"
$b = 2; $a.Count; # Will output "1"
$x = 1,2,3; $x.Count; # Will output "3"
And gathering even more data to be able to ask a sensible question here I did:
$true.Count # Will output "1"
$false.Count # Will output "1"
So we have the following different cases:
Array(like) variables, where .Count will output the number of items.
Non-existent variables, where .Count will output "0".
Declared variables, where .Count will output "1".
Built-in variables, where .Count will output "1".
Case 2, 3, and 4 don't make any sense to me (yet). What is going on here? Where is this documented? How does the .Count property work?
Starting in PowerShell V3, the properties Count and Length received very special treatment not related to extended type data (also known as ETS or extended type system).
If the instance has a Count/Length property, then everything continues to work like it did in V1/V2 - the value from the instance is returned.
If the instance does not have a Count/Length property, starting in V3, instead of that being an error, you'll get back 1. And if the instance is $null, you'll get back 0. If you have turned on strict mode, you'll get an error like in V2.
I'll admit this is a bit strange, but it solves a common problem when a cmdlet returns 0, 1, or multiple objects.
Often, you'll iterate through those results via the pipeline or with a foreach statement. For example:
dir nosuchfile* | % { $_ }
foreach ($item in dir nosuchfile*) { $_ }
In the foreach case above, V2 would actually enter the loop if the command didn't return any values. That was changed in V3 to better match peoples expectations, but that also meant that:
foreach ($item in $null) { $_ }
also never enters the loop.
The for statement is another way to iterate through results, e.g.
$results = dir nosuchfile*
for ($i = 0; $i -lt $results.Count; $i++) { $results[$i] }
In this example, it doesn't matter how many objects are in $result, the loop works just fine. Note that in V1/V2, you would have written:
$results = #(dir nosuchfile*)
This ensures $results is an array, but this syntax is ugly and many folks would forget it, hence the change to support Count/Length in a slightly more forgiving way.
To complement Paul's answer, this might be related to extended type data. To quote the relevant part of the documentation:
Extended type data defines additional properties and methods
("members") of object types in Windows PowerShell. You can extend any
type that is supported by Windows PowerShell and use the added
properties and methods in the same way that you use the properties
that are defined on the object types.
And:
There are three sources of extended type data in Windows PowerShell
sessions.
The Types.ps1xml files in the Windows PowerShell installation directory are loaded automatically into every Windows PowerShell session.
If you open that Types.ps1xml file (in $pshome), you'll see this at the beginning of the file:
<Type>
<Name>System.Array</Name>
<Members>
<AliasProperty>
<Name>Count</Name>
<ReferencedMemberName>Length</ReferencedMemberName>
</AliasProperty>
</Members>
</Type>
So my guess is that by providing the ".Count" property, PowerShell assumes this is an array.
Here is how i think it works:
Case 1: In Arrays the .Count Property actually links to the .Length property which shows the number of Items in the Array
Case 2: Non-exitent variables get automatically created by powershell and initialized with value $null
Case 3 / 4: On this one i am not exactly sure why it happens but since neither String nor Int or boolean Objects have a .Count property i could imagine that the Property is inherited by a parent-object.
The behaviour suggests that the variable is treated as array so with 1 Value assigned the output will be 1, without a value the result will be 0.
Edit:
For the sake of completeness here is the Link to the Documentation: Technet, thanks #David Brabant
I have a foreach loop that currently puts three entries in my hashtable:
$result = foreach($key in $serverSpace.Keys){
if($serverSpace[$key] -lt 80){
[pscustomobject]#{
Server = $key
Space = $serverSpace[$key]}}}
When I use
$result.count
I get 3 as expected.
I changed the foreach loop to exlude the entries less than or equal to one using
$result = foreach($key in $serverSpace.Keys){
if($serverSpace[$key] -lt 80 -and $serverSpace[$key] -gt 1){
[pscustomobject]#{
Server = $key
Space = $serverSpace[$key]}}}
$result.count should have 1 as its output but it doesn't recognize .count as a suggested command and $result.count doesn't output anything anymore. I'm assuming when theres only one entry in the hash table it won't allow a count? Not sure whats going on but my conditions for my script are dependent on the count of $result. Any help would be appreciated.
$result is not a hashtable so I prefixed it with #($result).count. Thank you to #Theo and #Lee_Dailey
What you're seeing is a bug in Windows PowerShell (as of the latest and final version, 5.1), which has since been corrected in PowerShell (Core) - see GitHub issue #3671 for the original bug report.
That is, since v3 all objects should have an intrinsic .Count property , not just collections, in the interest of unified treatment of scalars and collections - see this answer for more information.
The workaround for Windows PowerShell is indeed to force a value to be an array via #(...), the array-subexpression operator, which is guaranteed to have a .Count property, as shown in your answer, but it shouldn't be necessary and indeed isn't anymore in PowerShell (Core, v6+)
# !! Due to a BUG, this outputs $null in *Windows PowerShell*,
# !! but correctly outputs 1 in PowerShell (Core).
([pscustomobject] #{}).Count
# Workaround for Windows PowerShell that is effective in *both* editions,
# though potentially wasteful in PowerShell (Core):
#([pscustomobject] #{}).Count
The script below finds folders older than 30 days. I just want to add a simple IF statement to say "There are not folders older than 30 days" if there are not any. But I am not sure how to do that
Thank you
$Test = Get-ChildItem "\\Server\XFER\Cory" -Directory |
Sort LastWriteTime -Descending |
Where-Object {($_.LastWriteTime -lt (Get-Date).AddDays(-30))} |
Select-Object Name, LastWriteTime
Your question boils down to this:
In PowerShell, how can I determine if a command or expression produced any output?
In almost all cases, the following is sufficient, as Ash suggests:
$Test = ... # ... represents any command or expression
if ($null -eq $Test) { 'no output' }
If you know that the command or expression - when it does produce output - only ever emits non-numeric and non-Boolean objects, you can simplify to the following, as Santiago Squarzon suggests, relying on PowerShell's implicit to-Boolean coercion logic, summarized in the bottom section of this answer:
$Test = ... # ... represents any command or expression
if (-not $Test) { 'no output' }
If you're dealing with an (unusual) command that outputs a collection (array) as a single object (as opposed to enumerating the collection and outputting each element separately, which is the normal pipeline behavior) and you want to treat an empty collection object as lack of output too:
$Test = ... # ... represents any command or expression
# Caveat: Due to a bug, only works robustly with
# Set-StrictMode turned off (the default)
# or Set-StrictMode -Version 1 (not higher).
if ($Test.Count -eq 0) { 'no output' }
Note that this works even with $null output and a scalar output object (a single, non-collection object). In the interest of unified treatment of scalars and collections (arrays), PowerShell adds a .Count property even to scalars that themselves do not have it, so that scalars can be treated as if they were a single-element collection, also with respect to indexing; e.g. (42).Count is 1, and (42)[0] is 42; however, note that $null.Count is 0. Such PowerShell engine-supplied type members are called intrinsic members.
Caveat: Due to a long-standing bug - reported in GitHub issue #2798 and still present as of PowerShell 7.2 - accessing the intrinsic .Count property on objects that don't natively have it causes a statement-terminating error if Set-StrictMode -Version 2 or higher is in effect.
However, the tests above do not allow you to distinguish between no output at all and a (single) $null value, which requires the following approach - though do note that actual $null output is unusual in PowerShell:[1]
$Test = ... # ... represents any command or expression
if ($null -eq $Test -and #($Test).Length -eq 0) { 'no output' }
This obscure test is necessary, because no output in PowerShell is represented by the [System.Management.Automation.Internal.AutomationNull]::Value] singleton, which behaves like $null in expression contexts, but not in enumeration contexts such as in a pipeline, including with #(...), the array-subexpression operator, which returns an empty array for [System.Management.Automation.Internal.AutomationNull]::Value] (element count 0) and a single-element array for $null.
While making the distinction between $null and [System.Management.Automation.Internal.AutomationNull]::Value often isn't necessary, there are definitely cases when it is, given that their enumeration behavior differs. Being able to distinguish via a simple test such as $Test -is [AutomationNull] is the subject of GitHub proposal #13465.
[1] Returning $null from PowerShell commands (cmdlets, scripts, functions) is best avoided; instead, simply omit output commands. However, .NET API methods may still return $null and object properties may contain $null (even [string]-typed ones, and even in PowerShell class definitions - see GitHub issue #7294).
I have created a script that loops through an array and excludes any variables that are found within a second array.
While the code works; it got me wondering if it could be simplified or piped.
$result = #()
$ItemArray = #("a","b","c","d")
$exclusionArray = #("b","c")
foreach ($Item in $ItemArray)
{
$matchFailover = $false
:gohere
foreach ($ExclusionItem in $exclusionArray)
{
if ($Item -eq $ExclusionItem)
{
Write-Host "Match: $Item = $ExclusionItem"
$matchFailover = $true
break :gohere
}
else{
Write-Host "No Match: $Item != $ExclusionItem"
}
}
if (!($matchFailover))
{
Write-Host "Adding $Item to results"
$result += $Item
}
}
Write-Host "`nResults are"
$result
To give your task a name: You're looking for the relative complement aka set difference between two arrays:
In set-theory notation, it would be $ItemArray \ $ExclusionArray, i.e., those elements in $ItemArray that aren't also in $ExclusionArray.
This related question is looking for the symmetric difference between two sets, i.e., the set of elements that are unique to either side - at last that's what the Compare-Object-based solutions there implement, but only under the assumption that each array has no duplicates.
EyIM's helpful answer is conceptually simple and concise.
A potential problem is performance: a lookup in the exclusion array must be performed for each element in the input array.
With small arrays, this likely won't matter in practice.
With larger arrays, LINQ offers a substantially faster solution:
Note: In order to benefit from the LINQ solution, your arrays should be in memory already, and the benefit is greater the larger the exclusion array is. If your input is streaming via the pipeline, the overhead from executing the pipeline may make attempts to optimize array processing pointless or even counterproductive, in which case sticking with the native PowerShell solution makes sense - see iRon's answer.
# Declare the arrays as [string[]]
# so that calling the LINQ method below works as-is.
# (You could also cast to [string[]] ad hoc.)
[string[]] $ItemArray = 'a','b','c','d'
[string[]] $exclusionArray = 'b','c'
# Return only those elements in $ItemArray that aren't also in $exclusionArray
# and convert the result (a lazy enumerable of type [IEnumerable[string]])
# back to an array to force its evaluation
# (If you directly enumerate the result in a pipeline, that step isn't needed.)
[string[]] [Linq.Enumerable]::Except($ItemArray, $exclusionArray) # -> 'a', 'd'
Note the need to use the LINQ types explicitly, via their static methods, because PowerShell, as of v7, has no support for extension methods.
However, there is a proposal on GitHub to add such support; this related proposal asks for improved support for calling generic methods.
See this answer for an overview of how to currently call LINQ methods from PowerShell.
Performance comparison:
Tip of the hat to iRon for his input.
The following benchmark code uses the Time-Command function to compare the two approaches, using arrays with roughly 4000 and 2000 elements, respectively, which - as in the question - differ by only 2 elements.
Note that in order to level the playing field, the .Where() array method (PSv4+) is used instead of the pipeline-based Where-Object cmdlet, as .Where() is faster with arrays already in memory.
Here are the results averaged over 10 runs; note the relative performance, as shown in the Factor columns; from a single-core Windows 10 VM running Windows PowerShell v5.1.:
Factor Secs (10-run avg.) Command TimeSpan
------ ------------------ ------- --------
1.00 0.046 # LINQ... 00:00:00.0455381
8.40 0.382 # Where ... -notContains... 00:00:00.3824038
The LINQ solution is substantially faster - by a factor of 8+ (though even the much slower solution only took about 0.4 seconds to run).
It seems that the performance gap is even wider in PowerShell Core, where I've seen a factor of around 19 with v7.0.0-preview.4.; interestingly, both tests ran faster individually than in Windows PowerShell.
Benchmark code:
# Script block to initialize the arrays.
# The filler arrays are randomized to eliminate caching effects in LINQ.
$init = {
$fillerArray = 1..1000 | Get-Random -Count 1000
[string[]] $ItemArray = $fillerArray + 'a' + $fillerArray + 'b' + $fillerArray + 'c' + $fillerArray + 'd'
[string[]] $exclusionArray = $fillerArray + 'b' + $fillerArray + 'c'
}
# Compare the average of 10 runs.
Time-Command -Count 10 { # LINQ
. $init
$result = [string[]] [Linq.Enumerable]::Except($ItemArray, $exclusionArray)
}, { # Where ... -notContains
. $init
$result = $ItemArray.Where({ $exclusionArray -notcontains $_ })
}
You can use Where-Object with -notcontains:
$ItemArray | Where-Object { $exclusionArray -notcontains $_ }
Output:
a, d
Advocating native PowerShell:
As per #mklement0's answer, with no doubt, Language Integrated Query (LINQ) is //Fast...
But in some circumstances, native PowerShell commands using the pipeline as suggested by #EylM can still beat LINQ. This is not just theoretical but might happen in used cases where the concerned process is idle and waiting for a slow input. E.g. where the input comes from:
A remote server (e.g. Active Directory)
A slow device
A separate thread that has to make a complex calculation
The internet ...
Despite I haven't seen an easy prove for this yet, this is suggested at several sites and can be deducted from sites as e.g. High Performance PowerShell with LINQ and Ins and Outs of the PowerShell Pipeline.
Prove
To prove the above thesis, I have created a small Slack cmdlet that slows down each item dropped into the pipeline with 1 millisecond (by default):
Function Slack-Object ($Delay = 1) {
process {
Start-Sleep -Milliseconds $Delay
Write-Output $_
}
}; Set-Alias Slack Slack-Object
Now let's see if native PowerShell can actually beat LINQ:
(To get a good performance comparison, caches should be cleared by e.g. starting a fresh PowerShell session.)
[string[]] $InputArray = 1..200
[string[]] $ExclusionArray = 100..300
(Measure-Command {
$Result = [Linq.Enumerable]::Except([string[]] ($InputArray | Slack), $ExclusionArray)
}).TotalMilliseconds
(Measure-Command {
$Result = $InputArray | Slack | Where-Object {$ExclusionArray -notcontains $_}
}).TotalMilliseconds
Results:
LINQ: 411,3721
PowerShell: 366,961
To exclude the LINQ cache, a single run test should be done but as commented by #mklement0, the results of single runs might vary each run.
The results also highly depend on the size of the input arrays, the size of the result, the slack, the test system, etc.
Conclusion:
PowerShell might still be faster than LINQ in some scenarios!
Quoting mklement0's comment:
"Overall, it's fair to say that the difference in performance is so small in this scenario that it's not worth picking the approach based on performance - and it makes sense to go with the more PowerShell-like approach (Where-Object), given that the LINQ approach is far from obvious. The bottom line is: choose LINQ only if you have large arrays that are already in memory. If the pipeline is involved, the pipeline overhead alone may make optimizations pointless."
I am using the TFS PowerTools Cmdlets in PowerShell to try to get at some information about Changesets and related WorkItems from my server. I have boiled the problem down to behavior I don't understand and I am hoping it is not TFS specific (so someone out there might be able to explain the problem to me :) )
Here's the only command that I can get to work:
Get-TfsItemHistory C:\myDir -recurse -stopafter 5 | % { Write-Host $_.WorkItems[0]["Title"] }
It does what I expect - Get-TfsItemHistory returns a list of 5 ChangeSets, and it pipes those to a foreach that prints out the Title of the first associated WorkItem. So what's my problem? I am trying to write a large script, and I prefer to code things to look more like a C# program (powershell syntax makes me cry). Whenever I try to do the above written any other way, the WorkItems collection is null.
The following commands (which I interpret to be logically equivalent) do not work (The WorkItems collection is null):
$items = Get-TfsItemHistory C:\myDir -recurse -stopafter 5
$items | ForEach-Object { Write-Host $_.WorkItems[0]["Title"] }
The one I would really prefer:
$items = Get-TfsItemHistory C:\myDir -recurse -stopafter 5
foreach ($item in $items)
{
$item.WorkItems[0]["Title"]
# do lots of other stuff
}
I read an article about the difference between the 'foreach' operator and the ForEach-Object Cmdlet, but that seems to be more of a performance debate. This really appears to be an issue about when the piping is being used.
I'm not sure why all three of these approaches don't work. Any insight is appreciated.
This is indeed confusing. For now a work-around is to grab the items like so:
$items = #(Get-TfsItemHistory . -r -Stopafter 25 |
Foreach {$_.WorkItems.Count > $null; $_})
This accesses the WorkItems collection which seems to cause this property to be populated (I know - WTF?). I tend to use #() to generate an array in cases where I want to use the foreach keyword. The thing with the foreach keyword is that it will iterate a scalar value including $null. So the if the query returns nothing, $items gets assigned $null and the foreach will iterate the loop once with $item set to null. Now PowerShell generally deals with nulls very nicely. However if you hand that value back to the .NET Framework, it usually isn't as forgiving. The #() will guarantee an array with with either 0, 1 or N elements in it. If it is 0 then the foreach loop will not execute its body at all.
BTW your last approach - foreach ($item in $items) { ... } - should work just fine.