Home > Software engineering >  Is it possible to build a reusable breakable while loop?
Is it possible to build a reusable breakable while loop?

Time:02-23

I am often typing bash commands like while true – show me status of xyz – sleep 1 second:

while true; do
    echo "I am looking up some stuff and displaying the current result!"
    sleep 1
done

I then sometimes realize that it is difficult to break the loop again using Ctrl c, because it only cancels my "payload" command and keeps looping, so usually (after the first run) I type

while true; do
    echo "I am looking up some stuff and displaying the current result!"
    sleep 1
    read -t 0.1 -N 1 input;
    if [[ ! -z "$input" ]]; then 
        break; 
    fi; 
done

But as you see, this is much typing for basically just one line of code (the echo one in this example). Is there any way to make the "breakable while loop executing a command and sleep" reusable, e.g. as a function?

I've already tried with eval:

function runinloop {
    while true; do
        eval -- "$@"
        sleep 1
        read -t 0.1 -N 1 input
        if [[ ! -z "$input" ]]; then
            break
        fi
    done
}

But then, when I run something more complex like

runinloop curl --silent "https://api.stackexchange.com/2.3/questions?tagged=caesar&site=stackoverflow" | gunzip | jq

...it swallows every output and spits it out after I have exited the loop.

CodePudding user response:

anything | gunzip

is always going to "swallow every output" - it's going to gunzip. I think you want:

anything 'curl ... "stuff&stuff" | gunzip'

Note that eval is evil. In the code you posted the character & in caesar&site=st is interpreted as running curl in the background. Then the second command site=stackoverflow is executed, which sets the variable site to stackoverflow. Be aware of caveats when using eval. You have to double-qoute your command for it properly to execute.

Because I that, I recommend a function, which is a win in both safety and readability:

work() {
    curl --silent \
      "https://api.stackexchange.com/2.3/questions?tagged=caesar&site=stackoverflow" |
      gunzip | jq
}
runinloop() {
   ...
     "$@"
    ..
}
runinloop work

Anyway, use watch:

work() {
    ...
}
export -f work
watch bash -c work

    sleep 1
    read -t 0.1 -N 1 input

Just read -t 1, it already sleeps.

CodePudding user response:

I then sometimes realize that it is difficult to break the loop again using Ctrl C, because it only cancels my "payload" command and keeps looping

This should only happen if your "payload" traps SIGINT.

There are two possible solutions I can think of:

  1. Press Ctrl C twice. The first Ctrl C will interrupt your payload and the second one will interrupt sleep and break out of the loop (since sleep doesn't trap SIGINT and it will propagate to the shell).

  2. If your "payload" returns non-zero status when interrupted, you could modify your loop as follows:

    while payload; do sleep 1; done
    

    You could even define an alias

    alias nap="do sleep 1; done"
    

    and save yourself some more typing

    while payload; nap
    

UPDATE:

Here is a simple example of a program that traps SIGINT and returns non-zero exit status:

#include <csignal>
#include <cstdlib>
#include <thread>

int main()
{
    std::signal(SIGINT, [](int) { std::exit(1); });
    std::this_thread::sleep_for(std::chrono::seconds{ 3 });
    return 0;
}

CodePudding user response:

Is there any way to make the "breakable while loop executing a command and sleep" reusable, e.g. as a function?

It depends to some extent on exactly what semantics you want, especially when the command fails (including when it, not the sleep, is the process that receives the Ctrl-C). In any event, however, a key point is that when a process is killed by a signal, its exit status conveys that information. In particular, when the foreground process in a terminal is killed by typing a Ctrl-C, the exit status returned by the shell is 128 2 = 130.

You can use that to write a function that repeats until terminated by a signal:

repeat_until_signaled() {
  local exit_status
  while true; do
    "$@" && sleep 1
    exit_status=$?
    if [[ $exit_status -gt 128 ]]; then
      # The command or the sleep was interrupted by a signal
      return $exit_status
    fi
  done
}

That particular implementation also forwards the exit status indicating termination via signal to the function's caller.

You would use that by simply putting the function name in front of the simple command you want to repeat, like so:

repeat_until_signaled echo "Looking up stuff ..."

Note well, however, that that will work only for repeating simple commands, not compound commands, multi-command lists, or multi-command pipelines. On the other hand, compound commands, etc. can be turned into simple commands by wrapping them in a shell function.

Note also that any redirections you specify will be applied to the whole function invocation, not just the repeated command. Similarly, any environment variable settings must come first, before the function name, and will apply to the whole function invocation, not just the repeated command.

CodePudding user response:

Just add single quotes to your command and it will work fine.

Change

runinloop curl --silent "https://api.stackexchange.com/2.3/questions?tagged=caesar&site=stackoverflow" | gunzip | jq

To

runinloop 'curl --silent "https://api.stackexchange.com/2.3/questions?tagged=caesar&site=stackoverflow" | gunzip | jq'

It works:

bash test.sh
{
  "error_id": 400,
  "error_message": "site is required",
  "error_name": "bad_parameter"
}
{
  "error_id": 400,
  "error_message": "site is required",
  "error_name": "bad_parameter"
}
{
  "error_id": 400,
  "error_message": "site is required",
  "error_name": "bad_parameter"
}
  •  Tags:  
  • bash
  • Related