xref: /netbsd-src/usr.bin/qsubst/qsubst.c (revision 447bb3096168793a37af74a0a026e8eaea058438)
1 /*	$NetBSD: qsubst.c,v 1.8 2004/11/01 21:36:11 dsl 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 #include <sys/cdefs.h>
101 
102 #ifndef lint
103 __RCSID("$NetBSD: qsubst.c,v 1.8 2004/11/01 21:36:11 dsl Exp $");
104 #endif
105 
106 #include <sys/file.h>
107 
108 #include <ctype.h>
109 #include <errno.h>
110 #include <signal.h>
111 #include <stdio.h>
112 #include <stdlib.h>
113 #include <strings.h>
114 #include <termcap.h>
115 #include <termios.h>
116 #include <unistd.h>
117 
118 extern const char *__progname;
119 
120 #define MAX_C_A 10
121 #define MAX_C_B 10
122 #define BUF_SIZ 1024
123 
124 static int debugging;
125 static FILE *tempf;
126 static long tbeg;
127 static FILE *workf;
128 static char *str1;
129 static char *str2;
130 static int s1l;
131 static int s2l;
132 static long nls[MAX_C_A + 1];
133 static char buf[(BUF_SIZ * 2) + 2];
134 static char *bufp;
135 static char *bufp0;
136 static char *bufpmax;
137 static int rahead;
138 static int cabove;
139 static int cbelow;
140 static int wordmode;
141 static int flying;
142 static int flystate;
143 static int allfly;
144 static const char *nullstr = "";
145 static int ul_;
146 static char *current_file;
147 static const char *beginul;
148 static const char *endul;
149 static char tcp_buf[1024];
150 static char cap_buf[1024];
151 static struct termios orig_tio;
152 
153 static void
tstp_self(void)154 tstp_self(void)
155 {
156 	void (*old_tstp) (int);
157 	int mask;
158 
159 	mask = sigblock(0);
160 	kill(getpid(), SIGTSTP);
161 	old_tstp = signal(SIGTSTP, SIG_DFL);
162 	sigsetmask(mask & ~sigmask(SIGTSTP));
163 	signal(SIGTSTP, old_tstp);
164 }
165 
166 /* ARGSUSED */
167 static void
sigtstp(int sig)168 sigtstp(int sig)
169 {
170 	struct termios tio;
171 
172 	if (tcgetattr(0, &tio) < 0) {
173 		tstp_self();
174 		return;
175 	}
176 	tcsetattr(0, TCSAFLUSH | TCSASOFT, &orig_tio);
177 	tstp_self();
178 	tcsetattr(0, TCSADRAIN | TCSASOFT, &tio);
179 }
180 
181 static void
limit_above_below(void)182 limit_above_below(void)
183 {
184 	if (cabove > MAX_C_A) {
185 		cabove = MAX_C_A;
186 	}
187 	if (cbelow > MAX_C_B) {
188 		cbelow = MAX_C_B;
189 	}
190 }
191 
192 static int
issymchar(unsigned char c)193 issymchar(unsigned char c)
194 {
195 	return (isascii(c) && (isalnum(c) || (c == '_') || (c == '$')));
196 }
197 
198 static int
foundit(void)199 foundit(void)
200 {
201 	if (wordmode) {
202 		return (!issymchar(bufp[-1]) &&
203 		    !issymchar(bufp[-2 - s1l]) &&
204 		    !bcmp(bufp - 1 - s1l, str1, s1l));
205 	} else {
206 		return (!bcmp(bufp - s1l, str1, s1l));
207 	}
208 }
209 
210 static int
putcharf(int c)211 putcharf(int c)
212 {
213 	return (putchar(c));
214 }
215 
216 static void
put_ul(char * s)217 put_ul(char *s)
218 {
219 	if (ul_) {
220 		for (; *s; s++) {
221 			printf("_\b%c", *s);
222 		}
223 	} else {
224 		tputs(beginul, 1, putcharf);
225 		fputs(s, stdout);
226 		tputs(endul, 1, putcharf);
227 	}
228 }
229 
230 static int
getc_cbreak(void)231 getc_cbreak(void)
232 {
233 	struct termios tio;
234 	struct termios otio;
235 	char c;
236 
237 	if (tcgetattr(0, &tio) < 0)
238 		return (getchar());
239 	otio = tio;
240 	tio.c_lflag &= ~(ICANON | ECHOKE | ECHOE | ECHO | ECHONL);
241 	tio.c_cc[VMIN] = 1;
242 	tio.c_cc[VTIME] = 0;
243 	tcsetattr(0, TCSANOW | TCSASOFT, &tio);
244 	switch (read(0, &c, 1)) {
245 	case -1:
246 		break;
247 	case 0:
248 		break;
249 	case 1:
250 		break;
251 	}
252 	tcsetattr(0, TCSANOW | TCSASOFT, &otio);
253 	return (c);
254 }
255 
256 static int
doit(void)257 doit(void)
258 {
259 	long save;
260 	int i;
261 	int lastnl;
262 	int use_replacement;
263 
264 	if (flying) {
265 		return (flystate);
266 	}
267 	use_replacement = 0;
268 	save = ftell(workf);
269 	do {
270 		for (i = MAX_C_A - cabove; nls[i] < 0; i++);
271 		fseek(workf, nls[i], 0);
272 		for (i = save - nls[i] - rahead; i; i--) {
273 			putchar(getc(workf));
274 		}
275 		put_ul(use_replacement ? str2 : str1);
276 		fseek(workf, save + s1l - rahead, 0);
277 		lastnl = 0;
278 		i = cbelow + 1;
279 		while (i > 0) {
280 			int c;
281 			c = getc(workf);
282 			if (c == EOF) {
283 				clearerr(workf);
284 				break;
285 			}
286 			putchar(c);
287 			lastnl = 0;
288 			if (c == '\n') {
289 				i--;
290 				lastnl = 1;
291 			}
292 		}
293 		if (!lastnl)
294 			printf("\n[no final newline] ");
295 		fseek(workf, save, 0);
296 		i = -1;
297 		while (i == -1) {
298 			switch (getc_cbreak()) {
299 			case ' ':
300 				i = 1;
301 				break;
302 			case '.':
303 				i = 1;
304 				flying = 1;
305 				flystate = 0;
306 				break;
307 			case 'n':
308 				i = 0;
309 				break;
310 			case '\7':
311 				i = 0;
312 				flying = 1;
313 				flystate = 0;
314 				break;
315 			case '!':
316 				i = 1;
317 				flying = 1;
318 				flystate = 1;
319 				break;
320 			case ',':
321 				use_replacement = !use_replacement;
322 				i = -2;
323 				printf("(using %s string gives)\n",
324 				    use_replacement ? "new" : "old");
325 				break;
326 			case '?':
327 				printf("File is `%s'\n", current_file);
328 				break;
329 			default:
330 				putchar('\7');
331 				break;
332 			}
333 		}
334 	} while (i < 0);
335 	if (i) {
336 		printf("(replacing");
337 	} else {
338 		printf("(leaving");
339 	}
340 	if (flying) {
341 		if (flystate == i) {
342 			printf(" this and all the rest");
343 		} else if (flystate) {
344 			printf(" this, replacing all the rest");
345 		} else {
346 			printf(" this, leaving all the rest");
347 		}
348 	}
349 	printf(")\n");
350 	return (i);
351 }
352 
353 static void
add_shift(long * a,long e,int n)354 add_shift(long *a, long e, int n)
355 {
356 	int i;
357 
358 	n--;
359 	for (i = 0; i < n; i++) {
360 		a[i] = a[i + 1];
361 	}
362 	a[n] = e;
363 }
364 
365 static void
process_file(char * fn)366 process_file(char *fn)
367 {
368 	int i;
369 	long n;
370 	int c;
371 
372 	workf = fopen(fn, "r+");
373 	if (workf == NULL) {
374 		fprintf(stderr, "%s: cannot read %s\n", __progname, fn);
375 		return;
376 	}
377 	printf("(file: %s)\n", fn);
378 	current_file = fn;
379 	for (i = 0; i <= MAX_C_A; i++) {
380 		nls[i] = -1;
381 	}
382 	nls[MAX_C_A] = 0;
383 	tbeg = -1;
384 	if (wordmode) {
385 		bufp0 = &buf[1];
386 		rahead = s1l + 1;
387 		buf[0] = '\0';
388 	} else {
389 		bufp0 = &buf[0];
390 		rahead = s1l;
391 	}
392 	if (debugging) {
393 		printf("[rahead = %d, bufp0-buf = %ld]\n",
394 		    rahead, (long) (bufp0 - &buf[0]));
395 	}
396 	n = 0;
397 	bufp = bufp0;
398 	bufpmax = &buf[sizeof(buf) - s1l - 2];
399 	flying = allfly;
400 	flystate = 1;
401 	while (1) {
402 		c = getc(workf);
403 		if (c == EOF) {
404 			if (tbeg >= 0) {
405 				if (bufp > bufp0)
406 					fwrite(bufp0, 1, bufp - bufp0, tempf);
407 				fseek(workf, tbeg, 0);
408 				n = ftell(tempf);
409 				fseek(tempf, 0L, 0);
410 				for (; n; n--) {
411 					putc(getc(tempf), workf);
412 				}
413 				fflush(workf);
414 				ftruncate(fileno(workf), ftell(workf));
415 			}
416 			fclose(workf);
417 			return;
418 		}
419 		*bufp++ = c;
420 		n++;
421 		if (debugging) {
422 			printf("[got %c, n now %ld, bufp-buf %ld]\n",
423 			    c, n, (long) (bufp - bufp0));
424 		}
425 		if ((n >= rahead) && foundit() && doit()) {
426 			int wbehind;
427 			if (debugging) {
428 				printf("[doing change]\n");
429 			}
430 			wbehind = 1;
431 			if (tbeg < 0) {
432 				tbeg = ftell(workf) - rahead;
433 				fseek(tempf, 0L, 0);
434 				if (debugging) {
435 					printf("[tbeg set to %d]\n",
436 					    (int)tbeg);
437 				}
438 				wbehind = 0;
439 			}
440 			if (bufp[-1] == '\n')
441 				add_shift(nls, ftell(workf), MAX_C_A + 1);
442 			if ((n > rahead) && wbehind) {
443 				fwrite(bufp0, 1, n - rahead, tempf);
444 				if (debugging) {
445 					printf("[writing %ld from bufp0]\n",
446 					    n - rahead);
447 				}
448 			}
449 			fwrite(str2, 1, s2l, tempf);
450 			n = rahead - s1l;
451 			if (debugging) {
452 				printf("[n now %ld]\n", n);
453 			}
454 			if (n > 0) {
455 				bcopy(bufp - n, bufp0, n);
456 				if (debugging) {
457 					printf("[copying %ld back]\n", n);
458 				}
459 			}
460 			bufp = bufp0 + n;
461 		} else {
462 			if (bufp[-1] == '\n')
463 				add_shift(nls, ftell(workf), MAX_C_A + 1);
464 			if (bufp >= bufpmax) {
465 				if (tbeg >= 0) {
466 					fwrite(bufp0, 1, n - rahead, tempf);
467 					if (debugging) {
468 						printf("[flushing %ld]\n",
469 						    n - rahead);
470 					}
471 				}
472 				n = rahead;
473 				bcopy(bufp - n, bufp0, n);
474 				if (debugging) {
475 					printf("[n now %ld]\n[copying %ld back]\n", n, n);
476 				}
477 				bufp = bufp0 + n;
478 			}
479 		}
480 	}
481 }
482 
483 static void
process_indir_file(char * fn)484 process_indir_file(char *fn)
485 {
486 	char newfn[1024];
487 	FILE *f;
488 
489 	f = fopen(fn, "r");
490 	if (f == NULL) {
491 		fprintf(stderr, "%s: cannot read %s\n", __progname, fn);
492 		return;
493 	}
494 	while (fgets(newfn, sizeof(newfn), f) == newfn) {
495 		newfn[strlen(newfn) - 1] = '\0';
496 		process_file(newfn);
497 	}
498 	fclose(f);
499 }
500 
501 int
main(int ac,char ** av)502 main(int ac, char **av)
503 {
504 	int skip;
505 	char *cp;
506 
507 	if (ac < 3) {
508 		fprintf(stderr, "usage: %s str1 str2 [ -w -! -noask -go -f file -F file ]\n",
509 		    __progname);
510 		exit(1);
511 	}
512 	cp = getenv("TERM");
513 	if (cp == 0) {
514 		beginul = nullstr;
515 		endul = nullstr;
516 	} else {
517 		if (tgetent(tcp_buf, cp) != 1) {
518 			beginul = nullstr;
519 			endul = nullstr;
520 		} else {
521 			cp = cap_buf;
522 			if (tgetflag("os") || tgetflag("ul")) {
523 				ul_ = 1;
524 			} else {
525 				ul_ = 0;
526 				beginul = tgetstr("us", &cp);
527 				if (beginul == 0) {
528 					beginul = tgetstr("so", &cp);
529 					if (beginul == 0) {
530 						beginul = nullstr;
531 						endul = nullstr;
532 					} else {
533 						endul = tgetstr("se", &cp);
534 					}
535 				} else {
536 					endul = tgetstr("ue", &cp);
537 				}
538 			}
539 		}
540 	}
541 	{
542 		static char tmp[] = "/tmp/qsubst.XXXXXX";
543 		int fd;
544 		fd = mkstemp(&tmp[0]);
545 		if (fd < 0) {
546 			fprintf(stderr, "%s: cannot create temp file: %s\n",
547 			    __progname, strerror(errno));
548 			exit(1);
549 		}
550 		tempf = fdopen(fd, "w+");
551 	}
552 	if ((access(av[1], R_OK | W_OK) == 0) &&
553 	    (access(av[ac - 1], R_OK | W_OK) < 0) &&
554 	    (access(av[ac - 2], R_OK | W_OK) < 0)) {
555 		fprintf(stderr, "%s: argument order has changed, it's now: str1 str2 files...\n", __progname);
556 	}
557 	str1 = av[1];
558 	str2 = av[2];
559 	av += 2;
560 	ac -= 2;
561 	s1l = strlen(str1);
562 	s2l = strlen(str2);
563 	if (s1l > BUF_SIZ) {
564 		fprintf(stderr, "%s: search string too long (max %d chars)\n",
565 		    __progname, BUF_SIZ);
566 		exit(1);
567 	}
568 	tcgetattr(0, &orig_tio);
569 	signal(SIGTSTP, sigtstp);
570 	allfly = 0;
571 	cabove = 2;
572 	cbelow = 2;
573 	skip = 0;
574 	for (ac--, av++; ac; ac--, av++) {
575 		if (skip > 0) {
576 			skip--;
577 			continue;
578 		}
579 		if (**av == '-') {
580 			++*av;
581 			if (!strcmp(*av, "debug")) {
582 				debugging++;
583 			} else if (!strcmp(*av, "w")) {
584 				wordmode = !wordmode;
585 			} else if ((strcmp(*av, "!") == 0) ||
586 				    (strcmp(*av, "go") == 0) ||
587 			    (strcmp(*av, "noask") == 0)) {
588 				allfly = 1;
589 			} else if ((strcmp(*av, "nogo") == 0) ||
590 			    (strcmp(*av, "ask") == 0)) {
591 				allfly = 0;
592 			} else if (**av == 'c') {
593 				cabove = atoi(++*av);
594 				cbelow = cabove;
595 				limit_above_below();
596 			} else if (**av == 'C') {
597 				++*av;
598 				if (**av == 'A') {
599 					cabove = atoi(++*av);
600 					limit_above_below();
601 				} else if (**av == 'B') {
602 					cbelow = atoi(++*av);
603 					limit_above_below();
604 				} else {
605 					fprintf(stderr, "%s: -C must be -CA or -CB\n", __progname);
606 				}
607 			} else if ((strcmp(*av, "f") == 0) ||
608 			    (strcmp(*av, "F") == 0)) {
609 				if (++skip >= ac) {
610 					fprintf(stderr, "%s: -%s what?\n",
611 					    __progname, *av);
612 				} else {
613 					if (**av == 'f') {
614 						process_file(av[skip]);
615 					} else {
616 						process_indir_file(av[skip]);
617 					}
618 				}
619 			}
620 		} else {
621 			process_file(*av);
622 		}
623 	}
624 	exit(0);
625 }
626