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).
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:
questions
to collect the results of each iterationfor
loops over an infinite range of 1..
http get
to retrieve the next page. While in the original question I mentioned that I needed to use httpie
to handle the SE API's gzipped results, the Nushell builtin now handles this properly as well.$"string with ($variable_interprolation)"
to update the page number$res
holds the payload (the questions) in $res.items), which is then appended to the mutable
$questions` list.$res.has_more
to tell us if we need to continue the loop. If not, we break
it. This acts as a simple until
condition.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 "apache2.service: Failed with result 'exit-code'." and │ https://askubuntu.com/questions/1400332/find-reason-for-apache2-service-failed-with-result-exit-code │
│ │ │ "Failed to start The Apache HTTP Server." │ -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:
While: mut x = 0; while $x < 10 { $x = $x + 1 }
Loop: mut x = 0; loop { if $x > 10 { break }; $x = $x + 1 }; $x
Although in reality that would be better served with a for
, of course.
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 append
ed 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.