Reputation: 122172
I would like to find all directories at the top level from the location of the script that are stored in subversion.
In C# it would be something like this
Directory.GetDirectories(".")
.Where(d=>Directories.GetDirectories(d)
.Any(x => x == "_svn" || ".svn"));
I'm having a bit of difficulty finding the equivalent of "Any()" in PowerShell, and I don't want to go through the awkwardness of calling the extension method.
So far I've got this:
Get-ChildItem | ? {$_.PsIsContainer} | Get-ChildItem -force | ? {$_.PsIsContainer -and $_.Name -eq "_svn" -or $_.Name -eq ".svn"
This finds me the svn
directories themselves, but not their parent directories - which is what I want. Bonus points if you can tell me why adding
| Select-Object {$_.Directory}
to the end of that command list simply displays a sequence of blank lines.
Upvotes: 67
Views: 38156
Reputation: 60518
An implementation of mklement0's helpful answer turned into a cmdlet, it should be compatible with Windows PowerShell 5.1 and PowerShell 7+ (probably won't work in previous versions).
The main advantage here is that the StopUpstreamCommandsException
is stored in a static
field, meaning, we only need to get it once per session. This approach is also significantly faster due to the use of a SteppablePipeline
to process input objects.
You can either compile it ahead of time or if you want to compile it ad-hoc, put the code in a file, i.e.: TestAny.cs
then you can do:
Add-Type (Get-Content path\to\TestAny.cs -Raw) -PassThru |
Import-Module -Assembly { $_.Assembly }
And the usage would be:
0..1mb | Test-Any { $_ -gt 500 }
The -UseLocalScope
means that the scriptblock will be dot-sourced, something similar to how ForEach-Object
and Where-Object
, however do note there is a performance penalty using this method.
0..1mb | Test-Any { $i = $_; $_ -gt 500 } -UseLocalScope
$i # Outputs: 501
Lastly a performance comparison between this cmdlet and the PowerShell function counterpart:
using System;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
[Cmdlet(VerbsDiagnostic.Test, "Any")]
public sealed class TestAnyCommand : PSCmdlet, IDisposable
{
private const string _exceptionType =
"System.Management.Automation.StopUpstreamCommandsException";
private SteppablePipeline _pipe;
private static Exception s_exception;
private Exception StopUpstreamException
{
get
{
return s_exception ?? (s_exception = GetException());
}
}
[Parameter(ValueFromPipeline = true)]
public PSObject InputObject { get; set; }
[Parameter(Mandatory = true, Position = 0)]
public ScriptBlock Filter { get; set; }
[Parameter]
public SwitchParameter UseLocalScope { get; set; }
protected override void BeginProcessing()
{
_pipe = ScriptBlock
.Create("param($__sb) " + (UseLocalScope ? "." : "&") + " $__sb")
.GetSteppablePipeline(
CommandOrigin.Internal,
new[] { ConvertToProcessBlockIfUnnamed(Filter) });
_pipe.Begin(true);
}
protected override void ProcessRecord()
{
if (LanguagePrimitives.ConvertTo<bool>(_pipe.Process(InputObject)))
{
WriteObject(true);
throw StopUpstreamException;
}
}
protected override void EndProcessing()
{
WriteObject(false);
_pipe.End();
}
private static ScriptBlock ConvertToProcessBlockIfUnnamed(ScriptBlock scriptBlock)
{
ScriptBlockAst sbAst = (ScriptBlockAst)scriptBlock.Ast;
if (sbAst.BeginBlock != null || sbAst.ProcessBlock != null)
{
return scriptBlock;
}
ScriptBlockAst newSbAst = new ScriptBlockAst(
scriptBlock.Ast.Extent,
paramBlock: null,
beginBlock: null,
processBlock: new NamedBlockAst(
sbAst.EndBlock.Extent,
TokenKind.Process,
new StatementBlockAst(
sbAst.EndBlock.Extent,
sbAst.EndBlock.Statements.Select(s => s.Copy()).Cast<StatementAst>(),
null),
unnamed: false),
endBlock: null,
dynamicParamBlock: null);
return newSbAst.GetScriptBlock();
}
private Exception GetException()
{
return (Exception)Activator.CreateInstance(
typeof(Cmdlet).Assembly.GetType(_exceptionType), this);
}
public void Dispose()
{
_pipe.Dispose();
GC.SuppressFinalize(this);
}
}
Upvotes: 1
Reputation: 439193
To answer the immediate question with a PowerShell v3+ solution:
(Get-ChildItem -Force -Directory -Recurse -Depth 2 -Include '_svn', '.svn').Parent.FullName
-Directory
limits the matches to directories, -Recurse -Depth 2
recurses up to three levels (children, grandchildren, and great-grandchildren), Include
allows specifying multiple (filename-component) filters, and .Parent.FullName
returns the full path of the parent dirs. of the matching dirs., using member-access enumeration (implicitly accessing a collection's elements' properties).
As for the bonus question: select-object {$_.Directory}
does not work,
because the [System.IO.DirectoryInfo]
instances returned by Get-ChildItem
have no .Directory
property, only a .Parent
property; Select-Object -ExpandProperty Parent
should have been used.
In addition to only returning the property value of interest, -ExpandProperty
also enforces the existence of the property. By contrast, Select-Object {$_.Directory}
returns a custom object with a property literally named $_.Directory
, whose value is $null
, given that the input objects have no .Directory
property; these $null
values print as empty lines in the console.
As for the more general question about a PowerShell equivalent to LINQ's .Any()
method, which indicates [with a Boolean result] whether a given enumerable (collection) has any elements at all / any elements satisfying a given condition:
Natively, PowerShell offers no such equivalent, but the behavior can be emulated:
.Where()
method:Caveat: This requires collecting the entire input collection in memory first, which can be problematic with large collections and/or long-running input commands.
(...).Where({ $_ ... }, 'First').Count -gt 0
...
represents the command of interest, and $_ ...
the condition of interest, applied to each input object, where PowerShell's automatic $_
variable refers to the input object at hand; argument 'First'
ensures that the method returns once the first match has been found.
For example:
# See if there's at least one value > 1
PS> (1, 2, 3).Where({ $_ -gt 1 }, 'First').Count -gt 0
True
The advantage of a pipeline-based solution is that it can act on a command's output one by one, as it is being produced, without needing to collect the entire output in memory first.
If you don't mind that all objects are enumerated - even if you only care if there is at least one - use Paolo Tedesco's helpful extension to JaredPar's helpful answer. The down-side of this approach is that you always have to wait for a (potentially long-running) command to finish producing all output objects, even though - logically - the determination whether there are any output objects can be made as soon as the first object is received.
If you want to exit the pipeline as soon as one [matching] object has been encountered, you have two options:
[Ad-hoc: Easy to understand, but cumbersome to implement]
Enclose the pipeline in a dummy loop and use break
to break out of the pipeline and that loop (...
represents the command whose output to test, and $_ ...
match the condition):
# Exit on first input object.
[bool] $haveAny = do { ... | % { $true; break } } while ($false)
# Exit on first input object that matches a condition.
[bool] $haveAny = do { ... | % { if ($_ ...) { $true ; break } } } while ($false)
[Use a PowerShell v3+ self-contained utility function that is nontrivial to implement]
See the implementation of function Test-Any
below.
It can be added to scripts or, for use in interactive sessions, to your $PROFILE
file.
Test-Any
The function is nontrivial, because as of PowerShell (Core) v7.2.x, there is no direct way to exit a pipeline prematurely, so a workaround based on .NET reflection and a private type is currently necessary.
If you agree that there should be such a feature, take part in the conversation in GitHub issue #3821.
#requires -version 3
Function Test-Any {
[CmdletBinding()]
param(
[ScriptBlock] $Filter,
[Parameter(ValueFromPipeline = $true)] $InputObject
)
process {
if (-not $Filter -or (Foreach-Object $Filter -InputObject $InputObject)) {
$true # Signal that at least 1 [matching] object was found
# Now that we have our result, stop the upstream commands in the
# pipeline so that they don't create more, no-longer-needed input.
(Add-Type -Passthru -TypeDefinition '
using System.Management.Automation;
namespace net.same2u.PowerShell {
public static class CustomPipelineStopper {
public static void Stop(Cmdlet cmdlet) {
throw (System.Exception) System.Activator.CreateInstance(typeof(Cmdlet).Assembly.GetType("System.Management.Automation.StopUpstreamCommandsException"), cmdlet);
}
}
}')::Stop($PSCmdlet)
}
}
end { $false }
}
if (-not $Filter -or (Foreach-Object $Filter -InputObject $InputObject))
defaults to true if $Filter
wasn't specified, and otherwise evaluates the filter (script block) with the object at hand.
The (Add-Type ...
statement uses an ad-hoc type created with C# code that uses reflection to throw the same exception that Select-Object -First
(PowerShell v3+) uses internally to stop the pipeline, namely [System.Management.Automation.StopUpstreamCommandsException]
, which as of PowerShell v5 is still a private type.
Background here:
http://powershell.com/cs/blogs/tobias/archive/2010/01/01/cancelling-a-pipeline.aspx
A big thank-you to PetSerAl for contributing this code in the comments.
Examples:
PS> @() | Test-Any false
PS> Get-EventLog Application | Test-Any # should return *right away* true
PS> 1, 2, 3 | Test-Any { $_ -gt 1 } # see if any object is > 1 true
JaredPar's helpful answer and Paolo Tedesco's helpful extension fall short in one respect: they don't exit the pipeline once a match has been found, which can be an important optimization.
Sadly, even as of PowerShell v5, there is no direct way to exit a pipeline prematurely. If you agree that there should be such a feature, take part in the conversation in GitHub issue #3821.
A naïve optimization of JaredPar's answer actually shortens the code:
# IMPORTANT: ONLY EVER USE THIS INSIDE A PURPOSE-BUILT DUMMY LOOP (see below)
function Test-Any() { process { $true; break } end { $false } }
The process
block is only entered if there's at least one element in the pipeline.
process
block is still entered, with $_
set to $null
, so calling Test-Any
outside of a pipeline unhelpfully returns $true
. To distinguish between between $null | Test-Any
and Test-Any
, check $MyInvocation.ExpectingInput
, which is $true
only in a pipeline: Thanks, PetSerAl
function Test-Any() { process { $MyInvocation.ExpectingInput; break } end { $false } }
$true
, written to the output stream, signals that at least one object was found.
break
then terminates the pipeline and thus prevents superfluous processing of additional objects. HOWEVER, IT ALSO EXITS ANY ENCLOSING LOOP - break
is NOT designed to exit a PIPELINEThanks, PetSerAl
.
return
would simply move on to the next input object.Since the process
block unconditionally executes break
, the end
block is only reached if the process
block was never entered, which implies an empty pipeline, so $false
is written to the output stream to signal that.
Upvotes: 60
Reputation: 755121
Unfortunately there is no equivalent in PowerShell. I wrote a blog post about this with a suggestion for a general purpose Test-Any function / filter.
function Test-Any() {
begin {
$any = $false
}
process {
$any = $true
}
end {
$any
}
}
Blog post: Is there anything in that pipeline?
Upvotes: 26
Reputation: 177
I took a more linq-style approach.
I know this question is probably super old. I used this to accomplish my needs:
PS> $searchData = "unn"
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp")
PS> $delegate = [Func[string,bool]]{ param($d); return $d -eq $searchData }
PS> [Linq.Enumerable]::Any([string[]]$StringData, $delegate)
Taken from here:
Upvotes: 1
Reputation: 1130
You can use the original LINQ Any
:
[Linq.Enumerable]::Any($list)
Upvotes: 12
Reputation: 11340
I think that the best answer here is the function proposed by @JaredPar, but if you like one-liners as I do I'd like to propose following Any
one-liner:
# Any item is greater than 5
$result = $arr | %{ $match = $false }{ $match = $match -or $_ -gt 5 }{ $match }
%{ $match = $false }{ $match = $match -or YOUR_CONDITION }{ $match }
checks that at least one item match condition.
One note - usually the Any operation evaluates the array until it finds the first item matching the condition. But this code evaluates all items.
Just to mention, you can easily adjust it to become All
one-liner:
# All items are greater than zero
$result = $arr | %{ $match = $false }{ $match = $match -and $_ -gt 0 }{ $match }
%{ $match = $false }{ $match = $match -and YOUR_CONDITION }{ $match }
checks that all items match condition.
Notice, that to check Any you need -or
and to check All you need -and
.
Upvotes: 1
Reputation: 11
This is the best method that I found so far (does not iterate over all elements if already found a true, and does not break the pipeline):
From LINQ Any() equivalent in PowerShell
It’s possible to use a built-in $input variable that contains the whole pipeline in a scope of function.
So, the desired code could look like the following:
function Test-Any([scriptBlock] $scriptBlock = {$true}, [scriptBlock] $debugOut = $null)
{
if ($debugOut)
{
Write-Host(“{0} | % {{{1}}}” -f $input, $scriptBlock)
}
$_ret = $false;
$_input = ($input -as [Collections.IEnumerator])
if ($_input)
{
while ($_input.MoveNext())
{
$_ = $_input.Current;
Write-Host $_
if ($debugOut)
{
Write-Host(“Tested: [{0}]” -f (&$debugOut))
}
if (&$scriptBlock)
{
if ($debugOut)
{
Write-Host(“Matched: [{0}]” -f (&$debugOut))
}
$_ret = $true
break
}
}
}
$_ret
}
Upvotes: 1
Reputation: 122172
I ended up doing it with a count:
$directoryContainsSvn = {
(Get-ChildItem $_.Name -force | ? {$_.PsIsContainer -and $_.Name -eq "_svn" -or $_.Name -eq ".svn"} | Measure-Object).Count -eq 1
}
$svnDirs = Get-ChildItem | ? {$_.PsIsContainer} | ? $directoryContainsSvn
Upvotes: 3
Reputation: 2569
It's actually quite simple - just select first $true (formatted for clarity):
[bool] ($source `
| foreach { [bool] (<predicate>) } `
| where { $_ } `
| select -first 1)
Alternative way:
($source `
| where { <predicate> } `
| foreach { $true } `
| select -first 1)
Upvotes: 6
Reputation: 7773
I recommend the following solution:
<#
.SYNOPSIS
Tests if any object in an array matches the expression
.EXAMPLE
@( "red", "blue" ) | Where-Any { $_ -eq "blue" } | Write-Host
#>
function Where-Any
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $True)]
$Condition,
[Parameter(Mandatory = $True, ValueFromPipeline = $True)]
$Item
)
begin {
[bool]$isMatch = $False
}
process {
if (& $Condition $Item) {
[bool]$isMatch = $true
}
}
end {
Write-Output $isMatch
}
}
# optional alias
New-Alias any Where-Any
Upvotes: 2
Reputation: 57222
A variation on @JaredPar's answer, to incorporate the test in the Test-Any
filter:
function Test-Any {
[CmdletBinding()]
param($EvaluateCondition,
[Parameter(ValueFromPipeline = $true)] $ObjectToTest)
begin {
$any = $false
}
process {
if (-not $any -and (& $EvaluateCondition $ObjectToTest)) {
$any = $true
}
}
end {
$any
}
}
Now I can write "any" tests like
> 1..4 | Test-Any { $_ -gt 3 }
True
> 1..4 | Test-Any { $_ -gt 5 }
False
Upvotes: 19
Reputation: 201822
You can tighten this up a bit:
gci -fo | ?{$_.PSIsContainer -and `
(gci $_ -r -fo | ?{$_.PSIsContainer -and $_ -match '[_.]svn$'})}
Note - passing $__.Name to the nested gci is unnecessary. Passing it $_ is sufficent.
Upvotes: 2
Reputation: 354694
My approach now was:
gci -r -force `
| ? { $_.PSIsContainer -and $_.Name -match "^[._]svn$" } `
| select Parent -Unique
The reason why
select-object {$_.Directory}
doesn't return anything useful is that there is no such property on a DirectoryInfo
object. At least not in my PowerShell.
To elaborate on your own answer: PowerShell can treat most non-empty collections as $true
, so you can simply do:
$svnDirs = gci `
| ? {$_.PsIsContainer} `
| ? {
gci $_.Name -Force `
| ? {$_.PSIsContainer -and ($_.Name -eq "_svn" -or $_.Name -eq ".svn") }
}
Upvotes: 5