8

Is there a simple way to ensure that all values returned are True? In the example below, I have a collection of objects which represent file. I want to make sure that all the sources file exist before proceeding. I pass all the paths to Test-Path and the function returns True/False for each file.

    > $filesToUpdate = @(
      [PsCustomObject] @{ Source = (Join-Path $basePath "Abc.module"); }
      [PsCustomObject] @{ Source = (Join-Path $basePath "Def.module"); }
    )

    > $filesToUpdate.Source | Test-Path

    True
    True

    > # How can I check if all the returned values are true?

How do I check all the returned values are True?

Martin
  • 39,309
  • 62
  • 192
  • 278

4 Answers4

6

To complement FoxDeploy's helpful answer with the wider perspective of pipeline use:

tl;dr:

  • (Test-Path $filesToUpdate.Source) -contains $false:

    • Fast, but unsuitable for very large collections, because the input collection must fit into memory as a whole (which it does in the context of this question).

    • Test-Path accepts an array as input and outputs a parallel array reflecting each input item's existence; -contains tests membership of the RHS scalar in the LHS array.

    • This solution is conceptually simplest and fastest (compared to unoptimized pipeline use).
  • $false -ne ($filesToUpdate.Source | Test-Path | ? { -not $_ } | Select-Object -First 1)

    • This approach is a must if the input collection is too large to fit into memory as a whole / memory use must be kept constant.

      • Note: Here, pipeline input $filesToUpdate.Source is by definition an in-memory collection, but the scenario discussed applies to commands that produce a large number of output objects one by one and send them to the pipeline a such.
    • ? { -not $_ } (short for: Where-Object { -not $_ }) filters the Booleans output by Test-Path to only contain the $false values, if any. Thus, the filter will only produce output if at least one $false value is present.

    • Select-Object -First 1 optimizes processing [PSv3+] by exiting the pipeline once the first object is received (the first $false value), if any, which means that the output is either a single $false value, or no output at all.

      • The performance impact of this optimization depends entirely on the input data and can range from dramatic (the first item in a large input collection maps to $false) to none (no items map to $false).
    • $false -ne ... then tests the pipeline's output for not being $false, which implies that Test-Path returned $true for all input paths.

      • Note that explicit comparison with $false must be used, because -not (...) / ! (...) would not work as intended, because negating a command that produces no output also yields $true.

Generally, pipelines are a powerful concept integral to PowerShell and, even though they introduce processing overhead, they are worth using for their conceptual elegance, unless they present a performance problem.

  • If performance is paramount, pipelines can be worked around, which can be cumbersome, however.

  • Conversely, PowerShell's flexible operators sometimes offer solutions that are both conceptually elegant and fast, as is the case here, though potentially at the expense of memory consumption.
    The only reason to use a pipeline in such scenarios would be to deal with large input collections.

To give you a sense of relative performance, here are test timings comparing the solutions, parameterized by the size of the input collection and how many runs to average.

Find the test script at the bottom.

Note that the input data is constructed to place the first (and only) nonexistent path in the middle of the input collection.

This choice dramatically affects the performance of the Select-Object -First 1 solution: if you instead place a nonexistent path at the beginning, it will perform best, if you place it at the end or do not include one at all, there will be no performance again (on the contrary).


Sample numbers from my machine (late-2012 iMac), in seconds:

> .\Test.ps1 -count 10 -repeat 10  # 10 input items, timing averaged over 10 runs

Command                                           10-run average
-------                                           --------------
-contains, no pipeline                            .00124
-contains, pipeline                               .00170
pipeline, where-object, select -first 1           .00276
pipeline, where-object                            .00314
pipeline, where-object, Test-Path in script block .00460

> .\Test.ps1 -count 100 -repeat 10

Command                                           10-run average
-------                                           --------------
-contains, no pipeline                            .01190
pipeline, where-object, select -first 1           .01328
-contains, pipeline                               .01836
pipeline, where-object                            .02365
pipeline, where-object, Test-Path in script block .03725

> .\Test.ps1 -count 1000 -repeat 10

Command                                           10-run average
-------                                           --------------
pipeline, where-object, select -first 1           .11154
-contains, no pipeline                            .11764
-contains, pipeline                               .16508
pipeline, where-object                            .22246
pipeline, where-object, Test-Path in script block .37015

> .\Test.ps1 -count 10000 -repeat 10

Command                                           10-run average
-------                                           --------------
pipeline, where-object, select -first 1           1.09919
-contains, no pipeline                            1.15089
-contains, pipeline                               1.75926
pipeline, where-object                            2.21868
pipeline, where-object, Test-Path in script block 3.65946

Test.ps1

param(
  [int] $count = 50
  ,
  [int] $repeat = 10
)

# Create sample input array.
$paths = @('c:') * $count 
$paths[$paths.Count / 2] = 'nosuch'


$timingPropName = "$repeat-run average"

@(
  [pscustomobject] @{ Command = "-contains, no pipeline"; $timingPropName = 
    (1..$($repeat) | % { (Measure-Command { (Test-Path $paths) -contains $false }).TotalSeconds } | 
      Measure-Object -average | % Average) }
  [pscustomobject] @{ Command = "-contains, pipeline"; $timingPropName = 
    (1..$($repeat) | % { (Measure-Command { ($paths | Test-Path) -contains $false }).TotalSeconds } | 
      Measure-Object -average | % Average) }
  [pscustomobject] @{ Command = "pipeline, where-object, select -first 1"; $timingPropName = 
    ( 1..$($repeat) | % { (Measure-Command { $paths | Test-Path | ? { $_ -eq $false } | Select-Object -First 1 }).TotalSeconds } | 
      Measure-Object -average | % Average) }
  [pscustomobject] @{ Command = "pipeline, where-object"; $timingPropName = 
    (1..$($repeat) | % { (Measure-Command { $paths | Test-Path | ? { $_ -eq $false } }).TotalSeconds } | 
      Measure-Object -average | % Average) }
  [pscustomobject] @{ Command = "pipeline, where-object, Test-Path in script block"; $timingPropName = 
    (1..$($repeat) | % { (Measure-Command { $paths | ? { !(Test-Path $_) } }).TotalSeconds } | 
      Measure-Object -average | % Average) }
) | 
  Sort-Object $timingPropName | 
    Format-Table Command, @{ n=$timingPropName; e={ '{0:.00000}' -f $_.$timingPropName } }
mklement0
  • 382,024
  • 64
  • 607
  • 775
5
if (($filesToUpdate.Source | Test-Path) -contains $false){
#We know that one of the attempts was a failure
}
else{
#proceed
}
FoxDeploy
  • 12,569
  • 2
  • 33
  • 48
  • 3
    Perfect! You could also use `-notcontains` depending on how you'd like to organize your conditional (for example if you have nothing to execute unless all are true). – briantist Jul 11 '16 at 18:18
  • 2
    I'm a code pessimist, I also assume failure first and then move on to my success conditions. Sometimes I forget to even write a success condition. – FoxDeploy Jul 11 '16 at 18:19
  • Nicely done; you can simplify it somewhat, which also speeds it up: `if ((Test-Path $filesToUpdate.Source) -contains $false)` – mklement0 Jul 11 '16 at 18:41
  • 2
    You don't _need_ the pipe here. `(Test-Path $filesToUpdate.Source) -contains $false`. Path accepts arrays for input. – Matt Jul 11 '16 at 19:31
0

PowerShell does not directly have any LINQ-like equivalent (though there are some third-party modules that give you these functions).

In general though, you can simply filter for the items. The following expression would return a true-ish value if there are any $false values inside:

$filesToUpdate.Source | Test-Path | ? { $_ -ne $true }

So in order to get “true” when all paths exist, you could invert it:

!($filesToUpdate.Source | Test-Path | ? { $_ -ne $true })

Alternatively, in your specific situation, you can also combine your list directly with the Test-Path:

$filesToUpdate.Source | ? { !(Test-Path $_) }

This gives you all files that do not exist; which ends up being a trueish value (which you can use in conditions) when there are any files that don’t exist (i.e. when not all exist).

poke
  • 369,085
  • 72
  • 557
  • 602
  • PowerShell's implicit to-Boolean conversions are [quite tricky](https://stackoverflow.com/a/53108138/45375), unfortunately. All your commands return the wrong result if the inputs happens to contain just _one_ `$false` value. E.g., `!($true, $true, $true) | ? { $_ -ne $true }` and `!($true, $true, $false) | ? { $_ -ne $true }` _both_ return `$false`, even though the 2nd input collection clearly contains a `$false` value. `! (& {})` (a command with no output), `! $false`, and `! @($false)` all return `$true` in PowerShell - only 2+-element collections are _always_ "truthy". – mklement0 Aug 12 '19 at 16:01
0

Here's another way, with an "and" function that performs "-and" on all the elements of the array.

# foldright or reduce
function foldr ($sb, $accum, $list) {
  if ($list.count -eq 0) { $accum }
  else { & $sb $list[0] (foldr $sb $accum $list[1..$list.length]) }
}

function and ($list) {
  foldr { $args[0] -and $args[1] } $true $list
}

$filestoupdate = [pscustomobject]@{source = $true,$true}
and $filestoupdate.source


True
js2010
  • 23,033
  • 6
  • 64
  • 66