19

I want to create a small code in C++ with the same functionality as "tail-f": watch for new lines in a text file and show them in the standard output.

The idea is to have a thread that monitors the file

Is there an easy way to do it without opening and closing the file each time?

Hamming
  • 193
  • 1
  • 1
  • 4

6 Answers6

16

Have a look at inotify on Linux or kqueue on Mac OS.

Inotify is Linux kernel subsystem that allows you to subscribe for events on files and it will report to your application when the even happened on your file.

stefanB
  • 77,323
  • 27
  • 116
  • 141
15

Just keep reading the file. If the read fails, do nothing. There's no need to repeatedly open and close it. However, you will find it much more efficient to use operating system specific features to monitor the file, should your OS provide them.

  • 5
    +1: Trying to read to the end of the file from where you've got to so far (for a reasonable length file) once a second is pretty cheap in practice. You just read until you've got to the end, then sleep for a second and try the read again. (If you're on Windows, take care to open with the right sharing flags so that you don't lock out other writers. That probably means using the native IO calls rather than the C++ standard ones...) – Donal Fellows Apr 23 '10 at 08:23
2

Same as in https://stackoverflow.com/a/7514051/44729 except that the code below uses getline instead of getc and doesn't skip new lines

#include <iostream>
#include <string>
#include <fstream>
#include <sstream>

using namespace std;

static int last_position=0;
// read file untill new line
// save position

int find_new_text(ifstream &infile) {

   infile.seekg(0,ios::end);
   int filesize = infile.tellg();

   // check if the new file started
   if(filesize < last_position){
      last_position=0;
   }  
   // read file from last position  untill new line is found 

   for(int n=last_position;n<filesize;n++) {

      infile.seekg( last_position,ios::beg);
      char  test[256]; 
      infile.getline(test, 256);
      last_position = infile.tellg();
      cout << "Char: "  << test <<"Last position " << last_position<<  endl;
      // end of file 
      if(filesize == last_position){
        return filesize;
      } 

  }

  return 0;
}


int main() {

  for(;;) {
    std::ifstream infile("filename");
    int current_position = find_new_text(infile);
    sleep(1);
  } 

} 
Community
  • 1
  • 1
  • 1
    filesize < last_position doesn't necessarily mean a new file started. It could also mean reached the end of file – Wenjing Nov 06 '14 at 01:01
1

I read this in one of Perl manuals, but it is easily translated into standard C, which, in turn, can be translated to istreams.

   seek FILEHANDLE,POSITION,WHENCE
      Sets FILEHANDLE's position, just like the "fseek" call of
      "stdio".  
       <...>
       A WHENCE of 1 ("SEEK_CUR") is useful for not moving the file 
       position:

           seek(TEST,0,1);

       This is also useful for applications emulating "tail -f".  Once
       you hit EOF on your read, and then sleep for a while, you might
       have to stick in a seek() to reset things.  The "seek" doesn't
       change the current position, but it does clear the end-of-file
       condition on the handle, so that the next "<FILE>" makes Perl
       try again to read something.  We hope.

As far as I remember, fseek is called iostream::seekg. So you should basically do the same: seek to the end of the file, then sleep and seek again with ios_base::cur flag to update end-of-file and read some more data.

Instead of sleeping, you may use inotify, as suggested in the other answer, to sleep (block while reading from an emulated file, actually) exactly until the file is updated/closed. But that's Linux-specific, and is not standard C++.

Community
  • 1
  • 1
P Shved
  • 96,026
  • 17
  • 121
  • 165
  • love the "we hope" closing. Sticks well together with "this is weird, but it's good because it's weird" and the many other self-bashing realizations that are so typical of perl... – Stefano Borini Apr 23 '10 at 09:17
  • @Stefano - well, it's good here because it relates to Perl's implementation of linewise file reading (``), and is not about how `fseek` works. I hope. – P Shved Apr 23 '10 at 09:20
1

I needed to implement this too, I just wrote a quick hack in standard C++. The hack searches for the last 0x0A (linefeed character) in a file and outputs all data following that linefeed when the last linefeed value becomes a larger value. The code is here:

#include <iostream>
#include <string>
#include <fstream>

using namespace std;


int find_last_linefeed(ifstream &infile) {

  infile.seekg(0,ios::end);
  int filesize = infile.tellg();

  for(int n=1;n<filesize;n++) {
    infile.seekg(filesize-n-1,ios::beg);

    char c;
    infile.get(c);

    if(c == 0x0A) return infile.tellg();
  }
}


int main() {


  int last_position=-1;
  for(;;) {

    ifstream infile("testfile");
    int position = find_last_linefeed(infile);

    if(position > last_position) {
      infile.seekg(position,ios::beg);
      string in;
      infile >> in;
      cout << in << endl;
    }
    last_position=position;

    sleep(1);
  }

}
genpfault
  • 51,148
  • 11
  • 85
  • 139
new299
  • 804
  • 1
  • 7
  • 17
0
#include <iostream>
#include <fstream>
#include <string>
#include <list>
#include <sys/stat.h> 
#include <stdlib.h>

#define debug 0

class MyTail
{
private:
std::list<std::string> mLastNLine;
const int mNoOfLines;
std::ifstream mIn;

public:

explicit MyTail(int pNoOfLines):mNoOfLines(pNoOfLines) {}

const int getNoOfLines() {return mNoOfLines; }

void getLastNLines();

void printLastNLines();

void tailF(const char* filename);

};

void MyTail::getLastNLines() 
{
    if (debug) std::cout << "In: getLastNLines()" << std::endl;
    mIn.seekg(-1,std::ios::end);
    int pos=mIn.tellg();
    int count = 1;

    //Get file pointer to point to bottom up mNoOfLines.
    for(int i=0;i<pos;i++)
    {
        if (mIn.get() == '\n')
            if (count++ > mNoOfLines)
                break;
        mIn.seekg(-2,std::ios::cur);
    }

    //Start copying bottom mNoOfLines string into list to avoid I/O calls to print lines
    std::string line;
    while(getline(mIn,line)) {
        int data_Size = mLastNLine.size();
        if(data_Size >= mNoOfLines) {
            mLastNLine.pop_front();
        }
        mLastNLine.push_back(line);
    }

    if (debug) std::cout << "Out: getLastNLines()" << std::endl;
}

void MyTail::printLastNLines()
{    
     for (std::list<std::string>::iterator i = mLastNLine.begin();  i !=         mLastNLine.end(); ++i)
     std::cout << *i << std::endl;
}

void MyTail::tailF(const char* filename)
{
    if (debug) std::cout << "In: TailF()" << std::endl;

    int date = 0;
    while (true) {
        struct stat st;
        stat (filename, &st);
        int newdate = st.st_mtime;
        if (newdate != date){
            system("@cls||clear");
            std::cout << "Print last " << getNoOfLines() << " Lines: \n";
            mIn.open(filename);
            date = newdate;
            getLastNLines();
            mIn.close();
            printLastNLines();
        }
    }
    if (debug) std::cout << "Out: TailF()" << std::endl;        
}

int main(int argc, char **argv)
{
    if(argc==1) {
        std::cout << "No Extra Command Line Argument Passed Other Than Program Name\n"; 
        return 0;
    }

    if(argc>=2) {
        MyTail t1(10);
        t1.tailF(argv[1]);
    }
    return 0;
}