While loop that exit when the condition is met - bash

I need to write a while loop in bash script that does exit when the process is ended successfully what I have tried so far is;
VAR=`ps -ef |grep -i tail* |grep -v grep |wc -l`
while true; do
{
if [ $VAR = 0 ]
then
echo "Sending MAils ...."
exit
fi
}
done

use break instead of exit to continue the execution of your script.
Also, there is no need for {.

Your script has numerous errors. Probably try https://shellcheck.net/ before asking for human assistance.
You need to update the value of the variable inside the loop.
You seem to be reinventing pgrep, poorly.
(The regular expression tail* looks for tai, tail, taill, tailll ... What do you actually hope this should do?)
To break out of a loop and continue outside, use break.
The braces around your loop are superfluous. This is shell script, not C or Perl.
You are probably looking for something like
while true; do
if ! pgrep tail; then
echo "Sending mails ...."
break
fi
done
This avoids the use of a variable entirely; if you do need a variable, don't use upper case for your private variables.
Based on information in comments, if you have a number of processes like
tail -2000f /var/log/log.{1..10}
and no way to check their PIDs any longer, you might want to use fuser to tell you when none of them are running any longer:
while true; do
fuser /var/log/log.{1..10} || break
sleep 60
done
echo All processes are gone now.
Unfortunately, fuser does not reliably set its exit code - test on the command line (run tail -f $HOME/.bash_profile in one window and then fuser $HOME/.bash_profile && echo yes in another; then quit the tail and run fuser again. If it still prints yes you need something more.)
On MacOS, I found that fuser -u will print parentheses when the files are still open, and not when not:
while true; do
fuser -u /var/log/log.{1..10} 2>&1 | grep -q '[()]' || break
sleep 60
done
On Debian, fuser is in the package psmisc, and does set its exit code properly. You will probably also want to use the -s option to make it run quietly.
Notice also the addition of a sleep to avoid checking hundreds or thousands of times per second. You would probably want to make the same change to the original solution. How long to wait between iterations depends on how urgently you need the notification, and how heavy the operation to check the condition is. Even sleep 0.1 would be a significant improvement over the sleepless spin lock.

Related

Bash init script skips reading commands in if loop

I am trying to create an init script for a program in bash. (rhel6)
It checks for the processes first. If processes are found it will echo that program is already online and if not it'll move on to to start the program as a certain user by using launch script. After doing that it should tail the log file of the program and check for a string of words together. If the words are found it should kill tail and echo that program is online.
Here's the start segment.
prog=someProg
user=someUser
threadCount=$(ps -ef | grep $prog |grep -v 'grep' |awk '{ print $2 }'| wc -l)
startb() {
if [ "$threadCount" -eq 2 ]; then
echo "$prog already online."
else
echo "Bringing $prog online."
su $user -c "/path/to/start/script.sh"
tail -f /path/to/$prog/log/file |
while IFS=$'\n' read line
do
if [[ $line == *started\ up\ and\ registered\ in* ]]; then
pkill tail
echo "$prog now online."
fi
done
fi
}
My problems:
The variable $prog doesn't get picked in $threadcount no
matter how I try. (with single and double quotes)
The logic about tailing the log file works randomly. Some times it
just works perfect. It tails and waits till the string is found
before echoing program is online and at times it just starts script
and then echoes that program is online without the tail or wait.
It's unpredictable. I implemented the same logic in stop segment too to monitor log and then echo but even that works the same way as start. Just random.
I am sure that this might look dumb and broken. This is made by picking pieces here and there with my beginner bash skills.
Thanks in advance for suggestions and help.
I can't reproduce the error you are experiencing with the "grep $prog"...sorry.
But for the other part.
I will assume that the script starting your program, the line with su, is starting something in background and that the script end by itself. If not, your example will wait indefinitely.
Could be a personal preference, but when I'm using something like tail to verify lines, I use a named pipe (mkfifo).
That would give something like :
# Getting the tail in background
tail -f /path/to/$prog/log/file > some_fifo &
# Getting the tail PID
tailPID=$!
while read line; do #You don't need to modify/use IFS here.
if [[ $line == *started\ up\ and\ registered\ in* ]]; then
kill -15 $tailPID #since you know the PID you won't kill another tail
echo "$prog now online."
break # don't like the possibility, even remote, of an infinite loop :)
fi
done < some_fifo #reading from the named pipe
Hope it can help you

Waiting for a file to be created in Bash

I need to create a bash script to wait for a file to be created. The script will use sleep command inside a while loop to periodically check on a file every 10 seconds. Print out a message while waiting. Display the content of the file once the file is created. Below is what I have tried to implement and it obviously does not work. At this point, I'm not entirely sure how to proceed.
#!/bin/bash
let file=$1
while '( -f ! /tmp/$1)'
do
sleep 10
echo "still waiting"
done
echo "Content of the file $1:"
The problem here is with the test, not the sleep (as the original question hypothesized). The smallest possible fix might look as follows:
while ! test -f "/tmp/$1"; do
sleep 10
echo "Still waiting"
done
Keep in mind the syntax for a while loop:
while: while COMMANDS; do COMMANDS; done
Expand and execute COMMANDS as long as the final command in the
`while' COMMANDS has an exit status of zero.
That is to say, the first argument given to while, expanding the loop, is a command; it needs to follow the same syntax rules as any other shell command.
-f is valid as an argument to test -- a command which is also accessible under the name [, requiring a ] as the last argument when used in that name -- but it's not valid as a command in and of itself -- and when passed as part of a string, it's not even a shell word that could be parsed as an individual command name or argument.
When you run '( -f ! /tmp/$1)' as a command, inside quotes, the shell is looking for an actual command with exactly that name (including spaces). You probably don't have a file named '/usr/bin/( -f ! /tmp/$1)' in your PATH or any other command by that name found, so it'll always fail -- exiting the while loop immediately.
By the way -- if you're willing to make your code OS-specific, there are approaches other than using sleep to wait for a file to exist. Consider, for instance, inotifywait, from the inotify-tools package:
while ! test -f "/tmp/$1"; do
echo "waiting for a change to the contents of /tmp" >&2
inotifywait --timeout 10 --event create /tmp >/dev/null || {
(( $? == 2 )) && continue ## inotify exit status 2 means timeout expired
echo "unable to sleep with inotifywait; doing unconditional 10-second loop" >&2
sleep 10
}
done
The benefit of an inotify-based interface is that it returns immediately upon a filesystem change, and doesn't incur polling overhead (which can be particularly significant if it prevents a system from sleeping).
By the way, some practice notes:
Quoting expansions in filenames (ie. "/tmp/$1") prevents names with spaces or wildcards from being expanded into multiple distinct arguments.
Using >&2 on echo commands meant to log for human consumption keeps stderr available for programmatic consumption
let is used for math, not general-purpose assignments. If you want to use "$file", nothing wrong with that -- but the assignment should just be file=$1, with no preceding let.

Exit from BASH Infinite loop in a pipeline

I encountered a somewhat strange behavior of BASH infinite loops which outputs are pipelined to another processes. Namely, I run these two commands:
(while true; do echo xxx; done) | head -n 1
(while true; do date; done) | head -n 1
The first one exits instantly while the second one does not (and I assume it would run forever without being killed). I also tried an implicit infinite loop:
yes | head -n 1
and it also exits by itself. An appropriate line of output is immediately printed on the screen in each case. I am just curious what determines if such a commmand will finish.
When head exits, the standard output of the parenthesized expression is closed. If an external command, like date, is used, the loop hangs. If an internal command of bash is used, like echo, the loop exits. For proof, use
(while true; do /bin/echo xxx; done) | head -n 1
and it will hang. If you use
(while true; do date; echo $? 1>&2; sleep 1; done) | head -n 1
you will see that on the second round, the date command returns an error exit code, i.e. something other but zero. Bash obviously does this not take as serious as when an internal command gets into problems. I wonder if this is intended or rather a bug in bash.
To make sure the loop is exited, this seems to work:
(set -e; while true; do date ; done) | head -n 1

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.

Bash Script Monitor Program for Specific Output

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

Resources