1/*	$NetBSD: qsubst.c,v 1.7 2004/01/05 23:23:36 jmmv 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.7 2004/01/05 23:23:36 jmmv 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
118extern const char *__progname;
119
120#define MAX_C_A 10
121#define MAX_C_B 10
122#define BUF_SIZ 1024
123
124static int debugging;
125static FILE *tempf;
126static long tbeg;
127static FILE *workf;
128static char *str1;
129static char *str2;
130static int s1l;
131static int s2l;
132static long nls[MAX_C_A + 1];
133static char buf[(BUF_SIZ * 2) + 2];
134static char *bufp;
135static char *bufp0;
136static char *bufpmax;
137static int rahead;
138static int cabove;
139static int cbelow;
140static int wordmode;
141static int flying;
142static int flystate;
143static int allfly;
144static const char *nullstr = "";
145static int ul_;
146static char *current_file;
147static const char *beginul;
148static const char *endul;
149static char tcp_buf[1024];
150static char cap_buf[1024];
151static struct termios orig_tio;
152
153static void
154tstp_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 */
167static void
168sigtstp(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
181static void
182limit_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
192static int
193issymchar(unsigned char c)
194{
195	return (isascii(c) && (isalnum(c) || (c == '_') || (c == '$')));
196}
197
198static int
199foundit(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
210static int
211putcharf(int c)
212{
213	return (putchar(c));
214}
215
216static void
217put_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
230static int
231getc_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
256static int
257doit(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
353static void
354add_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
365static void
366process_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
483static void
484process_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
501int
502main(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