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