xref: /netbsd-src/usr.bin/qsubst/qsubst.c (revision 1ffa7b76c40339c17a0fb2a09fac93f287cfc046)
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@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