qsubst.c revision 1.5 1 /* $NetBSD: qsubst.c,v 1.5 2002/12/08 21:29:27 perry Exp $ */
2
3 /*
4 * qsubst -- designed for renaming routines existing in a whole bunch
5 * of files. Needs -ltermcap.
6 *
7 * Usage:
8 *
9 * qsubst str1 str2 [ options ]
10 *
11 * qsubst reads its options (see below) to get a list of files. For
12 * each file on this list, it then replaces str1 with str2 wherever
13 * possible in that file, depending on user input (see below). The
14 * result is written back onto the original file.
15 *
16 * For each possible substitution, the user is prompted with a few
17 * lines before and after the line containing the string to be
18 * substituted. The string itself is displayed using the terminal's
19 * standout mode, if any. Then one character is read from the
20 * terminal. This is then interpreted as follows (this is designed to
21 * be like Emacs' query-replace-string):
22 *
23 * space replace this occurrence and go on to the next one
24 * . replace this occurrence and don't change any more in
25 * this file (ie, go on to the next file).
26 * , tentatively replace this occurrence. The lines as they
27 * would look if the substitution were made are printed
28 * out. Then another character is read and it is used to
29 * decide the result (possibly undoing the tentative
30 * replacement).
31 * n don't change this one, but go on to the next one
32 * ^G don't change this one or any others in this file, but
33 * instead go on to the next file.
34 * ! change the rest in this file without asking, then go on
35 * to the next file (at which point qsubst will start
36 * asking again).
37 * ? print out the current filename and ask again.
38 *
39 * The first two arguments to qsubst are always the string to replace
40 * and the string to replace it with. The options are as follows:
41 *
42 * -w The search string is considered as a C symbol; it must
43 * be bounded by non-symbol characters. This option
44 * toggles. (`w' for `word'.)
45 * -! Enter ! mode automatically at the beginning of each
46 * file.
47 * -go Same as -!
48 * -noask Same as -!
49 * -nogo Negate -go
50 * -ask Negate -noask (same as -nogo)
51 * -cN (N is a number) Give N lines of context above and below
52 * the line with the match when prompting the user.
53 * -CAN (N is a number) Give N lines of context above the line
54 * with the match when prompting the user.
55 * -CBN (N is a number) Give N lines of context below the line
56 * with the match when prompting the user.
57 * -f filename
58 * The filename following the -f argument is one of the
59 * files qsubst should perform substitutions in.
60 * -F filename
61 * qsubst should read the named file to get the names of
62 * files to perform substitutions in. The names should
63 * appear one to a line.
64 *
65 * The default amount of context is -c2, that is, two lines above and
66 * two lines below the line with the match.
67 *
68 * Arguments not beginning with a - sign in the options field are
69 * implicitly preceded by -f. Thus, -f is really needed only when the
70 * file name begins with a - sign.
71 *
72 * qsubst reads its options in order and processes files as it gets
73 * them. This means, for example, that a -go will affect only files
74 * from -f or -F options appearing after the -go option.
75 *
76 * The most context you can get is ten lines each, above and below
77 * (corresponding to -c10).
78 *
79 * Str1 is limited to 512 characters; there is no limit on the size of
80 * str2. Neither one may contain a NUL.
81 *
82 * NULs in the file may cause qsubst to make various mistakes.
83 *
84 * If any other program modifies the file while qsubst is running, all
85 * bets are off.
86 *
87 * This program is in the public domain. Anyone may use it in any way
88 * for any purpose. Of course, it's also up to you to determine
89 * whether what it does is suitable for you; the above comments may
90 * help, but I can't promise they're accurate. It's free, and you get
91 * what you pay for.
92 *
93 * If you find any bugs I would appreciate hearing about them,
94 * especially if you also fix them.
95 *
96 * der Mouse
97 *
98 * mouse (at) rodents.montreal.qc.ca
99 */
100
101 #include <sys/file.h>
102
103 #include <ctype.h>
104 #include <errno.h>
105 #include <signal.h>
106 #include <stdio.h>
107 #include <stdlib.h>
108 #include <strings.h>
109 #include <termcap.h>
110 #include <termios.h>
111 #include <unistd.h>
112
113 extern const char *__progname;
114
115 #define MAX_C_A 10
116 #define MAX_C_B 10
117 #define BUF_SIZ 1024
118
119 static int debugging;
120 static FILE *tempf;
121 static long tbeg;
122 static FILE *workf;
123 static char *str1;
124 static char *str2;
125 static int s1l;
126 static int s2l;
127 static long nls[MAX_C_A + 1];
128 static char buf[(BUF_SIZ * 2) + 2];
129 static char *bufp;
130 static char *bufp0;
131 static char *bufpmax;
132 static int rahead;
133 static int cabove;
134 static int cbelow;
135 static int wordmode;
136 static int flying;
137 static int flystate;
138 static int allfly;
139 static const char *nullstr = "";
140 static int ul_;
141 static char *current_file;
142 static const char *beginul;
143 static const char *endul;
144 static char tcp_buf[1024];
145 static char cap_buf[1024];
146 static struct termios orig_tio;
147
148 static void
149 tstp_self(void)
150 {
151 void (*old_tstp) (int);
152 int mask;
153
154 mask = sigblock(0);
155 kill(getpid(), SIGTSTP);
156 old_tstp = signal(SIGTSTP, SIG_DFL);
157 sigsetmask(mask & ~sigmask(SIGTSTP));
158 signal(SIGTSTP, old_tstp);
159 }
160
161 /* ARGSUSED */
162 static void
163 sigtstp(int sig)
164 {
165 struct termios tio;
166
167 if (tcgetattr(0, &tio) < 0) {
168 tstp_self();
169 return;
170 }
171 tcsetattr(0, TCSAFLUSH | TCSASOFT, &orig_tio);
172 tstp_self();
173 tcsetattr(0, TCSADRAIN | TCSASOFT, &tio);
174 }
175
176 static void
177 limit_above_below(void)
178 {
179 if (cabove > MAX_C_A) {
180 cabove = MAX_C_A;
181 }
182 if (cbelow > MAX_C_B) {
183 cbelow = MAX_C_B;
184 }
185 }
186
187 static int
188 issymchar(char c)
189 {
190 return (isascii(c) && (isalnum(c) || (c == '_') || (c == '$')));
191 }
192
193 static int
194 foundit(void)
195 {
196 if (wordmode) {
197 return (!issymchar(bufp[-1]) &&
198 !issymchar(bufp[-2 - s1l]) &&
199 !bcmp(bufp - 1 - s1l, str1, s1l));
200 } else {
201 return (!bcmp(bufp - s1l, str1, s1l));
202 }
203 }
204
205 static int
206 putcharf(int c)
207 {
208 return (putchar(c));
209 }
210
211 static void
212 put_ul(char *s)
213 {
214 if (ul_) {
215 for (; *s; s++) {
216 printf("_\b%c", *s);
217 }
218 } else {
219 tputs(beginul, 1, putcharf);
220 fputs(s, stdout);
221 tputs(endul, 1, putcharf);
222 }
223 }
224
225 static int
226 getc_cbreak(void)
227 {
228 struct termios tio;
229 struct termios otio;
230 char c;
231
232 if (tcgetattr(0, &tio) < 0)
233 return (getchar());
234 otio = tio;
235 tio.c_lflag &= ~(ICANON | ECHOKE | ECHOE | ECHO | ECHONL);
236 tio.c_cc[VMIN] = 1;
237 tio.c_cc[VTIME] = 0;
238 tcsetattr(0, TCSANOW | TCSASOFT, &tio);
239 switch (read(0, &c, 1)) {
240 case -1:
241 break;
242 case 0:
243 break;
244 case 1:
245 break;
246 }
247 tcsetattr(0, TCSANOW | TCSASOFT, &otio);
248 return (c);
249 }
250
251 static int
252 doit(void)
253 {
254 long save;
255 int i;
256 int lastnl;
257 int use_replacement;
258
259 if (flying) {
260 return (flystate);
261 }
262 use_replacement = 0;
263 save = ftell(workf);
264 do {
265 for (i = MAX_C_A - cabove; nls[i] < 0; i++);
266 fseek(workf, nls[i], 0);
267 for (i = save - nls[i] - rahead; i; i--) {
268 putchar(getc(workf));
269 }
270 put_ul(use_replacement ? str2 : str1);
271 fseek(workf, save + s1l - rahead, 0);
272 lastnl = 0;
273 i = cbelow + 1;
274 while (i > 0) {
275 int c;
276 c = getc(workf);
277 if (c == EOF) {
278 clearerr(workf);
279 break;
280 }
281 putchar(c);
282 lastnl = 0;
283 if (c == '\n') {
284 i--;
285 lastnl = 1;
286 }
287 }
288 if (!lastnl)
289 printf("\n[no final newline] ");
290 fseek(workf, save, 0);
291 i = -1;
292 while (i == -1) {
293 switch (getc_cbreak()) {
294 case ' ':
295 i = 1;
296 break;
297 case '.':
298 i = 1;
299 flying = 1;
300 flystate = 0;
301 break;
302 case 'n':
303 i = 0;
304 break;
305 case '\7':
306 i = 0;
307 flying = 1;
308 flystate = 0;
309 break;
310 case '!':
311 i = 1;
312 flying = 1;
313 flystate = 1;
314 break;
315 case ',':
316 use_replacement = !use_replacement;
317 i = -2;
318 printf("(using %s string gives)\n",
319 use_replacement ? "new" : "old");
320 break;
321 case '?':
322 printf("File is `%s'\n", current_file);
323 break;
324 default:
325 putchar('\7');
326 break;
327 }
328 }
329 } while (i < 0);
330 if (i) {
331 printf("(replacing");
332 } else {
333 printf("(leaving");
334 }
335 if (flying) {
336 if (flystate == i) {
337 printf(" this and all the rest");
338 } else if (flystate) {
339 printf(" this, replacing all the rest");
340 } else {
341 printf(" this, leaving all the rest");
342 }
343 }
344 printf(")\n");
345 return (i);
346 }
347
348 static void
349 add_shift(long *a, long e, int n)
350 {
351 int i;
352
353 n--;
354 for (i = 0; i < n; i++) {
355 a[i] = a[i + 1];
356 }
357 a[n] = e;
358 }
359
360 static void
361 process_file(char *fn)
362 {
363 int i;
364 long n;
365 int c;
366
367 workf = fopen(fn, "r+");
368 if (workf == NULL) {
369 fprintf(stderr, "%s: cannot read %s\n", __progname, fn);
370 return;
371 }
372 printf("(file: %s)\n", fn);
373 current_file = fn;
374 for (i = 0; i <= MAX_C_A; i++) {
375 nls[i] = -1;
376 }
377 nls[MAX_C_A] = 0;
378 tbeg = -1;
379 if (wordmode) {
380 bufp0 = &buf[1];
381 rahead = s1l + 1;
382 buf[0] = '\0';
383 } else {
384 bufp0 = &buf[0];
385 rahead = s1l;
386 }
387 if (debugging) {
388 printf("[rahead = %d, bufp0-buf = %ld]\n",
389 rahead, (long) (bufp0 - &buf[0]));
390 }
391 n = 0;
392 bufp = bufp0;
393 bufpmax = &buf[sizeof(buf) - s1l - 2];
394 flying = allfly;
395 flystate = 1;
396 while (1) {
397 c = getc(workf);
398 if (c == EOF) {
399 if (tbeg >= 0) {
400 if (bufp > bufp0)
401 fwrite(bufp0, 1, bufp - bufp0, tempf);
402 fseek(workf, tbeg, 0);
403 n = ftell(tempf);
404 fseek(tempf, 0L, 0);
405 for (; n; n--) {
406 putc(getc(tempf), workf);
407 }
408 fflush(workf);
409 ftruncate(fileno(workf), ftell(workf));
410 }
411 fclose(workf);
412 return;
413 }
414 *bufp++ = c;
415 n++;
416 if (debugging) {
417 printf("[got %c, n now %ld, bufp-buf %ld]\n",
418 c, n, (long) (bufp - bufp0));
419 }
420 if ((n >= rahead) && foundit() && doit()) {
421 int wbehind;
422 if (debugging) {
423 printf("[doing change]\n");
424 }
425 wbehind = 1;
426 if (tbeg < 0) {
427 tbeg = ftell(workf) - rahead;
428 fseek(tempf, 0L, 0);
429 if (debugging) {
430 printf("[tbeg set to %d]\n",
431 (int)tbeg);
432 }
433 wbehind = 0;
434 }
435 if (bufp[-1] == '\n')
436 add_shift(nls, ftell(workf), MAX_C_A + 1);
437 if ((n > rahead) && wbehind) {
438 fwrite(bufp0, 1, n - rahead, tempf);
439 if (debugging) {
440 printf("[writing %ld from bufp0]\n",
441 n - rahead);
442 }
443 }
444 fwrite(str2, 1, s2l, tempf);
445 n = rahead - s1l;
446 if (debugging) {
447 printf("[n now %ld]\n", n);
448 }
449 if (n > 0) {
450 bcopy(bufp - n, bufp0, n);
451 if (debugging) {
452 printf("[copying %ld back]\n", n);
453 }
454 }
455 bufp = bufp0 + n;
456 } else {
457 if (bufp[-1] == '\n')
458 add_shift(nls, ftell(workf), MAX_C_A + 1);
459 if (bufp >= bufpmax) {
460 if (tbeg >= 0) {
461 fwrite(bufp0, 1, n - rahead, tempf);
462 if (debugging) {
463 printf("[flushing %ld]\n",
464 n - rahead);
465 }
466 }
467 n = rahead;
468 bcopy(bufp - n, bufp0, n);
469 if (debugging) {
470 printf("[n now %ld]\n[copying %ld back]\n", n, n);
471 }
472 bufp = bufp0 + n;
473 }
474 }
475 }
476 }
477
478 static void
479 process_indir_file(char *fn)
480 {
481 char newfn[1024];
482 FILE *f;
483
484 f = fopen(fn, "r");
485 if (f == NULL) {
486 fprintf(stderr, "%s: cannot read %s\n", __progname, fn);
487 return;
488 }
489 while (fgets(newfn, sizeof(newfn), f) == newfn) {
490 newfn[strlen(newfn) - 1] = '\0';
491 process_file(newfn);
492 }
493 fclose(f);
494 }
495
496 int
497 main(int ac, char **av)
498 {
499 int skip;
500 char *cp;
501
502 if (ac < 3) {
503 fprintf(stderr, "Usage: %s str1 str2 [ -w -! -noask -go -f file -F file ]\n",
504 __progname);
505 exit(1);
506 }
507 cp = getenv("TERM");
508 if (cp == 0) {
509 beginul = nullstr;
510 endul = nullstr;
511 } else {
512 if (tgetent(tcp_buf, cp) != 1) {
513 beginul = nullstr;
514 endul = nullstr;
515 } else {
516 cp = cap_buf;
517 if (tgetflag("os") || tgetflag("ul")) {
518 ul_ = 1;
519 } else {
520 ul_ = 0;
521 beginul = tgetstr("us", &cp);
522 if (beginul == 0) {
523 beginul = tgetstr("so", &cp);
524 if (beginul == 0) {
525 beginul = nullstr;
526 endul = nullstr;
527 } else {
528 endul = tgetstr("se", &cp);
529 }
530 } else {
531 endul = tgetstr("ue", &cp);
532 }
533 }
534 }
535 }
536 {
537 static char tmp[] = "/tmp/qsubst.XXXXXX";
538 int fd;
539 fd = mkstemp(&tmp[0]);
540 if (fd < 0) {
541 fprintf(stderr, "%s: cannot create temp file: %s\n",
542 __progname, strerror(errno));
543 exit(1);
544 }
545 tempf = fdopen(fd, "w+");
546 }
547 if ((access(av[1], R_OK | W_OK) == 0) &&
548 (access(av[ac - 1], R_OK | W_OK) < 0) &&
549 (access(av[ac - 2], R_OK | W_OK) < 0)) {
550 fprintf(stderr, "%s: argument order has changed, it's now: str1 str2 files...\n", __progname);
551 }
552 str1 = av[1];
553 str2 = av[2];
554 av += 2;
555 ac -= 2;
556 s1l = strlen(str1);
557 s2l = strlen(str2);
558 if (s1l > BUF_SIZ) {
559 fprintf(stderr, "%s: search string too long (max %d chars)\n",
560 __progname, BUF_SIZ);
561 exit(1);
562 }
563 tcgetattr(0, &orig_tio);
564 signal(SIGTSTP, sigtstp);
565 allfly = 0;
566 cabove = 2;
567 cbelow = 2;
568 skip = 0;
569 for (ac--, av++; ac; ac--, av++) {
570 if (skip > 0) {
571 skip--;
572 continue;
573 }
574 if (**av == '-') {
575 ++*av;
576 if (!strcmp(*av, "debug")) {
577 debugging++;
578 } else if (!strcmp(*av, "w")) {
579 wordmode = !wordmode;
580 } else if ((strcmp(*av, "!") == 0) ||
581 (strcmp(*av, "go") == 0) ||
582 (strcmp(*av, "noask") == 0)) {
583 allfly = 1;
584 } else if ((strcmp(*av, "nogo") == 0) ||
585 (strcmp(*av, "ask") == 0)) {
586 allfly = 0;
587 } else if (**av == 'c') {
588 cabove = atoi(++*av);
589 cbelow = cabove;
590 limit_above_below();
591 } else if (**av == 'C') {
592 ++*av;
593 if (**av == 'A') {
594 cabove = atoi(++*av);
595 limit_above_below();
596 } else if (**av == 'B') {
597 cbelow = atoi(++*av);
598 limit_above_below();
599 } else {
600 fprintf(stderr, "%s: -C must be -CA or -CB\n", __progname);
601 }
602 } else if ((strcmp(*av, "f") == 0) ||
603 (strcmp(*av, "F") == 0)) {
604 if (++skip >= ac) {
605 fprintf(stderr, "%s: -%s what?\n",
606 __progname, *av);
607 } else {
608 if (**av == 'f') {
609 process_file(av[skip]);
610 } else {
611 process_indir_file(av[skip]);
612 }
613 }
614 }
615 } else {
616 process_file(*av);
617 }
618 }
619 exit(0);
620 }
621