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