Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Watch file and execute command using tail -f and while read?

I am trying to watch a file and execute a command once every time the file is changed, ideally with just native bash commands.

This is as far as I have got, but how do I check if I have reached the beginning or end of the file? I realize that tail -f doesn't read EOF so how I can tell that I have reached the end of the file?

 tail -f source_file.js | while read line || [[ -n "$line" ]]; 
     # how do I execute a command here just **once**?
 done

Answers which don't use tail or while read will be accepted as long as they are native bash commands and about one line.

Perhaps I could zero a variable every time while is called?

like image 318
david_adler Avatar asked Sep 07 '14 16:09

david_adler


People also ask

How do you run a tail command?

Tail command also comes with an '+' option which is not present in the head command. With this option tail command prints the data starting from specified line number of the file instead of end. For command: tail +n file_name, data will start printing from line number 'n' till the end of the file specified.

How do you use head and tail command?

To read the entire file, 'cat', 'more', and 'less' commands are used. But when the specific part of the file is required to read then 'head' and 'tail' commands are used to do that task. 'head' command is used to read the file from the beginning and the 'tail' command is used to read the file from the ending.

What is the use of watch command?

The watch command is a built-in Linux utility used for running user-defined commands at regular intervals. It temporarily clears all the terminal content and displays the output of the attached command, along with the current system date and time. By default, the watch command updates the output every two seconds.


3 Answers

From the man:

-f

The -f option causes tail to not stop when end of file is reached, but rather to wait for additional data to be appended to the input.

So, if you want monitor and do actions, need break the script into two processes

  • one will show the content tail (if you want personally monitor the changes)
  • second will monitor changes and do actions

or

  • you should use for example perl or python what can monitor end-of-file and execute some actions when reach it (for example, run an bash script).

The bash soultion can be based of file-modification time

file="./file"

runcmd() {
        echo "======== file $1 is changed ============"
}

#tail -f "$file" &   #uncomment 3 lines is you want pesonally watch the file
#tailpid=$!
#trap "kill $tailpid;exit" 0 2    #kill the tail, when CTRL-C this script

lastmtime=0
while read -r mtime < <(stat -c '%Z' "$file")
do
        if [[ $lastmtime != $mtime ]]
        then
                lastmtime=$mtime
                runcmd "$file"
        fi
        sleep 1
done

added another solution based on standard perl

perltail() {
#adapted from the perlfaq5
#http://perldoc.perl.org/perlfaq5.html#How-do-I-do-a-tail--f-in-perl%3f
perl -MTime::HiRes=usleep -Mstrict -Mautodie -e '
$|=1;
open my $fh, "<", "$ARGV[0]";
my $curpos;
my $eof=1;
for (;;) {
    for( $curpos = tell($fh); <$fh>; $curpos =tell($fh) ) {
        print;
        $eof=1
    }
    print "=EOF-reached=\n" if $eof;
    $eof=0;
    usleep(1000); #adjust the microseconds
    seek($fh, $curpos, 0);
}' "$1"
}

eof_action() {
    echo "EOF ACTION"
    printf "%s\n" "${newlines[@]}"  #new lines received from the last eof
    newlines=()         #empty the newlines array
}

perltail "./file" | while read -r line
do
    if [[ $line =~ =EOF-reached= ]]
    then
        eof_action
        continue
    fi
    #do something with the received lines - if need
    #for example, store new lines into variable for processing in the do_action and like
    newlines+=($line)
done

Principe:

  • the perltail bash function runs an perl implementation of tail -f, and additionally to, when reached the end-of-file it prints an MARK to the output, here: =EOF-reached=.
  • the bash while read looking for the MARK and run action only the the mark exists - e.g. only when the end-of-file reached.
like image 165
jm666 Avatar answered Nov 15 '22 03:11

jm666


It's by no means a native bash solution but you could use libinotify to do what you want:

while inotifywait -qqe modify file; do 
    echo "file modified"
done

This watches for modifications to file and performs the action within the loop whenever they happen. The -qq switch suppresses the output of the program, which by default prints a message every time something happens to the file.

like image 33
Tom Fenech Avatar answered Nov 15 '22 02:11

Tom Fenech


I am not sure of your exact meaning of doing something "once every time a file is changed" or an implementation of "about one line". The tail command is usually used in a line-oriented manner, so I guess that something like 500 lines of text appended to a file in a single write(2) is an update worthy of only one invocation of you command.

But what about, say tens of lines appended after delays of tens of milliseconds? How often do you wish the command be called?

If libinotify is available, per Mr. Fenech, use it. If you're trying to use somewhat more basic shell utilities, find(1) can also be used to notice when one file has become newer than another.

For example, a script that watches a file, and runs a supplied command when it's updated, polling at 1-second intervals.

#!/bin/sh

[ $# -ge 2 ] || { echo "Usage: $(basename $0) <file> <cmd...>" 1>&2; exit 1; }
[ -r "$1"  ] || { echo "$1: cannot read" 1>&2; exit 1; }

FILE=$1; shift

FTMP=$(mktemp /tmp/$(basename "$FILE")_tsref.XXXXXX)
trap 'rm -f "$FTMP"' EXIT

touch -r "$FILE" "$FTMP"

while true; do
    FOUT=$(find "$FILE" -newer "$FTMP") || exit $?
    if [ "$FOUT" = "$FILE" ]; then
        touch -r "$FILE" "$FTMP"
        eval "$@"
    else
        sleep 1
    fi
done
like image 41
sjnarv Avatar answered Nov 15 '22 03:11

sjnarv