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:
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 (sincesleep
doesn't trapSIGINT
and it will propagate to the shell).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"
}