I'm trying to get the result code from a running a file search in a powershell function. I'm using .NET for efficiency, and I don't want to compromise that. Now I am trying to obtain some kind of result code from the function (or command) but I don't how to do this.
I can save the command result in a variable, but if there are 10,000+ items, I'm not sure that is a great idea. Here's a POC using EnumerateFiles:
$RES=[IO.Directory]::EnumerateFiles($PWD, "*xxxx.dll", [IO.EnumerationOptions] @{AttributesToSkip='Device,Temporary,SparseFile,ReparsePoint,Compressed,Offline,Encrypted'; RecurseSubdirectories=$true; IgnoreInaccessible=$true})
$RES.Length
# Doesn't output anything! (Why is it not zero?)
$RES=[IO.Directory]::EnumerateFiles($PWD, "*.dll", [IO.EnumerationOptions] @{AttributesToSkip='Device,Temporary,SparseFile,ReparsePoint,Compressed,Offline,Encrypted'; RecurseSubdirectories=$true; IgnoreInaccessible=$true})
$RES.Length
# outputs the length of each path... NOT what I want.
Likewise using $?
always returns True
.
I was also thinking if it would be possible to use output redirection to measure the output somehow. Not sure how this would be done though.
How can I check the result success from a call like this?
(More specifically I would like to check if there are no files found.)
I'm trying to get the result code from a running a file search in a powershell function. I'm using .NET for efficiency, and I don't want to compromise that. Now I am trying to obtain some kind of result code from the function (or command) but I don't how to do this.
I can save the command result in a variable, but if there are 10,000+ items, I'm not sure that is a great idea. Here's a POC using EnumerateFiles:
$RES=[IO.Directory]::EnumerateFiles($PWD, "*xxxx.dll", [IO.EnumerationOptions] @{AttributesToSkip='Device,Temporary,SparseFile,ReparsePoint,Compressed,Offline,Encrypted'; RecurseSubdirectories=$true; IgnoreInaccessible=$true})
$RES.Length
# Doesn't output anything! (Why is it not zero?)
$RES=[IO.Directory]::EnumerateFiles($PWD, "*.dll", [IO.EnumerationOptions] @{AttributesToSkip='Device,Temporary,SparseFile,ReparsePoint,Compressed,Offline,Encrypted'; RecurseSubdirectories=$true; IgnoreInaccessible=$true})
$RES.Length
# outputs the length of each path... NOT what I want.
Likewise using $?
always returns True
.
I was also thinking if it would be possible to use output redirection to measure the output somehow. Not sure how this would be done though.
How can I check the result success from a call like this?
(More specifically I would like to check if there are no files found.)
4 Answers
Reset to default 3If you want to know if there are any items matching your filter you could use Enumerable.Any
:
function hasany {
[CmdletBinding()]
param([string] $Path = $PWD, [string] $Filter = '*')
$Path = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path)
$options = [IO.EnumerationOptions]@{
AttributesToSkip = 24384 # Same as Device, Temporary, ...
RecurseSubdirectories = $true
IgnoreInaccessible = $true
}
[System.Linq.Enumerable]::Any(
[System.IO.Directory]::EnumerateFiles($Path, $Filter, $options))
}
hasany -Filter *.dll
Similarly, if you want to know how many, you could use Enumerable.Count
. Both methods are very efficient, specially in .NET 9.
function howmany {
[CmdletBinding()]
param([string] $Path = $PWD, [string] $Filter = '*')
$Path = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path)
$options = [IO.EnumerationOptions]@{
AttributesToSkip = 24384 # Same as Device, Temporary, ...
RecurseSubdirectories = $true
IgnoreInaccessible = $true
}
[System.Linq.Enumerable]::Count(
[System.IO.Directory]::EnumerateFiles($Path, $Filter, $options))
}
howmany -Filter *.dll
Regarding the question in comments, how could you enable hasany *.dll
and hasany ./dir *.txt
, if you're using $args
instead of named parameters you could do something like this, however there is no validation on how many arguments you're passing, it will only use $args[0]
and $args[1]
in this case.
function hasany {
$options = [IO.EnumerationOptions]@{
AttributesToSkip = 24384 # Same as Device, Temporary, ...
RecurseSubdirectories = $true
IgnoreInaccessible = $true
}
$params = $args.Count -eq 2 ?
(Convert-Path $args[0]), $args[1], $options :
$pwd.Path, $args[0], $options
[System.Linq.Enumerable]::Any(
[System.IO.Directory]::EnumerateFiles.Invoke($params))
}
Update:
Santiago's helpful answer shows a superior approach to testing an enumerable for being empty, using
[Linq.Enumerable]::Any()
.The answer below may still be of interest for background information and for how
$?
can be set to$false
in the caller's scope.
To efficiently test if the enumeration produces at least one matching file without completing the enumeration and processing / capturing the enumerated objects:
[bool] (
[IO.Directory]::EnumerateFiles($PWD, "*xxxx.dll", [IO.EnumerationOptions] @{AttributesToSkip='Device,Temporary,SparseFile,ReparsePoint,Compressed,Offline,Encrypted'; RecurseSubdirectories=$true; IgnoreInaccessible=$true}) |
Select-Object -First 1
)
System.IO.Directory.EnumerateFiles
returns a lazy enumerable; specifically, an instance of a type that implementsSystem.Collections.Generic.IEnumerable[string]
.Sending such an enumerable through the pipeline implicitly triggers its enumeration, and
Select-Object
-First 1
exits the pipeline as soon as the first object - if any - is received, and outputs that object (which happens to be a[string]
instance in this case).Coercing the result to a Boolean (
[bool]
) yields$true
if the enumeration yielded (at least) one (by definition non-empty) output string, and$false
otherwise, using PowerShell's rules for to-Boolean conversion.
To additionally complete the enumeration and process / capture any enumerated objects:
# Store the *enumerable* in a variable - no enumeration
# is performed at this point.
$enumerable = [IO.Directory]::EnumerateFiles("$pwd", "*xxxx.dll", [IO.EnumerationOptions] @{AttributesToSkip='Device,Temporary,SparseFile,ReparsePoint,Compressed,Offline,Encrypted'; RecurseSubdirectories=$true; IgnoreInaccessible=$true})
# Update:
# Use -not [Linq.Enumerable]::Any($enumerable) instead.
# Obtain the *enumerator* instance to *manually* start the enumeration
# via `.MoveNext().
# If this method call returns $false, the enumeration is empty.
$isEmpty = -not $enumerable.GetEnumerator().MoveNext()
if ($isEmpty) {
# Report an error.
# Note that this will *not* set $? to $false in the caller's scope.
# See comments below.
Write-Error "Nothing to enumerate."
return
} else {
# Perform the enumeration in the pipeline, as needed.
# See comments below re .Reset() below.
$enumerable | ForEach-Object { "[$_]" }
}
Note:
Conceptually, it would make sense to call
$enumerable.GetEnumerator().Reset()
to reset the enumeration after having called.MoveNext()
on it for the sake of the emptiness test. However, the particular enumerator at hand does not support this method, presumably because it is a forward-only enumerator.
However, not being able to call.Reset()
appears not to interfere with the subsequent enumeration - it still starts with the first object.As of PowerShell 7.5.x, there is no direct way for PowerShell code to set the automatic
$_
variable for the caller's scope, although implementing this ability in a future version has been green-lit in principle in November 2019:- See GitHub issue #10917
Notably, use of
Write-Error
does not cause the caller to see$?
as$false
.
However, if your code is / can be implemented as an advanced function or script, you can use$PSCmdlet.WriteError()
or$PSCmdlet.ThrowTerminatingError()
to make the caller see$false
(which invariably also emits an error record, which surfaces as an error message unless suppressed / caught by the caller) - see this answer for details.
As for your specific questions:
$RES.Length
# Doesn't output anything! (Why is it not zero?)
# ...
# outputs the length of each path... NOT what I want.
The specific enumerable type returned by [IO.Directory]::EnumerateFiles()
does not have a .Length
property (nor a .Count
property).
As such, the attempt to access a .Length
property triggers member-access enumeration, meaning that a .Length
property is looked for on each object produced by the enumeration:
If the enumeration produces no objects,
$null
is therefore returned.If the enumeration produces one or more objects, the
.Length
property values of these objects are returned - which in the case at hand are the string lengths of the paths being enumerated.
My Final Solution
And many thanks to mklement0 and Santiago Squarzon. I couldn't have solved this on my own.
.Count
property like:if (@($RES).Count) { ... }
. A more clumsy way would be testing$RES | Measure-Object -Line).Lines
– Theo Commented Feb 16 at 10:21([array]$RES).Count
or([string[]]$RES).Count
would also give you the number of files found or 0 if there aren't any – Theo Commented Feb 16 at 10:29Enumerable.Any
is good in this case. AndEnumerable.Count
if you want to know how many. Both methods are very efficient. – Santiago Squarzon Commented Feb 16 at 16:23