1 /* @(#)change.c	1.47 21/08/20 Copyright 1985, 87-90, 95-99, 2000-2021 J. Schilling */
2 #include <schily/mconfig.h>
3 #ifndef lint
4 static	UConst char sccsid[] =
5 	"@(#)change.c	1.47 21/08/20 Copyright 1985, 87-90, 95-99, 2000-2021 J. Schilling";
6 #endif
7 /*
8  *	find pattern and substitute in files
9  *
10  *	Copyright (c) 1985, 87-90, 95-99, 2000-2021 J. Schilling
11  */
12 /*
13  * The contents of this file are subject to the terms of the
14  * Common Development and Distribution License, Version 1.0 only
15  * (the "License").  You may not use this file except in compliance
16  * with the License.
17  *
18  * See the file CDDL.Schily.txt in this distribution for details.
19  * A copy of the CDDL is also available via the Internet at
20  * http://www.opensource.org/licenses/cddl1.txt
21  *
22  * When distributing Covered Code, include this CDDL HEADER in each
23  * file and include the License file CDDL.Schily.txt from this distribution.
24  */
25 
26 #include <schily/stdio.h>
27 #include <schily/varargs.h>
28 #include <schily/stdlib.h>
29 #include <schily/unistd.h>
30 #include <schily/string.h>
31 #include <schily/errno.h>
32 #include <schily/stat.h>
33 #	define	STATBUF		struct stat
34 #	define	file_ino(sp)	((sp)->st_ino)
35 #	define	file_dev(sp)	((sp)->st_dev)
36 #include <schily/signal.h>
37 #include <schily/standard.h>
38 #include <schily/patmatch.h>
39 #include <schily/utypes.h>
40 #define	GT_COMERR		/* #define comerr gtcomerr */
41 #define	GT_ERROR		/* #define error gterror   */
42 #include <schily/schily.h>
43 #include <schily/libport.h>
44 #include <schily/nlsdefs.h>
45 
46 /*
47  *	check for same file descriptor
48  */
49 #define	samefile(sp, sp2)	(file_dev(sp1) == file_dev(sp2) && file_ino(sp1) == file_ino(sp2))
50 
51 #ifndef	MAXLINE
52 #define	MAXLINE	8196
53 #endif
54 #define	LINEINCR 1024
55 #define	MAXNAME	1024
56 
57 
58 LOCAL	int	Vflag = 0;
59 LOCAL	int	Cflag = 0;
60 LOCAL	int	Iflag = 0;
61 LOCAL	int	Nflag = 0;
62 LOCAL	int	CHcnt = MAXLINE;
63 
64 LOCAL	char	*oline;
65 LOCAL	size_t	osize;
66 LOCAL	char	*newline;
67 LOCAL	size_t	newsize;
68 LOCAL	char	tmpname[MAXNAME];
69 LOCAL	int	*aux;
70 LOCAL	int	*state;
71 LOCAL	FILE	*tty;
72 LOCAL	STATBUF ostat;	/* Old stat buf */
73 LOCAL	STATBUF cstat;	/* Changed file stat buf */
74 
75 LOCAL	void	usage	__PR((int excode));
76 LOCAL	RETSIGTYPE intr	__PR((int signo));
77 EXPORT	int	main	__PR((int ac, char **av));
78 LOCAL	int	match	__PR((char *pat, int *auxp, char *linep, int off, int len, int alt));
79 LOCAL	int	change	__PR((char *name, FILE *ifile, FILE *ofile, char *pat, char *subst, int *auxp, int alt));
80 LOCAL	int	catsubst __PR((char *linep, char **newpp, int start, int end, char *subst, int idx, size_t *maxp));
81 LOCAL	int	appchar	__PR((int c, char **bufp, int off, size_t *maxp));
82 LOCAL	void	showchange __PR((char *name, char *linep, int start, int end, char *subst, int out, size_t max));
83 LOCAL	BOOL	yes	__PR((char *form, ...));
84 LOCAL	void	mktmp	__PR((char *name));
85 LOCAL	BOOL	mkbak	__PR((char *name));
86 
87 LOCAL void
usage(excode)88 usage(excode)
89 	int	excode;
90 {
91 	error("Usage:	change [options] pattern substitution [file1...filen]\n");
92 	error("Options:\n");
93 	error("	-v	Display Text before change\n");
94 	error("	-c	Display Text after change\n");
95 	error("	-i	Interactive prompting for each change\n");
96 	error("	-#	Maximum number of changes per line\n");
97 	error("	-nobak	Do not create filename.bak\n");
98 	error("	-help	Print this help.\n");
99 	error("	-version Print version number.\n");
100 	error("	Standard input will be used if no files given.\n");
101 	exit(excode);
102 	/* NOTREACHED */
103 }
104 
105 LOCAL RETSIGTYPE
intr(signo)106 intr(signo)
107 	int	signo;
108 {
109 	if (tmpname[0] != '\0')
110 		unlink(tmpname);
111 	exit(signo);
112 	/* NOTREACHED */
113 
114 	/*
115 	 * Was ist wenn RETSIGTYPE == int ?
116 	 */
117 }
118 
119 EXPORT int
main(ac,av)120 main(ac, av)
121 	int	ac;
122 	char	**av;
123 {
124 	FILE	*file;
125 	FILE	*tfile;
126 	char	*pat;
127 	int	alt;
128 	char	*subst;
129 	int	len;
130 	int	cnt;
131 	char	*opt = "help,version,v,c,i,#,nobak,n";
132 	BOOL	help = FALSE;
133 	BOOL	prversion = FALSE;
134 	BOOL	closeok;
135 	int	cac;
136 	char	* const *cav;
137 
138 	save_args(ac, av);
139 
140 	(void) setlocale(LC_ALL, "");
141 
142 #ifdef  USE_NLS
143 #if !defined(TEXT_DOMAIN)	/* Should be defined by cc -D */
144 #define	TEXT_DOMAIN "change"	/* Use this only if it weren't */
145 #endif
146 	{ char	*dir;
147 	dir = searchfileinpath("share/locale", F_OK,
148 					SIP_ANY_FILE|SIP_NO_PATH, NULL);
149 	if (dir)
150 		(void) bindtextdomain(TEXT_DOMAIN, dir);
151 	else
152 #if defined(PROTOTYPES) && defined(INS_BASE)
153 	(void) bindtextdomain(TEXT_DOMAIN, INS_BASE "/share/locale");
154 #else
155 	(void) bindtextdomain(TEXT_DOMAIN, "/usr/share/locale");
156 #endif
157 	(void) textdomain(TEXT_DOMAIN);
158 	}
159 #endif 	/* USE_NLS */
160 
161 #ifdef	SIGHUP
162 	signal(SIGHUP, intr);
163 #endif
164 #ifdef	SIGINT
165 	signal(SIGINT, intr);
166 #endif
167 #ifdef	SIGTERM
168 	signal(SIGTERM, intr);
169 #endif
170 
171 	cac = --ac;
172 	cav = ++av;
173 
174 	if (getallargs(&cac, &cav, opt,
175 					&help, &prversion,
176 					&Vflag, &Cflag, &Iflag,
177 					&CHcnt,
178 					&Nflag, &Nflag) < 0) {
179 		error("Bad flag: '%s'\n", cav[0]);
180 		usage(EX_BAD);
181 	}
182 	if (help) usage(0);
183 	if (prversion) {
184 		gtprintf("Change release %s %s (%s-%s-%s) Copyright (C) 1985, 87-90, 95-99, 2000-2021 %s\n",
185 				"1.47", "2021/08/20",
186 				HOST_CPU, HOST_VENDOR, HOST_OS,
187 				_("J�rg Schilling"));
188 		exit(0);
189 	}
190 
191 	if (Iflag) Cflag = 0;
192 
193 	cac = ac;
194 	cav = av;
195 	if (getfiles(&cac, &cav, opt) <= 0) {
196 		error("No pattern or substitution given.\n");
197 		usage(EX_BAD);
198 	}
199 	pat = cav[0];
200 	cac--, cav++;
201 	len = strlen(pat);
202 
203 	/*
204 	 * Cygwin32 (and newer Linux versions too) make
205 	 * stdin/stdout/stderr non constant expressions so we cannot do
206 	 * loader initialization.
207 	 *
208 	 * XXX May this be a problem?
209 	 */
210 	tty = stdin;
211 
212 	aux = malloc(sizeof (int)*len);
213 	state = malloc(sizeof (int)*(len+1));
214 	if (aux == NULL || state == NULL)
215 		comerrno(EX_BAD, "No memory for pattern compiler");
216 
217 	if ((alt = patcompile((unsigned char *)pat, len, aux)) == 0)
218 		comerrno(EX_BAD, "Bad pattern '%s'.\n", pat);
219 
220 	if (getfiles(&cac, &cav, opt) <= 0) {
221 		error("No substitution given.\n");
222 		usage(EX_BAD);
223 	}
224 	subst = cav[0];
225 
226 	cac--, cav++;
227 
228 	if (getfiles(&cac, &cav, opt) <= 0) {
229 		/*
230 		 * If no args, change is a filter from stdin to stdout.
231 		 */
232 		if (Iflag) {
233 #ifdef	HAVE__DEV_TTY
234 			if ((tty = fileopen("/dev/tty", "r")) == NULL)
235 				comerr("Can't open '/dev/tty'\n");
236 #else
237 			tty = stderr;
238 #endif
239 		}
240 		(void) change("", stdin, stdout, pat, subst, aux, alt);
241 		flush();
242 	} else for (; getfiles(&cac, &cav, opt) > 0; cac--, cav++) {
243 		file = fileopen(cav[0], "r");
244 		if (file == NULL) {
245 			errmsg("Can't open '%s'.\n", cav[0]);
246 		} else {
247 			mktmp(cav[0]);
248 			if ((tfile = fileopen(tmpname, "wct")) == NULL) {
249 				errmsg("Can't open '%s'.\n", tmpname);
250 			} else {
251 #ifdef	HAVE_FSYNC
252 				int	err;
253 				int	scnt;
254 #endif
255 				stat(cav[0], &ostat);
256 				stat(tmpname, &cstat);
257 				chmod(tmpname, ostat.st_mode);
258 				cnt = change(cav[0], file, tfile, pat, subst,
259 							aux, alt);
260 				fclose(file);
261 
262 				closeok = TRUE;
263 				if (fflush(tfile) != 0)
264 					closeok = FALSE;
265 #ifdef	HAVE_FSYNC
266 				err = 0;
267 				scnt = 0;
268 				do {
269 					if (fsync(fdown(tfile)) != 0)
270 						err = geterrno();
271 
272 					if (err == EINVAL)
273 						err = 0;
274 				} while (err == EINTR && ++scnt < 10);
275 				if (err != 0)
276 					closeok = FALSE;
277 #endif
278 				if (fclose(tfile) != 0)
279 					closeok = FALSE;
280 
281 				if (!closeok && cnt > 0)
282 					errmsg("Problems flushing outfile for '%s'.\n",
283 							cav[0]);
284 
285 				if (closeok && cnt > 0 && mkbak(cav[0]))
286 					chmod(cav[0], ostat.st_mode);
287 				else
288 					unlink(tmpname);
289 			}
290 		}
291 	}
292 	exit(0);
293 	/* NOTREACHED */
294 	return (0);	/* Keep lint happy */
295 }
296 
297 /*
298  * Return index of first char after matching pattern (if any) in line.
299  * If no match, return (-1).
300  * Start from position off in linep.
301  */
302 LOCAL int
match(pat,auxp,linep,off,len,alt)303 match(pat, auxp, linep, off, len, alt)
304 	char	*pat;
305 	int	*auxp;
306 	char	*linep;
307 	int	off;
308 	int	len;
309 	int	alt;
310 {
311 	char	*p;
312 
313 	p = (char *)patmatch((unsigned char *)pat, auxp,
314 					(unsigned char *)linep, off, len, alt, state);
315 	if (p == NULL)
316 		return (-1);
317 	else
318 		return (p-linep);
319 }
320 
321 /*
322  * Copy ifile to ofile, replace pattern with substitution.
323  */
324 LOCAL int
change(name,ifile,ofile,pat,subst,auxp,alt)325 change(name, ifile, ofile, pat, subst, auxp, alt)
326 	char	*name;
327 	FILE	*ifile;
328 	FILE	*ofile;
329 	char	*pat;
330 	char	*subst;
331 	int	*auxp;
332 	int	alt;
333 {
334 	register char	*linep;
335 	int	idx;
336 	int	cnt = 0;
337 	int	matend;
338 	int	lastmend;
339 	int	out;
340 	ssize_t	llen;
341 	int	changed;
342 	BOOL	hasnl;
343 
344 	while ((llen = fgetaline(ifile, &oline, &osize)) > 0) {
345 		linep = oline;
346 		if (linep[llen-1] == '\n') {
347 			linep[--llen] = '\0';
348 			hasnl = TRUE;
349 		} else {
350 			hasnl = FALSE;
351 		}
352 
353 		changed = 0;
354 		out = 0;
355 		lastmend = -1;
356 		for (idx = 0; linep[idx] != 0; ) {
357 			matend = match(pat, auxp, linep, idx, llen, alt);
358 			if (matend >= 0 && matend != lastmend) {
359 				lastmend = matend;
360 				if (changed >= CHcnt) {
361 					matend = llen;
362 					out = catsubst(linep, &newline, idx, matend, "&", out, &newsize);
363 					break;
364 				}
365 				if (Vflag && !changed)
366 					fprintf(stderr, "%s%s%s\n",
367 						name, *name?": ":"", linep);
368 				if (Iflag) {
369 					showchange(name, linep, idx, matend, subst, out, llen);
370 					if (!yes("OK? ")) {
371 						out = catsubst(linep, &newline, idx, matend, "&", out, &newsize);
372 						idx = matend;
373 						continue;
374 					}
375 				}
376 				out = catsubst(linep, &newline, idx, matend, subst, out, &newsize);
377 				cnt++;
378 				changed++;
379 			}
380 			if (matend == -1 || matend == idx)
381 				out = appchar(linep[idx++], &newline, out, &newsize);
382 			else
383 				idx = matend;
384 		}
385 		if (appchar('\0', &newline, out, &newsize) < 0) {
386 			errmsgno(EX_BAD, "Output line truncated: %s\n", newline);
387 			/*
388 			 * Abort if not in filter mode.
389 			 */
390 			if (ofile != stdout)
391 				return (-1);
392 		}
393 		fprintf(ofile, "%s%s", newline, hasnl?"\n":"");
394 		if (Cflag && changed)
395 			fprintf(stderr, "%s%s%s\n", name, *name?": ":"", newline);
396 	}
397 	if (llen < 0 && !feof(ifile)) {
398 		errmsg("Input read error on '%s'.\n", *name?name:"stdin");
399 		return (-1);
400 	}
401 	return (cnt);
402 }
403 
404 /*
405  * Concatenate substitution to current version of new line.
406  */
407 LOCAL int
catsubst(linep,newpp,start,end,subst,idx,maxp)408 catsubst(linep, newpp, start, end, subst, idx, maxp)
409 	char	*linep;
410 	char	**newpp;
411 	int	start;
412 	int	end;
413 	char	*subst;
414 	int	idx;
415 	size_t	*maxp;
416 {
417 	int	i;
418 
419 	while (*subst != '\0') {
420 		if (*subst == '&') {
421 			for (i = start; i < end; i++)
422 				idx = appchar(linep[i], newpp, idx, maxp);
423 			subst++;
424 		} else {
425 			if (*subst == '\\')
426 				subst++;
427 			idx = appchar(*subst++, newpp, idx, maxp);
428 		}
429 	}
430 	return (idx);
431 }
432 
433 /*
434  * Append a character to the buffer, use position off.
435  */
436 LOCAL int
appchar(c,bufp,off,maxp)437 appchar(c, bufp, off, maxp)
438 	char	c;
439 	char	**bufp;
440 	int	off;
441 	size_t	*maxp;
442 {
443 	size_t	bufsize = *maxp;
444 
445 	if (off < 0)
446 		return (-1);
447 	if (off >= bufsize) {
448 		char	*newbuf;
449 
450 		while (bufsize <= off)
451 			bufsize += LINEINCR;
452 		newbuf = realloc(*bufp, bufsize);
453 		if (newbuf == NULL) {
454 			errmsg("No memory to append to current line.\n");
455 			return (-1);
456 		}
457 		*bufp = newbuf;
458 		*maxp = bufsize;
459 	}
460 	(*bufp)[off++] = c;
461 	return (off);
462 }
463 
464 LOCAL void
showchange(name,linep,start,end,subst,out,max)465 showchange(name, linep, start, end, subst, out, max)
466 	char	*name;
467 	char	*linep;
468 	int	start;
469 	int	end;
470 	char	*subst;
471 	int	out;
472 	size_t	max;
473 {
474 static	char	*tmp = NULL;
475 static	size_t	tmpsize = 0;
476 	char	*new;
477 
478 	max++;
479 	while (tmpsize < max)
480 		tmpsize += LINEINCR;
481 	new = realloc(tmp, tmpsize);
482 	if (new == NULL) {
483 		errmsg("No memory for change preview.\n");
484 		return;
485 	}
486 
487 	tmp = new;
488 	movebytes(newline, tmp, max);
489 	out = catsubst(linep, &tmp, start, end, subst, out, &tmpsize);
490 	if (appchar('\0', &tmp, out, &tmpsize) < 0)
491 		error("Line will be truncated!\n");
492 	fprintf(stderr, "%s%s%s%s\n", name, *name?": ":"", tmp, &linep[end]);
493 }
494 
495 /* VARARGS1 */
496 #ifdef	PROTOTYPES
497 LOCAL BOOL
yes(char * form,...)498 yes(char *form, ...)
499 #else
500 LOCAL BOOL
501 yes(form, va_alist)
502 	char	*form;
503 	va_dcl
504 #endif
505 {
506 	va_list	args;
507 	char	okbuf[10];
508 
509 #ifdef	PROTOTYPES
510 	va_start(args, form);
511 #else
512 	va_start(args);
513 #endif
514 	fprintf(stderr, "%r", form, args);
515 	va_end(args);
516 	flush();
517 	fgetline(tty, okbuf, sizeof (okbuf));
518 	if (streql(okbuf, "y") || streql(okbuf, "yes"))
519 		return (TRUE);
520 	else
521 		return (FALSE);
522 }
523 
524 LOCAL void
mktmp(name)525 mktmp(name)
526 	char	*name;
527 {
528 	char	*p  = NULL;
529 	char	*p2 = name;
530 	char	c = '\0';
531 
532 	/*
533 	 * Extract dir from name
534 	 */
535 	while (*p2) {
536 #ifdef	tos
537 		if (*p2++ == '\\')
538 #else
539 		if (*p2++ == '/')
540 #endif
541 			p = p2;
542 	}
543 	if (p) {
544 		c = *p;
545 		*p = '\0';
546 	} else {
547 		name  = "";
548 	}
549 	js_snprintf(tmpname, sizeof (tmpname), "%sch%llo", name, (Llong)getpid());
550 	if (p)
551 		*p = c;
552 }
553 
554 LOCAL BOOL
mkbak(name)555 mkbak(name)
556 	char	*name;
557 {
558 	char	bakname[MAXNAME];
559 
560 	if (!Nflag) {
561 		/*
562 		 * Try to create a backup file.
563 		 */
564 		if (strlen(name) > (MAXNAME-5) &&
565 		    strchr(&name[MAXNAME-5], '/') != NULL) {
566 			errmsgno(EX_BAD, "Cannot backup '%s'; name too long\n", name);
567 			return (FALSE);
568 		}
569 
570 		strncpy(bakname, name, MAXNAME-5);
571 		bakname[MAXNAME-5] = '\0';
572 		strcat(bakname, ".bak");
573 
574 		if (rename(name, bakname) < 0) { /* make cur file .bak	*/
575 
576 			errmsg("Cannot backup '%s'\n", name);
577 			return (FALSE);
578 		}
579 	}
580 
581 	if (rename(tmpname, name) < 0) {	/* rename new file	*/
582 		errmsg("Cannot rename '%s' to '%s'\n", tmpname, name);
583 		if (!Nflag) {
584 			/*
585 			 * Try to make .bak current again.
586 			 */
587 			if (rename(bakname, name) < 0)
588 				errmsg("Cannot rename backup '%s' back to '%s'\n",
589 					bakname, name);
590 		}
591 		return (FALSE);
592 	}
593 	return (TRUE);
594 }
595 
596