Skip to content

Commit

Permalink
initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
yaroslaff committed Oct 29, 2023
1 parent fcf2f12 commit e49bf45
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 0 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# newcat

print (cat) only new lines in files (after previous run of newcat). This is useful to parse recent log records.

## Install
~~~
pipx install newcat
~~~

## Example
~~~bash
$ echo This is line 1 > /tmp/test.log

# works like normal /bin/cat now
$ newcat /tmp/test.log
This is line 1

# only new lines are printed
$ echo This is line 2 >> /tmp/test.log
$ newcat /tmp/test.log
This is line 2

# no new lines => nothing on stderr
$ newcat /tmp/test.log

# despite file is having two lines
$ cat /tmp/test.log
This is line 1
This is line 2
~~~

## How does this magic work?
newcat stores some info about file (including position in file) in *state file*. Default state file is `/tmp/.newcat.state`, but can be overriden with `NEWCAT_STATE` environment variable or `-s` / `--state` option. Normally, newcat prints file content starting from this position (which was saved before), thus lines which were already printed before, will not be printed on new newcat run.

Sometime file content it rewritten (e.g. after `echo New content > /tmp/test.log`). To detect this situation newcat checks two things:
- file inode changed
- file size is less then old printed position

if any of these conditions are met, file considered as rewritten and newcat prints it from the beginning.
Note: this is not absolutely reliable, and there is a chance that sometimes newcat will not detect when file is overwritten.


70 changes: 70 additions & 0 deletions newcat
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python

import argparse
import json
import os
import sys
import shutil

__version__ = '0.0.1'

state = dict()

def get_args():
def_state = os.getenv('NEWCAT_STATE', '/tmp/.newcat.state')
parser = argparse.ArgumentParser(description=f'newcat version {__version__}. print / cat only new lines in log files')
parser.add_argument('-s','--state',metavar='PATH', default=def_state, help=f'path to state file. default: {def_state}')
parser.add_argument('files', metavar='LOGFILE', nargs='+', help='log file(s) to read')
return parser.parse_args()

def cat(path: str, filestate: dict) -> None:
with open(path ,"r") as fh:

if not file_rewritten(path, filestate):
fh.seek(filestate['pos'])

shutil.copyfileobj(fsrc=fh, fdst=sys.stdout)
filestate['pos'] = fh.tell()

def create_state(path):
s = dict()
stat = os.stat(path)
s['pos'] = 0
s['st_ino'] = stat.st_ino
return s

def file_rewritten(path: str, oldstate: dict) -> bool:

stat = os.stat(path)

if oldstate['st_ino'] != stat.st_ino:
return True

if oldstate['pos'] > stat.st_size:
return True

return False

def main():
global state
args=get_args()

try:
with open(args.state) as fh:
state = json.load(fh)
except FileNotFoundError:
state = dict()
for file in args.files:
file = os.path.realpath(file)

if file not in state or file_rewritten(file, state[file]):
state[file] = create_state(file)

cat(file, state[file])


with open(args.state, "w") as fh:
json.dump(state, fh, indent=4, sort_keys=True)

if __name__ == '__main__':
main()
45 changes: 45 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3

from setuptools import setup
import os
import sys

from importlib.machinery import SourceFileLoader

def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()

def get_version(path):
foo = SourceFileLoader(os.path.basename(path), path).load_module()
return foo.__version__

setup(
name='newcat',
version=get_version('newcat'),
packages=[],
scripts=['newcat'],

install_requires=[],

url='https://github.com/yaroslaff/newcat',
license='MIT',
author='Yaroslav Polyakov',
long_description=read('README.md'),
long_description_content_type='text/markdown',
author_email='[email protected]',
description='print / cat only new lines in log files',
python_requires='>=3.6',
classifiers=[
'Development Status :: 3 - Alpha',

# Indicate who your project is intended for
'Intended Audience :: Developers',

# Pick your license as you wish (should match "license" above)
'License :: OSI Approved :: MIT License',

# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
'Programming Language :: Python :: 3.6',
],
)

0 comments on commit e49bf45

Please sign in to comment.