Home > Software design >  Do a script to watch inside a file [closed]
Do a script to watch inside a file [closed]

Time:09-16

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
  • Related