3

I'm trying to use PowerShell pipeline for some recurring tasks and checks, like perform certain checks X times or skip forward after the response in pipeline will have different state.

The simplest script I can write to do such checks is something like this:

do {
    $result=Update-ACMEIdentifier dns1 -ChallengeType dns-01
    if($result.Status -ne 'pending')
    { 
        "Hurray"
        break 
    }

    "Still pending"
    Start-Sleep -s 3
} while ($true)

The question is - how can I write this script as a single pipeline. It looks like the only thing I need is infinity pipeline to start with:

  1..Infinity |
    %{ Start-Sleep -Seconds 1 | Out-Null; $_ } |
    %{Update-ACMEIdentifier dns1 -ChallengeType dns-01 } |
    Select -ExpandProperty Status | ?{$_ -eq 'pending'} |
    #some code here to stop infinity producer or stop the pipeline

So is there any simple one-liner, which allows me to put infinity object producer on one side of the pipeline?

Good example of such object may be a tick generator that generates current timestamp into pipeline every 13 seconds

Matt
  • 45,022
  • 8
  • 78
  • 119
Alexey Shcherbak
  • 3,394
  • 2
  • 27
  • 44
  • Why does it have to be a pipeline statement? In your first example `} while ($result.Status -ne 'pending')` would run as long as you need it to without the need of a break. In your first example you are not running infinitely. You have an exit strategy based on a condition. – Matt Feb 19 '16 at 01:41
  • 3
    `1..Infinity` -> `&{for($i=1;;++$i){$i}}`; `#some code here to stop infinity producer or stop the pipeline` -> `Select-Object -First 1` – user4003407 Feb 19 '16 at 01:44
  • @matt well, because I want to write them as one liners, to fire and forget ( and get back to that console from time to time. My first example is not the perfect one, as it just shows the behaviour I want to convert from script-style to a pipeline expression. – Alexey Shcherbak Feb 19 '16 at 02:08
  • 1
    @PetSerAl Great, I haven't thought about it. Post it as a comment and I accept. In a bit reworked style it solves all my needs. `&{do{Start-Sleep -Seconds 1 ; Get-Date}while($true)} | Select-Object -First 5` . Hat tip to you. – Alexey Shcherbak Feb 19 '16 at 02:13
  • the comment above should say *as an answer – Alexey Shcherbak Feb 19 '16 at 02:19
  • 1
    FWIW, using `System.Management.Automation.ScriptBlock.GetSteppablePipeline()` you can create pipelines with different topologies than just a straight line. I have created a pipeline with a tee: one input and two outputs. In theory you could create a circular pipeline. A single object could circle around until you reach a stop condition. The `select -first 1` idiom wouldn't work, but you could use a special token that your function (that drives the pipeline) looks for, to signal a stop. – Χpẘ Feb 19 '16 at 18:49

2 Answers2

2

@PetSerAl gave the crucial pointer in a comment on the question: A script block containing an infinite loop, invoked with the call operator (&), creates an infinite source of objects that can be sent through a pipeline:

& { while ($true) { ... } }

A later pipeline segment can then stop the pipeline on demand.

Note:

  • As of PS v5, only Select-Object is capable of directly stopping a pipeline.

    • An imperfect generic pipeline-stopping function can be found in this answer of mine.
  • Using break to stop the pipeline is tricky, because it doesn't just stop the pipeline, but breaks out of any enclosing loop - safe use requires wrapping the pipeline in a dummy loop.

  • Alternatively, a Boolean variable can be used to terminate the infinite producer.

Here are examples demonstrating each approach:


A working example with Select-Object -First:

& { while ($true) { Get-Date; Start-Sleep 1 } } | Select-Object -First 5

This executes Get-Date every second indefinitely, but is stopped by Select-Object after 5 iterations.

An equivalent example with break and a dummy loop:

do { 
   & { while ($true) { Get-Date; Start-Sleep 1 } } |
     % { $i = 0 } { $_; if (++$i -eq 5) { break } }  # `break` stops the pipeline and
                                                     # breaks out of the dummy loop
} while ($false)

An equivalent example with a Boolean variable that terminates the infinite producer:

& { while (-not $done) { Get-Date; Start-Sleep 1 } } |
  % { $done = $false; $i = 0 } { $_; if (++$i -eq 5) { $done = $true } }

Note how even though $done is only initialized in the 2nd pipeline segment - namely in the ForEach-Object (%) cmdlet's (implicit) -Begin block - that initialization still happens before the 1st pipeline segment - the infinite producer - starts executing.Thanks again, @PetSerAl.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    It is not necessary to change `while () {}` to `do {} while ()`: `&{Write-Host second}|%{Write-Host first}{}`. – user4003407 Feb 19 '16 at 04:42
1

Not sure why you'd want to use a pipeline over a loop in this scenario, but it is possible by using a bit of C#; e.g.

$Source = @"
using System.Collections.Generic;
public static class Counter
{
    public static bool Running = false;
    public static IEnumerable<long> Run()
    {
        Running = true;
        while(Running)
        {
            for (long l = 0; l <= long.MaxValue; l++) 
            {
                yield return l;
                if (!Running) {
                    break;
                }
            }
        }
    }
}
"@

Add-Type -TypeDefinition $Source -Language CSharp

[Counter]::Run() | %{
    start-sleep -seconds 1
    $_
} | %{
    "Hello $_"
    if ($_ -eq 12) {
        [Counter]::Running = $false;
    }
}

NB: Because the numbers are generated in parallel with the pipeline execution it's possible that the generator may create a backlog of numbers before it's stopped. In my testing that didn't happen; but I believe that scenario is possible.

You'll also notice that I've stuck a for loop inside the while loop; that's to ensure that the values produced are valid; i.e. so I don't overrun the max value for the data type.


Update

Per @PetSerAl's comment above, here's an adapted version in pure PowerShell:

$run=$true; &{for($i=0;$run;$i++){$i}} | %{ #infinite loop outputting to pipeline demo
    "hello $_";
        if($_ -eq 10){"stop";$run=$false <# stopping condition demo #>}
}
JohnLBevan
  • 22,735
  • 13
  • 96
  • 178
  • Well, it's not an one-liner really. – Alexey Shcherbak Feb 19 '16 at 02:16
  • @AlexeyShcherbak _Anything_ can be a one liner if you use semicolons. What does is matter? +1 – Matt Feb 19 '16 at 02:39
  • @Matt: Perhaps what AlexeyShcherbak means: it's not a single _statement_ (pipeline). – mklement0 Feb 19 '16 at 02:53
  • 1
    My question is not about how to DO such thing at all, but more about - how to do this **in pipeline** and in elegant way. I may not expressed this very explicitly in the first line of the question, but I mentioned it 2 times. Because the purpose of such statements is one-off monitoring and checking - I don't want to write more code than actually needed. @PetSerAl 's comment is much better answer for me than proposed .NET class and jumping forth and back between CLR and Powershell for the sake of such simple thing as infinity loop. – Alexey Shcherbak Feb 19 '16 at 03:08
  • 1
    If @PetSerAl won't add his comment as an answer - I'll accept this answer. But I still think that his answer is exactly pinpoints and solves described problem, rather than JohnLBevan's one. – Alexey Shcherbak Feb 19 '16 at 03:13
  • @AlexeyShcherbak PetSerAl will usually answer within a day or so if someone else has not expanded on his comment. – Matt Feb 19 '16 at 03:53
  • @mklement0 I had assumed as much. I just have a pet peeve for the phrase "one liner" – Matt Feb 19 '16 at 03:53
  • @Matt I'll wait till Monday then. Well principles and pet peeves are good, I also have one - don't over-complicate solutions. When someone asks for an apple - maybe he needs exactly an apple, not a bag of apple seeds, shovel and instruction how to grow an apple garden in 21 days – Alexey Shcherbak Feb 19 '16 at 03:58