最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

How to get the result code when using .NET in powershell script? - Stack Overflow

programmeradmin4浏览0评论

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.)

Share Improve this question edited Feb 16 at 22:41 mklement0 439k68 gold badges702 silver badges912 bronze badges asked Feb 16 at 0:25 not2qubitnot2qubit 17k9 gold badges116 silver badges159 bronze badges 3
  • You can always force the result into an array so you can test using its .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
  • Come to think of it: ([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:29
  • 1 Enumerable.Any is good in this case. And Enumerable.Count if you want to know how many. Both methods are very efficient. – Santiago Squarzon Commented Feb 16 at 16:23
Add a comment  | 

4 Answers 4

Reset to default 3

If 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 implements System.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.

发布评论

评论列表(0)

  1. 暂无评论