├── Makefile ├── rwc.1 ├── README └── rwc.c /Makefile: -------------------------------------------------------------------------------- 1 | ALL=rwc 2 | 3 | CFLAGS=-g -O2 -Wall -Wno-switch -Wextra -Wwrite-strings 4 | 5 | DESTDIR= 6 | PREFIX=/usr/local 7 | BINDIR=$(PREFIX)/bin 8 | MANDIR=$(PREFIX)/share/man 9 | 10 | all: $(ALL) 11 | 12 | README: rwc.1 13 | mandoc -Tutf8 $< | col -bx >$@ 14 | 15 | clean: FRC 16 | rm -f $(ALL) 17 | 18 | install: FRC all 19 | mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 20 | install -m0755 $(ALL) $(DESTDIR)$(BINDIR) 21 | install -m0644 $(ALL:=.1) $(DESTDIR)$(MANDIR)/man1 22 | 23 | FRC: 24 | -------------------------------------------------------------------------------- /rwc.1: -------------------------------------------------------------------------------- 1 | .Dd January 3, 2021 2 | .Dt RWC 1 3 | .Os 4 | .Sh NAME 5 | .Nm rwc 6 | .Nd report when changed 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Op Fl 0cdep 10 | .Op Ar path\ ... 11 | .Sh DESCRIPTION 12 | .Nm 13 | takes a list of files or directories, watches them using 14 | .Xr inotify 7 , 15 | and prints each file name as absolute path when it changed. 16 | If 17 | .Ar path 18 | is a single dash 19 | .Pq Sq - 20 | or absent, 21 | .Nm 22 | reads file names from the standard input. 23 | .Pp 24 | Watching a directory will result in watching all changes to files 25 | which resides directly in that directory. 26 | .Pp 27 | The options are as follows: 28 | .Bl -tag -width Ds 29 | .It Fl 0 30 | Read input filenames seperated by NUL bytes. 31 | Likewise, output filenames seperated by NUL bytes. 32 | .It Fl c 33 | Detect all file creations, including 34 | .Xr open 2 35 | with 36 | .Dv O_CREAT , 37 | .Xr mkdir 2 , 38 | .Xr link 2 , 39 | .Xr symlink 2 , 40 | and 41 | .Xr bind 2 . 42 | In this case, created files are prefixed by 43 | .Sq Li "+ " 44 | (that is, a plus and a space). 45 | This option has the side-effect of printing files twice 46 | that are created and immediately changed after. 47 | .It Fl d 48 | Also detect file deletion. 49 | In this case, deleted files are prefixed by 50 | .Sq Li "- " 51 | (that is, a dash and a space). 52 | .It Fl e 53 | Exit after the first reported change. 54 | .It Fl p 55 | Pipe mode; 56 | don't report changes while the standard output pipe is not empty. 57 | Use this to pipe 58 | .Nm 59 | to programs which read standard input slowly. 60 | .El 61 | .Sh EXIT STATUS 62 | .Ex -std 63 | .Sh EXAMPLES 64 | Watch all source files and run 65 | .Xr make 1 66 | when something changes: 67 | .Pp 68 | .Dl % git ls-files | rwc -p | xe -v -s make 69 | .Pp 70 | Make a sound when a download is done: 71 | .Pp 72 | .Dl % rwc ~/Downloads | xe -s 'mpv ~/.sounds/bing.wav' 73 | .Sh SEE ALSO 74 | .Xr entr 1 , 75 | .Xr inotifywatch 1 , 76 | .Xr wendy 1 77 | .Sh AUTHORS 78 | .An Leah Neukirchen Aq Mt leah@vuxu.org 79 | .Sh CAVEATS 80 | .Nm 81 | is limited by some restrictions of 82 | .Xr inotify 7 . 83 | You can only watch files and directories you can read, 84 | and the amount of inotify descriptors is limited. 85 | Watching directories is not recursive. 86 | .Pp 87 | .Nm 88 | only uses one watch descriptor per directory, 89 | and filters file names itself. 90 | This allows tracking files which get safely written by 91 | .Xr unlink 2 92 | and 93 | .Xr rename 2 , 94 | and also watching files which don't exist yet. 95 | .Pp 96 | Many tools like to create temporary files in their working directory, 97 | which may distort the output. 98 | .Sh LICENSE 99 | .Nm 100 | is in the public domain. 101 | .Pp 102 | To the extent possible under law, 103 | the creator of this work 104 | has waived all copyright and related or 105 | neighboring rights to this work. 106 | .Pp 107 | .Lk http://creativecommons.org/publicdomain/zero/1.0/ 108 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | RWC(1) General Commands Manual RWC(1) 2 | 3 | NAME 4 | rwc – report when changed 5 | 6 | SYNOPSIS 7 | rwc [-0cdep] [path ...] 8 | 9 | DESCRIPTION 10 | rwc takes a list of files or directories, watches them using inotify(7), 11 | and prints each file name as absolute path when it changed. If path is a 12 | single dash (‘-’) or absent, rwc reads file names from the standard 13 | input. 14 | 15 | Watching a directory will result in watching all changes to files which 16 | resides directly in that directory. 17 | 18 | The options are as follows: 19 | 20 | -0 Read input filenames seperated by NUL bytes. Likewise, output 21 | filenames seperated by NUL bytes. 22 | 23 | -c Detect all file creations, including open(2) with O_CREAT, 24 | mkdir(2), link(2), symlink(2), and bind(2). In this case, 25 | created files are prefixed by ‘+ ’ (that is, a plus and a space). 26 | This option has the side-effect of printing files twice that are 27 | created and immediately changed after. 28 | 29 | -d Also detect file deletion. In this case, deleted files are 30 | prefixed by ‘- ’ (that is, a dash and a space). 31 | 32 | -e Exit after the first reported change. 33 | 34 | -p Pipe mode; don't report changes while the standard output pipe is 35 | not empty. Use this to pipe rwc to programs which read standard 36 | input slowly. 37 | 38 | EXIT STATUS 39 | The rwc utility exits 0 on success, and >0 if an error occurs. 40 | 41 | EXAMPLES 42 | Watch all source files and run make(1) when something changes: 43 | 44 | % git ls-files | rwc -p | xe -v -s make 45 | 46 | Make a sound when a download is done: 47 | 48 | % rwc ~/Downloads | xe -s 'mpv ~/.sounds/bing.wav' 49 | 50 | SEE ALSO 51 | entr(1), inotifywatch(1), wendy(1) 52 | 53 | AUTHORS 54 | Leah Neukirchen 55 | 56 | CAVEATS 57 | rwc is limited by some restrictions of inotify(7). You can only watch 58 | files and directories you can read, and the amount of inotify descriptors 59 | is limited. Watching directories is not recursive. 60 | 61 | rwc only uses one watch descriptor per directory, and filters file names 62 | itself. This allows tracking files which get safely written by unlink(2) 63 | and rename(2), and also watching files which don't exist yet. 64 | 65 | Many tools like to create temporary files in their working directory, 66 | which may distort the output. 67 | 68 | LICENSE 69 | rwc is in the public domain. 70 | 71 | To the extent possible under law, the creator of this work has waived all 72 | copyright and related or neighboring rights to this work. 73 | 74 | http://creativecommons.org/publicdomain/zero/1.0/ 75 | 76 | Void Linux January 3, 2021 Void Linux 77 | -------------------------------------------------------------------------------- /rwc.c: -------------------------------------------------------------------------------- 1 | /* 2 | * rwc [-0cdep] [PATH...] - report when changed 3 | * -0 use NUL instead of newline for input/output separator 4 | * -c detect all creations (open/O_CREAT, mkdir, link, symlink, bind) 5 | * -d detect deletions too (prefixed with "- ") 6 | * -e exit after the first reported change 7 | * -p pipe mode, don't generate new events if stdout pipe is not empty 8 | * 9 | * To the extent possible under law, Leah Neukirchen 10 | * has waived all copyright and related or neighboring rights to this work. 11 | * http://creativecommons.org/publicdomain/zero/1.0/ 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | char *argv0; 28 | char ibuf[8192]; 29 | int ifd; 30 | 31 | int cflag; 32 | int dflag; 33 | int eflag; 34 | int pflag; 35 | char input_delim = '\n'; 36 | 37 | static void *root = 0; // tree 38 | 39 | static int 40 | order(const void *a, const void *b) 41 | { 42 | return strcmp((char *)a, (char *)b); 43 | } 44 | 45 | struct wdmap { 46 | int wd; 47 | char *dir; 48 | }; 49 | 50 | static void *wds = 0; // tree 51 | 52 | static int 53 | wdorder(const void *a, const void *b) 54 | { 55 | struct wdmap *ia = (struct wdmap *)a; 56 | struct wdmap *ib = (struct wdmap *)b; 57 | 58 | if (ia->wd == ib->wd) 59 | return 0; 60 | else if (ia->wd < ib->wd) 61 | return -1; 62 | else 63 | return 1; 64 | } 65 | 66 | static char * 67 | realpath_nx(char *path) 68 | { 69 | char *r = realpath(path, 0); 70 | 71 | if (!r && errno == ENOENT) { 72 | // resolve dirname and append basename 73 | 74 | char *path2 = strdup(path); 75 | if (!path2) 76 | return 0; 77 | char *d = realpath(dirname(path2), 0); 78 | free(path2); 79 | if (!d) 80 | return 0; 81 | char *b = basename(path); 82 | size_t l = strlen(d) + 1 + strlen(b) + 1; 83 | r = malloc(l); 84 | if (!r) 85 | return 0; 86 | snprintf(r, l, "%s/%s", d, b); 87 | } 88 | 89 | return r; 90 | } 91 | 92 | static void 93 | add(char *path) 94 | { 95 | struct stat st; 96 | int wd; 97 | 98 | char *file = realpath_nx(path); 99 | if (!file) { 100 | fprintf(stderr, "%s: realpath: %s: %s\n", 101 | argv0, path, strerror(errno)); 102 | return; 103 | } 104 | 105 | char *dir = file; 106 | 107 | tsearch(strdup(file), &root, order); 108 | 109 | // assume non-existing files are regular files 110 | if (lstat(file, &st) < 0 || !S_ISDIR(st.st_mode)) 111 | dir = dirname(file); 112 | 113 | wd = inotify_add_watch(ifd, dir, 114 | IN_MOVED_TO | IN_CLOSE_WRITE | cflag | dflag); 115 | if (wd < 0) { 116 | fprintf(stderr, "%s: inotify_add_watch: %s: %s\n", 117 | argv0, dir, strerror(errno)); 118 | } else { 119 | struct wdmap *newkey = malloc(sizeof (struct wdmap)); 120 | newkey->wd = wd; 121 | newkey->dir = dir; 122 | tsearch(newkey, &wds, wdorder); 123 | } 124 | } 125 | 126 | int 127 | main(int argc, char *argv[]) 128 | { 129 | int c, i; 130 | char *line = 0; 131 | 132 | argv0 = argv[0]; 133 | 134 | while ((c = getopt(argc, argv, "0cdep")) != -1) 135 | switch (c) { 136 | case '0': input_delim = 0; break; 137 | case 'c': cflag = IN_CREATE; break; 138 | case 'd': dflag = IN_DELETE|IN_DELETE_SELF|IN_MOVED_FROM; break; 139 | case 'e': eflag++; break; 140 | case 'p': pflag++; break; 141 | default: 142 | fprintf(stderr, "Usage: %s [-0cdep] [PATH...]\n", argv0); 143 | exit(2); 144 | } 145 | 146 | ifd = inotify_init(); 147 | if (ifd < 0) { 148 | fprintf(stderr, "%s: inotify_init: %s\n", 149 | argv0, strerror(errno)); 150 | exit(111); 151 | } 152 | 153 | i = optind; 154 | if (optind == argc) 155 | goto from_stdin; 156 | for (; i < argc; i++) { 157 | if (strcmp(argv[i], "-") != 0) { 158 | add(argv[i]); 159 | continue; 160 | } 161 | from_stdin: 162 | while (1) { 163 | size_t linelen = 0; 164 | ssize_t rd; 165 | 166 | errno = 0; 167 | rd = getdelim(&line, &linelen, input_delim, stdin); 168 | if (rd == -1) { 169 | if (errno != 0) 170 | return -1; 171 | break; 172 | } 173 | 174 | if (rd > 0 && line[rd-1] == input_delim) 175 | line[rd-1] = 0; // strip delimiter 176 | 177 | add(line); 178 | } 179 | } 180 | free(line); 181 | 182 | while (1) { 183 | ssize_t len, i; 184 | struct inotify_event *ev; 185 | 186 | len = read(ifd, ibuf, sizeof ibuf); 187 | if (len <= 0) { 188 | fprintf(stderr, "%s: error reading inotify buffer: %s", 189 | argv0, strerror(errno)); 190 | exit(1); 191 | } 192 | 193 | for (i = 0; i < len; i += sizeof (*ev) + ev->len) { 194 | ev = (struct inotify_event *)(ibuf + i); 195 | 196 | if (ev->mask & IN_IGNORED) 197 | continue; 198 | 199 | struct wdmap key, **result; 200 | key.wd = ev->wd; 201 | key.dir = 0; 202 | result = tfind(&key, &wds, wdorder); 203 | if (!result) 204 | continue; 205 | 206 | char *dir = (*result)->dir; 207 | char fullpath[PATH_MAX]; 208 | char *name = ev->name; 209 | if (strcmp(dir, ".") != 0) { 210 | snprintf(fullpath, sizeof fullpath, "%s/%s", 211 | dir, ev->name); 212 | name = fullpath; 213 | } 214 | 215 | if (tfind(name, &root, order) || 216 | tfind(dir, &root, order)) { 217 | if (pflag) { 218 | int n; 219 | ioctl(1, FIONREAD, &n); 220 | if (n > 0) 221 | break; 222 | } 223 | const char *mark = ""; 224 | if (ev->mask & (IN_DELETE | IN_MOVED_FROM)) 225 | mark = "- "; 226 | else if (ev->mask & IN_CREATE) 227 | mark = "+ "; 228 | printf("%s%s%c", mark, name, input_delim); 229 | fflush(stdout); 230 | if (eflag) 231 | exit(0); 232 | } 233 | } 234 | } 235 | 236 | return 0; 237 | } 238 | --------------------------------------------------------------------------------