Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 941
not2qubit Avatar asked Sep 18 '26 22:09

not2qubit


2 Answers

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))
}
like image 100
Santiago Squarzon Avatar answered Sep 21 '26 00:09

Santiago Squarzon


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.

like image 20
mklement0 Avatar answered Sep 21 '26 01:09

mklement0