I am currently doing a script to watch a file for any added lines. My current script is:
#!/bin/bash
GRAY_R='\033[1;30m'
RESET='\033[0m'
refreshtime="0.5"
filetowatch="/root/upgrade.log"
while [ -n "$1" ]; do
case "$1" in
-r | --refresh)
isnumber='^[0-9] ([.][0-9] )?$'
if ! [[ $2 =~ $isnumber ]] ; then
echo "--refresh || -r need a number as argument (int or float)" >&2; exit 1
else
refreshtime="$2"
shift
fi;;
-h | --help)
echo "You can use -r or --refresh to set a refresh time (0.5s by default) or give a file as argument to watch this file (/root/upgrade.log by default)."
exit;;
*)
if [ -f "$1" ]; then
filetowatch="$1"
else
echo "$1 is not a file, please use -h or --help to see help."
fi;;
esac
shift
done
tput smcup
while true
do
clear
cat ${filetowatch}
echo
echo -e "${GRAY_R}Press Q to quit - - - - - - - - - - - - - - - Refresh time ${refreshtime}s${RESET}"
read -t ${refreshtime} -N 1 input
if [[ $input = "q" ]] || [[ $input = "Q" ]]
then
echo
clear
break
fi
done
tput rmcup
And this works very fine, just as I want, but there is 1 problem.
When I'm watching a very long file (longer than maybe 30 lines) part of it scrolls out of view and I can't use my scroll bar to read it, because the script uses clear
then cat
again and again, interfering with the scrolling.
So, I would like to know how I could do something like tail -f
?
I want to script it and not just use tail -f
to do it (I'm doing this for learning, otherwise I could just use watch cat
instead of this script)
Sorry for my English and thank you for your help !
CodePudding user response:
If I understand this correctly, you are trying to emulate the command tail -f
.
There is a quick and elegant way, using read
, as suggested by triplee's comment:
# parse command line and get filename and timeout
...
# watch the file until told to quit
clear
while true; do
# read line by line while there is something to read
while IFS='' read -r line; do
echo "$line"
done
# then wait for user input for the specified time
read -t ${refreshtime} -N 1 input
if [[ $input = "q" ]] || [[ $input = "Q" ]]; then
break
fi
# then loop back to trying to read the file. The script "remembers"
# where it left off in the file, because the file is opened by the
# outer while loop, which is still running
done <"${filetowatch}"
In your original script, you have a prompt at the bottom (Press Q to quit...). Let's add that here. We'll use a trick so we don't have to use clear
every time. This will work as long as your prompt string is not longer than one terminal line:
...
# then wait for user input for the specified time
echo -ne "${GRAY_R}Press Q to quit - Refresh time ${refreshtime}s${RESET}" # -n means the cursor stays on this line
read -t ${refreshtime} -N 1 input
if [[ $input = "q" ]] || [[ $input = "Q" ]]; then
break
fi
echo -ne '\r\033[K' # go back to the beginning of the line and erase it
# then loop back to trying to read the file
...
Now let's add an edge case - if you're watching a logfile, it might get truncated from time to time, as logfiles usually are rotated on a regular basis.
The read
solution will not work, because it will keep trying to read at the last position it left off.
Since this question is for learning purposes, I'll assume you are ok with solutions that are slow and more disk intensive (and which wouldn't be recommended in production).
One way to do this is to keep track of the number of lines you've already seen in the tracked file, and only print the part that is new. When the logfile is rotated, you'll need to reset the number of "seen" lines back to zero - and you may lose some of the output (lines that were added to the logfile before it was rotated, but after you last read the file).
For printing part of a file, you could use tail
itself - it has an option to print a file starting at a given line (-n K
).
If (for learning purposes) you don't want to use tail
at all, you can also read each line and print what you need (again, slower):
# parse command line and get filename and timeout
#...
# watch the file until told to quit
lines_seen=0
clear
while true; do
lines_total=$(wc -l "${filetowatch} | cut -f 1 -d ' '")
if ((lines_total < lines_seen)); then
# log file was truncated
lines_seen=0
fi
if ((lines_total > lines_seen)); then
for (( i=0; ; i )); do
# read the whole file
if IFS='' read -r line; then
# but only print newer lines
if ((lines_seen <= i)); then
echo "$line"
fi
else
break
fi
done <${filetowatch}
# update the number of lines printed (which may be different than lines_total meanwhile)
lines_seen=$i
fi
echo -ne "${GRAY_R}Press Q to quit - Refresh time ${refreshtime}s - ${lines_seen} lines${RESET}"
read -t ${refreshtime} -N 1 input
echo -ne '\r\033[K'
if [[ $input = "q" ]] || [[ $input = "Q" ]]; then
break
fi
done