Bash Script Monitor Program for Specific Output - bash

So this is probably an easy question, but I am not much of a bash programmer and I haven't been able to figure this out.
We have a closed source program that calls a subprogram which runs until it exits, at which point the program will call the subprogram again. This repeats indefinitely.
Unfortunately the main program will sometimes spontaneously (and repeatedly) fail to call the subprogram after a random period of time. The eventual solution is to contact the original developers to get support, but in the meantime we need a quick hotfix for the issue.
I'm trying to write a bash script that will monitor the output of the program and when it sees a specific string, it will restart the machine (the program will run again automatically on boot). The bash script needs to pass all standard output through to the screen up until it sees the specific string. The program also needs to continue to handle user input.
I have tried the following with limited success:
./program1 | ./watcher.sh
watcher.sh is basically just the following:
while read line; do
echo $line
if [$line == "some string"]
then
#the reboot script works fine
./reboot.sh
fi
done
This seems to work OK, but leading whitespace is stripped on the echo statement, and the echo output hangs in the middle until subprogram exits, at which point the rest of the output is printed to the screen. Is there a better way to accomplish what I need to do?
Thanks in advance.

I would do something along the lines of:
stdbuf -o0 ./program1 | grep --line-buffered "some string" | (read && reboot)

you need to quote your $line variable, i.e. "$line" for all references *(except the read line bit).
Your program1 is probably the source of the 'paused' data. It needs to flush its output buffer. You probably don't have control of that, so
a. check if your system has unbuffer command available. If so try unbuffer cmd1 | watcher You may have to experiment with which cmd you wrap unbuffer with, maybe you whill have to do cmd1 | unbuffer watcher.
b. OR you can try wrapping watcher as a process-group, (I think that is the right terminology), i.e.
./program1 | { ./watcher.sh ; printf "\n" ; }
I hope this helps.
P.S. as you appear to be a new user, if you get an answer that helps you please remember to mark it as accepted, and/or give it a + (or -) as a useful answer.

use read's $REPLY variable, also I'd suggest using printf instead of echo
while read; do
printf "%s\n" "$REPLY"
# '[[' is Bash, quotes are not necessary
# use '[ "$REPLY" == "some string" ]' if in another shell
if [[ $REPLY == "some string" ]]
then
#the reboot script works fine
./reboot.sh
fi
done

Related

BASH stops without error, but works if copied in terminal

I am trying to write a script to slice a 13 Gb file in smaller parts to launch a split computation on a cluster. What I wrote so far works on terminal if I copy and paste it, but stops at the first cycle of the for loop.
set -ueo pipefail
NODES=8
READS=0days_rep2.fasta
Ntot=$(cat $READS | grep 'read' | wc -l)
Ndiv=$(($Ntot/$NODES))
for i in $(seq 0 $NODES)
do
echo $i
start_read=$(cat $READS | grep 'read' | head -n $(($Ndiv*${i}+1)) | tail -n 1)
echo ${start_read}
end_read=$(cat $READS | grep 'read' | head -n $(($Ndiv*${i}+$Ndiv)) | tail -n 1)
echo ${end_read}
done
If I run the script:
(base) [andrea#andrea-xps data]$ bash cluster.sh
0
>baa12ba1-4dc2-4fae-a989-c5817d5e487a runid=314af0bb142c280148f1ff034cc5b458c7575ff1 sampleid=0days_rep2 read=280855 ch=289 start_time=2019-10-26T02:42:02Z
(base) [andrea#andrea-xps data]$
it seems to stop abruptly after the command "echo ${start_read}" without raising any sort of error. If I copy and paste the script in terminal it runs without problems.
I am using Manjaro linux.
Andrea
The problem:
The problem here (as #Jens suggested in a comment) has to do with the use of the -e and pipefail options; -e makes the shell exit immediately if any simple command gets an error, and pipefail makes a pipeline fail if any command in it fails.
But what's failing? Take a look at the command here:
start_read=$(cat $READS | grep 'read' | head -n $(($Ndiv*${i}+1)) | tail -n 1)
Which, clearly, runs the cat, grep, head, and tail commands in a pipeline (which runs in a subshell so the output can be captured and put in the start_read variable). So cat starts up, and starts reading from the file and shoving it down the pipe to grep. grep reads that, picks out the lines containing 'read', and feeds them on toward head. head reads the first line of that (note that on the first pass, Ndiv is 0, so it's running head -n 1) from its input, feeds that on toward the tail command, and then exits. tail passes on the one line it got, then exits as well.
The problem is that when head exited, it hadn't read everything grep had to give it; that left grep trying to shove data into a pipe with nothing on the other end, so the system sent it a SIGPIPE signal to tell it that wasn't going to work, and that caused grep to exit with an error status. And then since it exited, cat was similarly trying to stuff an orphaned pipe, so it got a SIGPIPE as well and also exited with an error status.
Since both cat and grep exited with errors, and pipefail is set, that subshell will also exit with an error status, and that means the parent shell considers the whole assignment command to have failed, and abort the script on the spot.
Solutions:
So, one possible solution is to remove the -e option from the set command. -e is kind of janky in what it considers an exit-worthy error and what it doesn't, so I don't generally like it anyway (see BashFAQ #105 for details).
Another problem with -e is that (as we've seen here) it doesn't give much of any indication of what went wrong, or even that something went wrong! Error checking is important, but so's error reporting.
(Note: the danger in removing -e is that your script might get a serious error partway through... and then blindly keep running, in a situation that doesn't make sense, possibly damaging things in the process. So you should think about what might go wrong as the script runs, and add manual error checking as needed. I'll add some examples to my script suggestion below.)
Anyway, just removing -e is just papering over the fact that this isn't a really good approach to the problem. You're reading (or trying to read) over the entire file multiple times, and processing it through multiple commands each time. You really should only be reading through the thing twice: once to figure out how many reads there are, and once to break it into chunks. You might be able to write a program to do the splitting in awk, but most unix-like systems already have a program specifically for this task: split. There's also no need for cat everywhere, since the other commands are perfectly capable of reading directly from files (again, #Jens pointed this out in a comment).
So I think something like this would work:
#!/bin/bash
set -uo pipefail # I removed the -e 'cause I don't trust it
nodes=8 # Note: lower- or mixed-case variables are safer to avoid conflicts
reads=0days_rep2.fasta
splitprefix=0days_split_
Ntot=$(grep -c 'read' "$reads") || { # grep can both read & count in a single step
# The || means this'll run if there was an error in that command.
# A normal thing to do is print an error message to stderr
# (with >&2), then exit the script with a nonzero (error) status
echo "$0: Error counting reads in $reads" >&2
exit 1
}
Ndiv=$((($Ntot+$nodes-1)/$nodes)) # Force it to round *up*, not down
grep 'read' "$reads" | split -l $Ndiv -a1 - "$splitprefix" || {
echo "$0: Error splitting fasta file" >&2
exit 1
}
This'll create files named "0days_split_a" through "0days_split_h". If you have the GNU version of split, you could add its -d option (use numeric suffixes instead of letters) and/or --additional-suffix=.fasta (to add the .fasta extension to the split files).
Another note: if only a little bit of that big file is read lines, it might be faster to run grep 'read' "$reads" >sometempfile first, and then run the rest of the script on the temp file, so you don't have to read & thin it twice. But if most of the file is read lines, this won't help much.
Alright, we have found the troublemaker: set -e in combination with set -o pipefail.
Gordon Davisson's answer provides all the details. I provide this answer for the sole purpose of reaping an upvote for my debugging efforts in the comments to your answer :-)

Why read -p won't work when use curl to get bash script? [duplicate]

I apologize in advance - I don't fully understand the ideas behind what I'm asking well enough to understand why it's not working (I don't know what I need to learn). I searched stack exchange for answers first - I found some information that seemed possibly relevant, but didn't explain the concepts well enough that I understood how to build a working solution. I've been scouring google but haven't found any information that describes exactly what's going on in such a way that I understand. Any direction to background concepts that may help me understand what's going on would be greatly appreciated.
Is it possible to get user input in a bash script that was executed from a pipe?
For example:
wget -q -O - http://myscript.sh | bash
And in the script:
read -p "Do some action (y/n): " __response
if [[ "$__response" =~ ^[Yy]$ ]]; then
echo "Performing some action ..."
fi
As I understand it, this doesn't work because read attempts to read the input from stdin and the bash script is currently "executing through that pipe" (i'm sure there is a more technical accurate way to describe what is occurring, but i don't know how).
I found a solution that recommended using:
read -t 1 __response </dev/tty
However, this does not work either.
Any light shed on the concepts I need to understand to make this work, or explanations of why it is not working or solutions would be greatly appreciated.
The tty solution works. Test it with this code, for example:
$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? y
Sat Apr 12 10:51:16 PDT 2014
$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? n
OK
The prompt from read appears on the terminal and read waits for a response before deciding to echo the date or not.
What I wrote above differs from the line below in two key aspects:
read -t 1 __response </dev/tty
First, the option -t 1 gives read a timeout of one second. Secondly, this command does not provide a prompt. The combination of these two probably means that, even though read was briefly asking for input, you didn't know it.
The main reason why this is not working is, as the OP validly indicated,
The | <pipe> which is used, sends the standard output from the first command as standard input to the second command. In this case, the first command is
wget -q -O - http://myscript.sh
which passes a downloaded script via the pipe to its interpreter bash
The read statement in the script uses the same standard input to obtain its value.
So this is where it collapses because read is not awaiting input from you but takes it from its own script. Example:
$ cat - <<EOF | bash
> set -x
> read p
> somecommand
> echo \$p
> EOF
+ read p
+ echo somecommand
somecommand
In this example, I used a here-document which is piped to bash. The script enables debugging using set -x to show what is happening. As you see, somecommand is never executed but actually read by read and stored in the variable p which is then outputted by echo (note, the $ has been escaped to avoid the substitution in the here-document).
So how can we get this to work then?
First of, never pipe to an interpreter such as {ba,k,z,c,tc,}sh. It is ugly and should be avoided, even though it feels the natural thing to do. The better thing to do is to use any of its options:
bash -c string: If the -c option is present, then commands are read from string. If there are arguments after the string, they are assigned to the positional parameters, starting with $0.
$ bash -c "$(command you want to pipe)"
This also works for zsh, csh, tcsh, ksh, sh and probably a lot of others.

Using read -p in a bash script that was executed from pipe

I apologize in advance - I don't fully understand the ideas behind what I'm asking well enough to understand why it's not working (I don't know what I need to learn). I searched stack exchange for answers first - I found some information that seemed possibly relevant, but didn't explain the concepts well enough that I understood how to build a working solution. I've been scouring google but haven't found any information that describes exactly what's going on in such a way that I understand. Any direction to background concepts that may help me understand what's going on would be greatly appreciated.
Is it possible to get user input in a bash script that was executed from a pipe?
For example:
wget -q -O - http://myscript.sh | bash
And in the script:
read -p "Do some action (y/n): " __response
if [[ "$__response" =~ ^[Yy]$ ]]; then
echo "Performing some action ..."
fi
As I understand it, this doesn't work because read attempts to read the input from stdin and the bash script is currently "executing through that pipe" (i'm sure there is a more technical accurate way to describe what is occurring, but i don't know how).
I found a solution that recommended using:
read -t 1 __response </dev/tty
However, this does not work either.
Any light shed on the concepts I need to understand to make this work, or explanations of why it is not working or solutions would be greatly appreciated.
The tty solution works. Test it with this code, for example:
$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? y
Sat Apr 12 10:51:16 PDT 2014
$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? n
OK
The prompt from read appears on the terminal and read waits for a response before deciding to echo the date or not.
What I wrote above differs from the line below in two key aspects:
read -t 1 __response </dev/tty
First, the option -t 1 gives read a timeout of one second. Secondly, this command does not provide a prompt. The combination of these two probably means that, even though read was briefly asking for input, you didn't know it.
The main reason why this is not working is, as the OP validly indicated,
The | <pipe> which is used, sends the standard output from the first command as standard input to the second command. In this case, the first command is
wget -q -O - http://myscript.sh
which passes a downloaded script via the pipe to its interpreter bash
The read statement in the script uses the same standard input to obtain its value.
So this is where it collapses because read is not awaiting input from you but takes it from its own script. Example:
$ cat - <<EOF | bash
> set -x
> read p
> somecommand
> echo \$p
> EOF
+ read p
+ echo somecommand
somecommand
In this example, I used a here-document which is piped to bash. The script enables debugging using set -x to show what is happening. As you see, somecommand is never executed but actually read by read and stored in the variable p which is then outputted by echo (note, the $ has been escaped to avoid the substitution in the here-document).
So how can we get this to work then?
First of, never pipe to an interpreter such as {ba,k,z,c,tc,}sh. It is ugly and should be avoided, even though it feels the natural thing to do. The better thing to do is to use any of its options:
bash -c string: If the -c option is present, then commands are read from string. If there are arguments after the string, they are assigned to the positional parameters, starting with $0.
$ bash -c "$(command you want to pipe)"
This also works for zsh, csh, tcsh, ksh, sh and probably a lot of others.

Read content from stdout in realtime

I have an external device that I need to power up and then wait for it to get started properly. The way I want to do this is by connecting to it via serial port (via Plink which is a command-line tool for PuTTY) and read all text lines that it prints and try to find the text string that indicates that it has been started properly. When that text string is found, the script will proceed.
The problem is that I need to read these text lines in real-time. So far, I have only seen methods for calling a command and then process its output when that command is finished. Alternatively, I could let Plink run in the background by appending an & to the command and redirecting the output to a file. But the problem is that this file will be empty from the beginning so the script will just proceed directly. Is there maybe a way to wait for a new line of a certain file and read it once it comes? Or does anyone have any other ideas how to accomplish this?
Here is the best solution I have found so far:
./plink "connection_name" > new_file &
sleep 10 # Because I know that it will take a little while before the correct text string pops up but I don't know the exact time it will take...
while read -r line
do
# If $line is the correct string, proceed
done < new_file
However, I want the script to proceed directly when the correct text string is found.
So, in short, is there any way to access the output of a command continously before it has finished executing?
This might be what you're looking for:
while read -r line; do
# do your stuff here with $line
done < <(./plink "connection_name")
And if you need to sleep 10:
{
sleep 10
while read -r line; do
# do your stuff here with $line
done
} < <(./plink "connection_name")
The advantage of this solution compared to the following:
./plink "connection_name" | while read -r line; do
# do stuff here with $line
done
(that I'm sure someone will suggest soon) is that the while loop is not run in a subshell.
The construct <( ... ) is called Process Substitution.
Hope this helps!
Instead of using a regular file, use a named pipe.
mkfifo new_file
./plink "connection_name" > new_file &
while read -r line
do
# If $line is the correct string, proceed
done < new_file
The while loop will block until there is something to read from new_file, so there is no need to sleep.
(This is basically what process substitution does behind the scenes, but doesn't require any special shell support; POSIX shell does not support process substitution.)
Newer versions of bash (4.2 or later) also support an option to allow the final command of a pipeline to execute in the current shell, making the simple solution
shopt +s lastpipe
./plink "connection_name" | while read -r line; do
# ...
done
possible.

Using tail in a subshell in conjunction with while/break does not exit the loop

I have been facing a very peculiar issue with shell scripts.
Here is the scenario
Script1 (spawns in background)--> Script2
Script2 has the following code
function check_log()
{
logfile=$1
tail -5f ${logfile} | while read line
do
echo $line
if echo $line|grep "${triggerword}";then
echo "Logout completion detected"
start_leaks_detection
triggerwordfound=true
echo "Leaks detection complete"
fi
if $triggerwordfound;then
echo "Trigger word found and processing complete.Exiting"
break
fi
done
echo "Outside loop"
exit 0
}
check_log "/tmp/somefile.log" "Logout detected"
Now the break in while loop does not help here. I can see "Logout completion detected" as well as "Leaks detection complete" being echoed on the stdout, but not the string "outside loop"
I am assuming this has to do something with tail -f creating a subshell. What I want to do is, exit that subshell as well as exit Script2 to get control back to Script1.
Can someone please shed some light on how to do this?
Instead of piping into your while loop, use this format instead:
while read line
do
# put loop body here
done < <(tail -5f ${logfile})
Try this, although it's not quite the same (it doesn't skip the beginning of the log file at startup):
triggerwordfound=
while [ -z "$triggerwordfound" ]; do
while read line; do
echo $line
if echo $line|grep "${triggerword}";then
echo "Logout completion detected"
start_leaks_detection
triggerwordfound=true
echo "Leaks detection complete"
fi
done
done < "$logfile"
echo "Outside loop"
The double loop effectively does the same thing as tail -f.
Your function works in a sense, but you won't notice that it does so until another line is written to the file after the trigger word has been found. That's because tail -5 -f can usually write all of the last five lines of the file to the pipe in one write() call and continue to write new lines all in one call, so it won't be sent a SIGPIPE signal until it tries to write to the pipe after the while loop has exited.
So, if your file grows regularly then there shouldn't be a problem, but if it's more common for your file to stop growing just after the trigger word is written to it, then your watcher script will also hang until any new output is written to the file.
I.e. SIGPIPE is not sent immediately when a pipe is closed, even if there's un-read data buffered in it, but only when a subsequent write() on the pipe is attempted.
This can be demonstrated very simply. This command will not exit (provided the tail of the file is less than a pipe-sized buffer) until you either interrupt it manually, or you write one more byte to the file:
tail -f some_large_file | read one
However if you force tail to make multiple writes to the pipe and make sure the reader exits before the final write, then everything will work as expected:
tail -c 1000000 some_large_file | read one
Unfortunately it's not always easy to discover the size of a pipe buffer on a given system, nor is it always possible to only start reading the file when there's already more than a pipe buffer's worth of data in the file, and the trigger word is already in the file and at least a pipe buffer's size bytes from the end of the file.
Unfortunately tail -F (which is what you should probably use instead of -f) doesn't also try writing zero bytes every 5 seconds, or else that would maybe solve your problem in a more efficient manner.
Also, if you're going to stick with using tail, then -1 is probably sufficient, at least for detecting any future event.
BTW, here's a slightly improved implementation, still using tail since I think that's probably your best option (you could always add a periodic marker line to the log with cron or similar (most syslogd implementations have a built-in mark feature too) to guarantee that your function will return within the period of the marker):
check_log ()
{
tail -1 -F "$1" | while read line; do
case "$line" in
*"${2:-SOMETHING_IMPOSSIBLE_THAT_CANNOT_MATCH}"*)
echo "Found trigger word"
break
;;
esac
done
}
Replace the echo statement with whatever processing you need to do when the trigger phrase is read.

Resources