[comp.sources.unix] v11i007: A query-replace program

rs@uunet.UU.NET (Rich Salz) (08/14/87)

Submitted-by: der Mouse  <mouse@larry.ee.mcgill>
Posting-number: Volume 11, Issue 7
Archive-name: qsubst

[  This is a program to do large amounts of query-replaces on a bunch
   of files.  I spliced the Makefile into the shar.  --r$  ]

A useful little program.  Designed one evening when someone wanted to
rename a few routines which were used in umpteen zillion places all
over a bunch of files.  This code is known to work on mtXinu 4.3+NFS
and on Sunix 3.0; I would expect it to work on BSD systems in general.
I have no idea how easy porting to a USG (SysIII, SysV) environment
would be; we don't have USG systems around here.

#! /bin/sh
#
# Shar: Shell Archiver
#
# Run this through sh to create:
#	Makefile
#	qsubst.1
#	qsubst.c
echo x - Makefile \(some number of characters\)
sed 's/^X//' > Makefile << \SHAR_EOF
all:	qsubst
install:	qsubst qsubst.1
	@echo copy qsubst and qsubst.1 to appropriate directories.
qsubst:		qsubst.c
	$(CC) $(CFLAGS) -o qsubst qsubst.c
SHAR_EOF
echo No error check because the moderator is lazy...
echo x - qsubst.1 \(4752 characters\)
sed 's/^X//' > qsubst.1 << \SHAR_EOF
X.TH QSUBST 1 "31 March 1985"
X.UC 4
X.SH NAME
Xqsubst - query/substitute strings in files
X.SH SYNOPSIS
X.nf
X.ft B
Xqsubst string1 string2 [ options / files ]
X.PP
X.fi
X.SH DESCRIPTION
X\fIQsubst\fP is designed for substituting strings in (large) files.  It
Xaccepts a list of file names and two strings.  For each of the files,
X\fIqsubst\fP modifies it in-place to replace \fIstring1\fP with \fIstring2\fP
Xwherever the user approves the change.
X.PP
XEach time \fIqsubst\fP finds the \fIstring1\fP in a file, it prints a few
Xlines of context (see the -c option below) and expects the user to type a
Xcharacter to indicate whether or not the replacement should be done.  The
Xuser can type:
X.TP 8
X space
XReplace this instance and go on to the next one.
X.TP
X .
XReplace this instance but don't do any more in this file (ie, go on to the
Xnext file).
X.TP
X n
XDon't change this instance, just go on to the next one.
X.TP
X ^G
XDon't change this instance or any more in this file, just go on to the next
Xfile.
X.TP
X !
XChange this instance and all further ones in this file without asking, then
Xgo on to the next file (for which \fIqsubst\fP will begin asking again).
X.PP
XThe arguments preceding the strings are interpreted as filenames (which
X\fIqsubst\fP then performs replacements in) if they do not begin with \-
Xsigns.  If they do, they are interpreted as follows:
X.TP 8
X\-!
X.br
X.ns
X.TP
X\-go
X.br
X.ns
X.TP
X\-noask
XDon't bother asking, just replace.  This is like hitting ! as soon as the
Xfirst instance appears for each file except that nothing is printed.  This is
Xin effect for all files encountered until a \-nogo or \-ask option is given.
XNote that if you use csh, you will probably have to backslash the !, which is
Xwhy the alternative forms exist.
X.TP
X\-nogo
X.br
X.ns
X.TP
X\-ask
XNegate a \-go or \-noask option.  \fIQsubst\fP will once more prompt the user
Xas usual.
X.TP
X\-CA\fIN\fP
XGive \fIN\fP lines of context above the line with the match when prompting
Xthe user.
X.TP
X\-CB\fIN\fP
XGive \fIN\fP lines of context below the line with the match when prompting
Xthe user.
X.TP
X\-c\fIN\fP
XGive \fIN\fP lines of context above and as many below the line with the
Xmatch when prompting the user (equivalent to \-CA\fIN\fP \-CB\fIN\fP).
X.TP
X.SM
X\-f \fIfilename\fP
XSame as a simple \fIfilename\fP argument, but is useful if the file name
Xbegins with a \-.
X.TP
X.SM
X\-F \fIfilename\fP
X\fIQsubst\fP reads file names from the file, one per line, and performs
Xsubstitutions on the files whose names it reads thus.
X.SH ERRORS
X\fIQsubst\fP can generate the following error messages:
X.PP
XUsage: qsubst str1 str2 [ \-! \-noask \-go \-f file \-F file ]
X.PP
XThis is a usage message printed if fewer than two arguments are given.
X.PP
Xqsubst: argument order has changed, it's now: str1 str2 files...
X.PP
XQsubst used to take arguments differently; the two strings were the *last*
Xtwo arguments rather than the first two.  This message is printed if the
Xfirst argument is a valid file name but the last two are not - qsubst assumes
Xthat you didn't know of the change yet.  However, qsubst continues as though
Xwhat you gave were actually what you meant.
X.PP
Xqsubst: cannot create temp file /tmp/qsubst.012345
X.PP
X(The number may vary).  This message is generated if \fIqsubst\fP cannot open
Xits temporary file (the name appears in the message).  Something is seriously
Xwrong with either \fIqsubst\fP or /tmp.
X.PP
Xqsubst: search string too long (max 512 chars)
X.PP
XThis is surely self-explanatory.
X.PP
Xqsubst: \-C must be \-CA or \-CB
X.PP
XYou gave \-C, but it was neither \-CA nor \-CB.
X.PP
Xqsubst: \-f what?
X.br
Xqsubst: \-F what?
X.PP
XYou gave \-f (or -\F) without a file name.
X.PP
Xqsubst: cannot read \fIfilename\fP
X.PP
X\fIQsubst\fP couldn't open the named file for read-only.
X.PP
Xqsubst: cannot rewrite \fIfilename\fP
X.PP
X\fIQsubst\fP couldn't open the named file read/write for substitution.
X.SH NOTES
X\fIQsubst\fP reads its argument list in order and processes files as it comes
Xto them.  Hence (for instance), a \-go option will affect only files named
Xafter the \-go option.
X.PP
XThe most context you can get is \-c10.
X.PP
XThe search string (\fIstring1\fP) is limited to 512 characters.  There is no
Xsimilar limit on the length of the replacement string (\fIstring2\fP).
X.PP
XNulls in the file may cause \fIqsubst\fP to make various mistakes.
X.PP
XIf any other program modifies the file while \fIqsubst\fP is running, all
Xbets are off.
X.PP
X\fIQsubst\fP expect the files it is performing substitutions in to be real
Xfiles, that is, objects on which \fIlseek(2)\fP works sensibly.  Not, for
Xexample, terminals or pipes.
X.PP
X\fIQsubst\fP is careful to modify files only when necessary, so it interacts
Xnicely with \fImake(1)\fP.
X.SH AUTHOR
Xder Mouse (Mike Parker, mcgill-vision!mouse), March, 1985.
SHAR_EOF
if test 4752 -ne "`wc -c qsubst.1`"
then
echo shar: error transmitting qsubst.1 \(should have been 4752 characters\)
fi
echo x - qsubst.c \(10813 characters\)
sed 's/^X//' > qsubst.c << \SHAR_EOF
X/*
X * Qsubst -- designed for renaming routines existing in a whole bunch of
X *  files.  Must be compiled with -ltermcap.
X *
X * Usage:
X *
X * qsubst str1 str2 [ options ]
X *
X * Qsubst reads its options (see below) to get a list of files.  For each
X *  file on this list, it then replaces str1 with str2 wherever possible in
X *  that file, depending on user input (see below).  The result is written
X *  back onto the original file.
X *
X * For each possible substitution, the user is prompted with a few lines
X *  before and after the line containing the string to be substituted.  The
X *  string itself is underlined, if the terminal supports this.  Then one
X *  character is read from the terminal.  This is then interpreted as
X *  follows (this is designed to be like Emacs' query-replace-string):
X *
X *	space	replace this occurrence and go on to the next one
X *	.	replace this occurrence and don't change any more in this
X *		file (ie, go on to the next file).
X *	,	tentatively replace this occurrence.  The lines as they would
X *		look if the substitution were made are printed out.  Then
X *		another character is read and it is used to decide the
X *		result (possibly undoing the tentative replacement).
X *	n	don't change this one, but go on to the next one
X *	^G	don't change this one or any others in this file, but instead
X *		go on to the next file.
X *	!	change the rest in this file without asking, then go on to
X *		the next file (at which point qsubst will start asking
X *		again).
X *	?	print out the current filename and ask again.
X *
X * The first two arguments to qsubst are always the string to replace and the
X *  string to replace it with.  The options are as follows:
X *
X *	-!	Enter ! mode automatically at the beginning of each file
X *	-go	Same as -!
X *	-noask	Same as -!
X *	-nogo	Negate -go
X *	-ask	Negate -noask (same as -nogo)
X *	-cN	(N is a number) Give N lines of context above and below the
X *		line with the match when prompting the user.
X *	-CAN	(N is a number) Give N lines of context above the line with
X *		the match when prompting the user.
X *	-CBN	(N is a number) Give N lines of context below the line with
X *		the match when prompting the user.
X *	-f filename
X *		The filename following the -f argument is one of the files
X *		qsubst should perform substitutions in.
X *	-F filename
X *		Qsubst should read the named file to get the names of files
X *		to perform substitutions in.  The names should appear one to
X *		a line.
X *
X * The default amount of context is -c2, that is, two lines above and two
X *  lines below the line with the match.
X *
X * Arguments not beginning with a - sign in the options field are implicitly
X *  preceded by -f.  Thus, -f is really needed only when the file name begins
X *  with a - sign.
X *
X * Qsubst reads its options in order and processes files as it gets them.
X *  This means, for example, that a -go will affect only files from -f or -F
X *  options appearing after the -go option.
X *
X * The most context you can get is ten lines each, above and below
X *  (corresponding to -c10).
X *
X * Str1 is limited to 512 characters; there is no limit on the size of str2.
X *  Neither one may contain a null.
X *
X * Nulls in the file may cause qsubst to make various mistakes.
X *
X * If any other program modifies the file while qsubst is running, all bets
X *  are off.
X */
X#include <stdio.h>
X#include <sgtty.h>
X#include <signal.h>
X#include <sys/file.h>
X
X#ifndef sigmask
X#define sigmask(sig) (1<<((sig)-1))
X#endif
X
X#define MAX_C_A 10
X#define MAX_C_B 10
X#define BUF_SIZ 1024
X
Xchar **argvec;
X
Xchar *getenv();
Xchar *tgetstr();
Xchar *malloc();
Xchar *sindex();
Xchar *mktemp();
X
XFILE *tempf;
Xlong int tbeg;
XFILE *workf;
Xchar *str1;
Xchar *str2;
Xint s1l;
Xint s2l;
Xlong int nls[MAX_C_A+1];
Xchar buf[BUF_SIZ*2];
Xchar *buf1 = buf;
Xchar *buf2 = buf + BUF_SIZ;
Xint cabove;
Xint cbelow;
Xint flying;
Xint flystate;
Xint allfly;
Xchar *nullstr = "";
Xint ul_;
Xchar *current_file;
Xchar *beginul;
Xchar *endul;
Xchar tcp_buf[1024];
Xchar cap_buf[1024];
Xstruct sgttyb orig_sg;
X
Xtstp_self()
X{
X int (*old_tstp)();
X int mask;
X
X mask = sigblock(0);
X old_tstp = signal(SIGTSTP,SIG_DFL);
X sigsetmask(mask&~sigmask(SIGTSTP));
X kill(getpid(),SIGTSTP);
X signal(SIGTSTP,old_tstp);
X}
X
Xsigtstp()
X{
X struct sgttyb sg;
X
X if (ioctl(fileno(stdin),TIOCGETP,&sg) < 0)
X  { tstp_self();
X    return;
X  }
X ioctl(fileno(stdin),TIOCSETN,&orig_sg);
X tstp_self();
X ioctl(fileno(stdin),TIOCSETN,&sg);
X}
X
Xmain(ac,av)
Xint ac;
Xchar **av;
X{
X int skip;
X char *cp;
X
X argvec = av;
X if (ac < 3)
X  { fprintf(stderr,"Usage: %s str1 str2 [ -! -noask -go -f file -F file ]\n",
X				argvec[0]);
X    exit(1);
X  }
X cp = getenv("TERM");
X if (cp == 0)
X  { beginul = nullstr;
X    endul = nullstr;
X  }
X else
X  { if (tgetent(tcp_buf,cp) != 1)
X     { beginul = nullstr;
X       endul = nullstr;
X     }
X    else
X     { cp = cap_buf;
X       if (tgetflag("os") || tgetflag("ul"))
X	{ ul_ = 1;
X	}
X       else
X	{ ul_ = 0;
X	  beginul = tgetstr("us",&cp);
X	  if (beginul == 0)
X	   { beginul = tgetstr("so",&cp);
X	     if (beginul == 0)
X	      { beginul = nullstr;
X		endul = nullstr;
X	      }
X	     else
X	      { endul = tgetstr("se",&cp);
X	      }
X	   }
X	  else
X	   { endul = tgetstr("ue",&cp);
X	   }
X	}
X     }
X  }
X cp = mktemp("/tmp/qsubst.XXXXXX");
X tempf = fopen(cp,"w+");
X if (tempf == NULL)
X  { fprintf(stderr,"%s: cannot create temp file %s\n",argvec[0],cp);
X    exit(1);
X  }
X unlink(cp);
X if ( (access(av[1],R_OK|W_OK) == 0) &&
X      (access(av[ac-1],R_OK|W_OK) < 0) &&
X      (access(av[ac-2],R_OK|W_OK) < 0) )
X  { fprintf(stderr,"\
X%s: argument order has changed, it's now: str1 str2 files...\n",argvec[0]);
X  }
X str1 = av[1];
X str2 = av[2];
X av += 2;
X ac -= 2;
X s1l = strlen(str1);
X s2l = strlen(str2);
X if (s1l > BUF_SIZ/2)
X  { fprintf(stderr,"%s: search string too long (max %d chars)\n",
X						argvec[0],BUF_SIZ/2);
X    exit(1);
X  }
X ioctl(fileno(stdin),TIOCGETP,&orig_sg);
X signal(SIGTSTP,sigtstp);
X allfly = 0;
X cabove = 2;
X cbelow = 2;
X skip = 0;
X for (ac--,av++;ac;ac--,av++)
X  { if (skip > 0)
X     { skip --;
X       continue;
X     }
X    if (**av == '-')
X     { ++*av;
X       if ( (strcmp(*av,"!") == 0) ||
X	    (strcmp(*av,"go") == 0) ||
X	    (strcmp(*av,"noask") == 0) )
X	{ allfly = 1;
X	}
X       else if ( (strcmp(*av,"nogo") == 0) ||
X		 (strcmp(*av,"ask") == 0) )
X	{ allfly = 0;
X	}
X       else if (**av == 'c')
X	{ cabove = atoi(++*av);
X	  cbelow = cabove;
X	  limit_above_below();
X	}
X       else if (**av == 'C')
X	{ ++*av;
X	  if (**av == 'A')
X	   { cabove = atoi(++*av);
X	     limit_above_below();
X	   }
X	  else if (**av == 'B')
X	   { cbelow = atoi(++*av);
X	     limit_above_below();
X	   }
X	  else
X	   { fprintf(stderr,"%s: -C must be -CA or -CB\n",argvec[0]);
X	   }
X	}
X       else if ( (strcmp(*av,"f") == 0) ||
X		 (strcmp(*av,"F") == 0) )
X	{ if (++skip >= ac)
X	   { fprintf(stderr,"%s: -%s what?\n",argvec[0],*av);
X	   }
X	  else
X	   { if (**av == 'f')
X	      { process_file(av[skip]);
X	      }
X	     else
X	      { process_indir_file(av[skip]);
X	      }
X	   }
X	}
X     }
X    else
X     { process_file(*av);
X     }
X  }
X exit(0);
X}
X
Xlimit_above_below()
X{
X if (cabove > MAX_C_A)
X  { cabove = MAX_C_A;
X  }
X if (cbelow > MAX_C_B)
X  { cbelow = MAX_C_B;
X  }
X}
X
Xprocess_indir_file(fn)
Xchar *fn;
X{
X char newfn[1024];
X FILE *f;
X
X f = fopen(fn,"r");
X if (f == NULL)
X  { fprintf(stderr,"%s: cannot read %s\n",argvec[0],fn);
X    return;
X  }
X while (fgets(newfn,sizeof(newfn),f) == newfn)
X  { newfn[strlen(newfn)-1] = '\0';
X    process_file(newfn);
X  }
X fclose(f);
X}
X
Xprocess_file(fn)
Xchar *fn;
X{
X int i;
X long int n;
X char *cp;
X
X workf = fopen(fn,"r+");
X if (workf == NULL)
X  { fprintf(stderr,"%s: cannot read %s\n",argvec[0],fn);
X    return;
X  }
X printf("(file: %s)\n",fn);
X current_file = fn;
X for (i=0;i<=MAX_C_A;i++)
X  { nls[i] = -1;
X  }
X nls[MAX_C_A] = 0;
X tbeg = -1;
X n = 0;
X flying = allfly;
X flystate = 1;
X while (1)
X  { while (n < s1l)
X     { buf[n] = getc(workf);
X       if (buf[n] == '\n')
X	{ add_shift(nls,ftell(workf),MAX_C_A+1);
X	}
X       n ++;
X       if (feof(workf))
X	{ if (tbeg >= 0)
X	   { n --;
X	     fwrite(buf,1,n,tempf);
X	     fseek(workf,tbeg,0);
X	     n = ftell(tempf);
X	     fseek(tempf,0L,0);
X	     for (;n;n--)
X	      { putc(getc(tempf),workf);
X	      }
X	     fflush(workf);
X	     ftruncate(fileno(workf),ftell(workf));
X	   }
X	  fclose(workf);
X	  return;
X	}
X     }
X    if ((bcmp(buf,str1,s1l) == 0) && doit())
X     { if (tbeg < 0)
X	{ tbeg = ftell(workf) - s1l;
X	  fseek(tempf,0L,0);
X	}
X       fwrite(str2,1,s2l,tempf);
X       n = 0;
X     }
X    else
X     { if (tbeg >= 0)
X	{ putc(buf[0],tempf);
X	}
X       n --;
X       bcopy(buf+1,buf,n);
X     }
X  }
X}
X
Xadd_shift(a,e,n)
Xregister long int *a;
Xregister long int e;
Xregister int n;
X{
X register int i;
X
X n --;
X for (i=0;i<n;i++)
X  { a[i] = a[i+1];
X  }
X a[n] = e;
X}
X
Xchar getc_cbreak()
X{
X struct sgttyb sg;
X struct sgttyb osg;
X char c;
X
X if (ioctl(fileno(stdin),TIOCGETP,&sg) < 0)
X  { return(getchar());
X  }
X osg = sg;
X sg.sg_flags |= CBREAK;
X sg.sg_flags &= ~ECHO;
X ioctl(fileno(stdin),TIOCSETN,&sg);
X c = getchar();
X ioctl(fileno(stdin),TIOCSETN,&osg);
X return(c);
X}
X
Xint doit()
X{
X long int save;
X int i;
X char c;
X int use_replacement;
X
X if (flying)
X  { return(flystate);
X  }
X use_replacement = 0;
X save = ftell(workf);
X i = -1;
X while (i < 0)
X  { for (i=MAX_C_A-cabove;nls[i]<0;i++) ;
X    fseek(workf,nls[i],0);
X    for (i=save-nls[i]-s1l;i;i--)
X     { putchar(getc(workf));
X     }
X    put_ul(use_replacement?str2:str1);
X    fseek(workf,save,0);
X    i = cbelow + 1;
X    while (i > 0)
X     { char c;
X       c = getc(workf);
X       if (feof(workf))
X	{ clearerr(workf);
X	  break;
X	}
X       putchar(c);
X       if (c == '\n')
X	{ i --;
X	}
X     }
X    fseek(workf,save,0);
X    i = -1;
X    while (i == -1)
X     { switch (getc_cbreak())
X	{ case ' ':
X	     i = 1;
X	     break;
X	  case '.':
X	     i = 1;
X	     flying = 1;
X	     flystate = 0;
X	     break;
X	  case 'n':
X	     i = 0;
X	     break;
X	  case '\7':
X	     i = 0;
X	     flying = 1;
X	     flystate = 0;
X	     break;
X	  case '!':
X	     i = 1;
X	     flying = 1;
X	     flystate = 1;
X	     break;
X	  case ',':
X	     use_replacement = ! use_replacement;
X	     i = -2;
X	     printf("(using %s string gives)\n",use_replacement?"new":"old");
X	     break;
X	  case '?':
X	     printf("File is `%s'\n",current_file);
X	     break;
X	  default:
X	     putchar('\7');
X	     break;
X	}
X     }
X  }
X if (i)
X  { printf("(replacing");
X  }
X else
X  { printf("(leaving");
X  }
X if (flying)
X  { if (flystate == i)
X     { printf(" this and all the rest");
X     }
X    else if (flystate)
X     { printf(" this, replacing all the rest");
X     }
X    else
X     { printf(" this, leaving all the rest");
X     }
X  }
X printf(")\n");
X return(i);
X}
X
Xputcharf(c)
Xchar c;
X{
X putchar(c);
X}
X
Xput_ul(s)
Xchar *s;
X{
X if (ul_)
X  { for (;*s;s++)
X     { printf("_\b%c",*s);
X     }
X  }
X else
X  { tputs(beginul,1,putcharf);
X    fputs(s,stdout);
X    tputs(endul,1,putcharf);
X  }
X}
SHAR_EOF
if test 10813 -ne "`wc -c qsubst.c`"
then
echo shar: error transmitting qsubst.c \(should have been 10813 characters\)
fi
exit 0
# end of shell archive

					der Mouse

Smart mailers: mouse@mcgill-vision.uucp
USA: {ihnp4,decvax,akgua,utzoo,etc}!utcsri!musocs!mcgill-vision!mouse
     think!mosart!mcgill-vision!mouse
ARPAnet: think!mosart!mcgill-vision!mouse@harvard.harvard.edu

-- 

Rich $alz
Cronus Project, BBN Labs			rsalz@bbn.com
Moderator, comp.sources.unix			sources@uunet.uu.net