while-loopuntil-loopnushell

while/until loops in Nushell


Author's note: This question uses outdated syntax and statements from an older version of Nushell. Please see the answer(s) below for the updated syntax.

How do you do while/until loops in Nushell script?

Since Nushell has a fairly amazing table/JSON parsing system, I've been trying to work with the Stack Exchange API through it.

One of the first challenges is looping over the multiple possible pages of results from an API call. My (normally procedural, sometimes OOP) background had me reaching for a construct in Nushell like:

let page = 1
let re = (http (echo "/2.3/questions?fromdate=1648771200&todate=1648944000&order=desc&sort=activity&site=askubuntu&page=" $page) | from json)
let questions = $re.items

while ($re.has_more) {
    let page = page + 1
    let re = (http (echo "/2.3/questions?fromdate=1648771200&todate=1648944000&order=desc&sort=activity&site=askubuntu&page=" $page) | from json)
    let questions = $questions | append $re.items
}

... or the equivalent until construct.

How would I accomplish this in Nushell?

Note - Using httpie in the above example since it automagically handles the gzip compression that the Stack API requires (unlike wget or Nushell's internal fetch command).


Solution

  • Short answer:

    As of Nushell 0.72, while loops are now supported, along with mutable variables to assist in the handling of such loops. until-style loops can also be replicated through loop/for with the break statement.

    Note: I'm updating this answer to the latest syntax based on the currently available Nushell 0.84. Keep in mind that Nushell continues to evolve its syntax on its path to 1.0.

    The example in the question can now be handled fairly easily (as concisely and cleanly as my original question's pseudo-code, at least) with:

    mut questions = []
    for page in 1.. {
      print $"Retrieving page ($page)"
      let res = ( http get $"https://api.stackexchange.com/2.3/questions?fromdate=1648771200&todate=1648944000&order=asc&sort=creation&site=askubuntu&pagesize=100&page=($page)" )
      $questions = ( $questions | append $res.items )
    
      if not $res.has_more { break } # "until" condition
    }
    

    This works by:

    With that in place, you can now access $questions to slice-and-dice the results as needed:

    $questions | where view_count > 100 and view_count < 110 | select view_count title link
    

    Result (at the time of the original answer):

    ╭───┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────╮
    │ # │ view_count │                                          title                                           │                                                 link                                                 │
    ├───┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┤
    │ 0 │        103 │ Find reason for &quot;apache2.service: Failed with result &#39;exit-code&#39;.&quot; and │ https://askubuntu.com/questions/1400332/find-reason-for-apache2-service-failed-with-result-exit-code │
    │   │            │ &quot;Failed to start The Apache HTTP Server.&quot;                                      │ -and-failed-t                                                                                        │
    │ 1 │        103 │ Public folder is forbidden in nginx                                                      │ https://askubuntu.com/questions/1400333/public-folder-is-forbidden-in-nginx                          │
    │ 2 │        101 │ WSL Nano scrolling up to see terminal                                                    │ https://askubuntu.com/questions/1400431/wsl-nano-scrolling-up-to-see-terminal                        │
    ╰───┴────────────┴──────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────╯
    
    

    And yes, Nushell actually pretty-prints the results table.

    Of course, while loops are also supported quite cleanly using mutable variables. And a generic loop/break construct is until short-hand for for x in 1.. when you don't need an incrementing variable.

    From the Nushell help:


    Older info:

    However, my original, pre-0.72 answer, using recursive functions, is still a valid way of handling this (and may a useful technique), but do keep in mind that Nushell does not have tail-recursion.

    Using recursion, a basic "while" loop in Nushell might look something like:

    def wloop [] {
        let re = (random bool)
        if ($re) { 
            print $re
            wloop
        }
    }
    $ wloop
    $ wloop
    $ wloop
    true
    $ wloop
    true
    true
    true
    

    And a corresponding until-loop might look like:

    def uloop [] {
        let re = (random bool)
        print $re
        if ($re) { uloop }
    }
    $ uloop
    false
    $ uloop
    false
    $ uloop
    true
    false
    

    If you need to modify a variable, keep in mind that it is scoped to its block, so you'll need to pass it back in to the recursive function. For instance, to work with the Stack Exchange API and update the page number for each call:

    $ let baseUri = "https://api.stackexchange.com/2.3/questions?fromdate=1648771200&todate=1648944000&order=asc&sort=creation&site=askubuntu&pagesize=100"
    $ def getAskUbuntuQuestionPageLoop [ page? ] {
        let page = if ( $page == null ) {1} else {$page}
        let pageUri = ((echo $baseUri "&page=" $page) | str collect)
        let re = (http $pageUri | from json )
        
        if ($re.has_more) {
            $re.items | append (getAskUbuntuQuestionPageLoop ($page + 1))
        } else {
            $re.items
        }
    }
    $ let questions = (getAskUbuntuQuestionPageLoop)
    

    Note that each future call is appended to the current results.

    Also note that the return results must be the last statement executed in the function.

    Side-note: Personal opinion -- I envision that Nushell will eventually add a yield keyword to allow generator expressions. This will simply the above example further by allowing it inside a reduce that can accumulate the results.