Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
|
93cf7b35b9 | |
|
25717fff98 | |
|
5d42d92598 | |
|
29491e1639 | |
|
c619e74e15 | |
|
411939468b | |
|
2cd059908c | |
|
9ab32e5b6b |
13
README.md
13
README.md
|
@ -12,7 +12,7 @@ Usage
|
|||
-----
|
||||
|
||||
```
|
||||
kitsune [--match-filename pattern] [paths]
|
||||
kitsune [--match-filename pattern] [--start] [paths]
|
||||
```
|
||||
|
||||
There are zero or more paths. If 0 paths are provided, "." (the current
|
||||
|
@ -26,10 +26,13 @@ any new file matching pattern will also added.
|
|||
pattern is a standard shell glob pattern (as implemented by Pythons
|
||||
fnmatch module).
|
||||
|
||||
The output contains of the file name, a timestamp and the contents of
|
||||
each line for each line in the watched files. It pads the file names to
|
||||
the length of the longest file name seen so far, so the three columns
|
||||
should line up nicely.
|
||||
By default, kitsune will print only content added after program start.
|
||||
The `--start` option will dump each file from the beginning.
|
||||
|
||||
The output contains of the file name, a timestamp, a deletion indicator
|
||||
and the contents of each line for each line in the watched files. It
|
||||
pads the file names to the length of the longest file name seen so far,
|
||||
so the three columns should line up nicely.
|
||||
|
||||
Name
|
||||
----
|
||||
|
|
109
kitsune
109
kitsune
|
@ -9,19 +9,29 @@ import stat
|
|||
import time
|
||||
|
||||
class WatchedFile:
|
||||
def __init__(self, path):
|
||||
def __init__(self, path, seek_to_end):
|
||||
self.path = path
|
||||
self.reopen()
|
||||
self.seek_to_end = seek_to_end
|
||||
stf = os.stat(self.path)
|
||||
self.st_ctime_ns = stf.st_ctime_ns
|
||||
self.st_size = stf.st_size
|
||||
self.fileno = None
|
||||
if stf.st_ctime_ns >= time.time_ns() - 1E9:
|
||||
self.reopen()
|
||||
|
||||
def reopen(self):
|
||||
self.fd = open(self.path, errors='replace')
|
||||
self.fileno = self.fd.fileno()
|
||||
self.last_ts = 0
|
||||
if self.seek_to_end:
|
||||
self.fd.seek(self.st_size, 0)
|
||||
self.seek_to_end = False
|
||||
|
||||
def format_ts(ts_ns):
|
||||
return datetime.datetime.fromtimestamp(ts_ns / 1E9).strftime("%H:%M:%S.%f")
|
||||
|
||||
def watch(args):
|
||||
seek_to_end = not args.start
|
||||
watched_dirs = []
|
||||
watched_files = {}
|
||||
filename_length = 0
|
||||
|
@ -30,7 +40,7 @@ def watch(args):
|
|||
if stat.S_ISDIR(st.st_mode):
|
||||
watched_dirs.append(a)
|
||||
elif stat.S_ISREG(st.st_mode):
|
||||
watched_files[a] = WatchedFile(a)
|
||||
watched_files[a] = WatchedFile(a, seek_to_end)
|
||||
if len(a) > filename_length:
|
||||
filename_length = len(a)
|
||||
else:
|
||||
|
@ -44,40 +54,81 @@ def watch(args):
|
|||
if st.st_mtime_ns > last_ts:
|
||||
last_ts = st.st_mtime_ns
|
||||
for de in os.scandir(d):
|
||||
if de.is_file():
|
||||
if args.match_filename is None or fnmatch.fnmatch(de.name, args.match_filename):
|
||||
if de.path not in watched_files:
|
||||
watched_files[de.path] = WatchedFile(de.path)
|
||||
if len(de.path) > filename_length:
|
||||
filename_length = len(de.path)
|
||||
try:
|
||||
if de.is_file():
|
||||
if args.match_filename is None or fnmatch.fnmatch(de.name, args.match_filename):
|
||||
if de.path not in watched_files:
|
||||
watched_files[de.path] = WatchedFile(de.path, seek_to_end)
|
||||
if len(de.path) > filename_length:
|
||||
filename_length = len(de.path)
|
||||
except (PermissionError, OSError):
|
||||
# ignore,
|
||||
# or maybe remove from watched_files?
|
||||
# or just mark as unavailable?
|
||||
pass
|
||||
|
||||
# has any of the files changed
|
||||
for f in watched_files.values():
|
||||
# XXX - we should also detect replaced files.
|
||||
st = os.stat(f.fileno)
|
||||
try:
|
||||
stf = os.stat(f.path)
|
||||
if stf.st_ino != st.st_ino:
|
||||
f.reopen()
|
||||
except FileNotFoundError:
|
||||
# ignore,
|
||||
# or maybe remove from watched_files?
|
||||
# or just mark as deleted?
|
||||
pass
|
||||
if st.st_mtime_ns > f.last_ts:
|
||||
f.last_ts = st.st_mtime_ns
|
||||
new_content = f.fd.read()
|
||||
lines = new_content.split("\n")
|
||||
if lines[-1] == "":
|
||||
lines.pop()
|
||||
for ln in lines:
|
||||
print(f"{f.path:{filename_length}}", format_ts(f.last_ts), ln)
|
||||
if f.fileno:
|
||||
st = os.stat(f.fileno)
|
||||
try:
|
||||
stf = os.stat(f.path)
|
||||
if stf.st_ino != st.st_ino:
|
||||
f.reopen()
|
||||
st = os.stat(f.fileno)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
# ignore,
|
||||
# or maybe remove from watched_files?
|
||||
# or just mark as deleted?
|
||||
pass
|
||||
if st.st_mtime_ns > f.last_ts:
|
||||
if st.st_size < f.fd.tell():
|
||||
# We are beyond the end of the file, so it has probably
|
||||
# been truncated and rewritten - read from beginning
|
||||
f.fd.seek(0, 0)
|
||||
dead = "✝" if st.st_nlink == 0 else " "
|
||||
f.last_ts = st.st_mtime_ns
|
||||
new_content = f.fd.read()
|
||||
lines = new_content.split("\n")
|
||||
if lines[-1] == "":
|
||||
lines.pop()
|
||||
for ln in lines:
|
||||
s = ""
|
||||
if args.print_filename:
|
||||
s += f"{f.path:{filename_length}} "
|
||||
if args.print_timestamp:
|
||||
s += format_ts(f.last_ts) + " "
|
||||
s += dead + " " + ln
|
||||
print(s)
|
||||
else:
|
||||
try:
|
||||
stf = os.stat(f.path)
|
||||
if stf.st_ctime_ns != f.st_ctime_ns:
|
||||
f.reopen()
|
||||
except (FileNotFoundError, PermissionError):
|
||||
# ignore,
|
||||
# or maybe remove from watched_files?
|
||||
# or just mark as deleted?
|
||||
pass
|
||||
time.sleep(0.1)
|
||||
seek_to_end = False
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--match-filename")
|
||||
ap.add_argument("files", nargs="*", default=["."])
|
||||
ap.add_argument("--start", action='store_true',
|
||||
help="dump existing files from the start")
|
||||
ap.add_argument("--match-filename",
|
||||
help="follow only matching files in directories",
|
||||
metavar="GLOB-PATTERN")
|
||||
ap.add_argument("--no-filename", "--no-print-filename", action="store_false",
|
||||
dest="print_filename",
|
||||
help="don't print filename in each line")
|
||||
ap.add_argument("--no-timestamp", "--no-print-timestamp", action="store_false",
|
||||
dest="print_timestamp",
|
||||
help="don't print timestamp in each line")
|
||||
ap.add_argument("files", nargs="*", default=["."],
|
||||
metavar="file")
|
||||
args = ap.parse_args()
|
||||
|
||||
watch(args)
|
||||
|
|
Loading…
Reference in New Issue