Skip to content
Browse files

Initial import of "qsubst", a command line query search and replace

facility. Note that this is the raw distribution I got from der Mouse,
for reference on the vendor branch.
  • Loading branch information...
1 parent a2bca40 commit 34306391f8b710c6959aec209055b5671138ce24 perry committed Sep 4, 1999
Showing with 749 additions and 0 deletions.
  1. +127 −0 usr.bin/qsubst/qsubst.1
  2. +622 −0 usr.bin/qsubst/qsubst.c
View
127 usr.bin/qsubst/qsubst.1
@@ -0,0 +1,127 @@
+.\" This file is in the public domain.
+.Dd May 29, 1997
+.Dt QSUBST 1
+.Os NetBSD 1.2BETA
+.Sh NAME
+.Nm qsubst
+.Nd query-replace strings in files
+.Sh SYNOPSIS
+.Nm
+.Ar str1
+.Ar str2
+.Op Ar flags
+.Ar file
+.Op Ar file Op Ar ...
+.Sh DESCRIPTION
+.Nm
+reads its options (see below) to get a list of files. For each file on
+this list, it then replaces
+.Ar str1
+with
+.Ar str2
+wherever possible in that file, depending on user input (see below).
+The result is written back onto the original file.
+.Pp
+For each potential substitution found, the user is prompted with a few
+lines before and after the line containing the string to be
+substituted. The string itself is displayed using the terminal's
+standout mode, if any. Then one character is read from the terminal.
+This is then interpreted as follows (this is designed to be like Emacs'
+query-replace-string):
+.Bl -tag -compact -offset indent
+.It space
+Replace this occurrence and go on to the next one.
+.It \&.
+Replace this occurrence and don't change any more in this file (ie, go
+on to the next file).
+.It \&,
+Tentatively replace this occurrence. The lines as they would look if
+the substitution were made are printed out. Then another character is
+read and it is used to decide the result as if the tentative
+replacement had not happened.
+.It n
+Don't change this one; just go on to the next one.
+.It \&^G
+Don't change this one or any others in this file, but instead simply go
+on to the next file.
+.It \&!
+Change the rest in this file without asking, then go on to the next
+file (at which point qsubst will start asking again).
+.It \&?
+Print out the current filename and ask again.
+.El
+.Pp
+The first two arguments to qsubst are always the string to replace and
+the string to replace it with. The options are as follows:
+.Bl -tag -compact -offset indent
+.It Fl w
+The search string is considered as a C symbol; it must be bounded by
+non-symbol characters. This option toggles.
+.Pf ( Sq w
+for
+.Sq word . )
+.It Fl \&!
+.It Fl go
+.It Fl noask
+Enter \&! mode automatically at the beginning of each file.
+.It Fl nogo
+.It Fl ask
+Negate
+.Fl -go ,
+that is, ask as usual.
+.It Fl c Ns Ar N
+(Where N is a number.) Give N lines of context above and below the
+line with the match when prompting the user.
+.It Fl CA Ns Ar N
+(Where N is a number.) Give N lines of context above the line with the
+match when prompting the user.
+.It Fl CB Ns Ar N
+(Where N is a number.) Give N lines of context below the line with the
+match when prompting the user.
+.It Fl f Ar filename
+The
+.Ar filename
+argument is one of the files qsubst should perform substitutions in.
+.It Fl F Ar filename
+.Nm
+reads
+.Ar filename
+to get the names of files to perform substitutions in. The names
+should appear one to a line.
+.El
+.Pp
+The default amount of context is
+.Fl c2 ,
+that is, two lines above and two lines below the line with the match.
+.Pp
+Arguments not beginning with a
+.Fl \&
+sign in the options field are implicitly preceded by
+.Fl f .
+Thus,
+.Fl f
+is really needed only when the file name begins with a
+.Fl \&
+sign.
+.Pp
+.Nm
+reads its options in order and processes files as it gets them. This
+means, for example, that a
+.Fl go
+will affect only files named after the
+.Fl go .
+.Pp
+The most context you can get is ten lines each, above and below.
+.Pp
+.Ar str1
+is limited to 512 characters; there is no limit on the size of
+.Ar str2 .
+Neither one may contain a NUL.
+.Pp
+NULs in the file may cause qsubst to make various mistakes.
+.Pp
+If any other program modifies the file while qsubst is running, all
+bets are off.
+.Sh AUTHOR
+der Mouse,
+.Aq mouse@rodents.montreal.qc.ca .
View
622 usr.bin/qsubst/qsubst.c
@@ -0,0 +1,622 @@
+/*
+ * qsubst -- designed for renaming routines existing in a whole bunch
+ * of files. Needs -ltermcap.
+ *
+ * Usage:
+ *
+ * qsubst str1 str2 [ options ]
+ *
+ * qsubst reads its options (see below) to get a list of files. For
+ * each file on this list, it then replaces str1 with str2 wherever
+ * possible in that file, depending on user input (see below). The
+ * result is written back onto the original file.
+ *
+ * For each possible substitution, the user is prompted with a few
+ * lines before and after the line containing the string to be
+ * substituted. The string itself is displayed using the terminal's
+ * standout mode, if any. Then one character is read from the
+ * terminal. This is then interpreted as follows (this is designed to
+ * be like Emacs' query-replace-string):
+ *
+ * space replace this occurrence and go on to the next one
+ * . replace this occurrence and don't change any more in
+ * this file (ie, go on to the next file).
+ * , tentatively replace this occurrence. The lines as they
+ * would look if the substitution were made are printed
+ * out. Then another character is read and it is used to
+ * decide the result (possibly undoing the tentative
+ * replacement).
+ * n don't change this one, but go on to the next one
+ * ^G don't change this one or any others in this file, but
+ * instead go on to the next file.
+ * ! change the rest in this file without asking, then go on
+ * to the next file (at which point qsubst will start
+ * asking again).
+ * ? print out the current filename and ask again.
+ *
+ * The first two arguments to qsubst are always the string to replace
+ * and the string to replace it with. The options are as follows:
+ *
+ * -w The search string is considered as a C symbol; it must
+ * be bounded by non-symbol characters. This option
+ * toggles. (`w' for `word'.)
+ * -! Enter ! mode automatically at the beginning of each
+ * file.
+ * -go Same as -!
+ * -noask Same as -!
+ * -nogo Negate -go
+ * -ask Negate -noask (same as -nogo)
+ * -cN (N is a number) Give N lines of context above and below
+ * the line with the match when prompting the user.
+ * -CAN (N is a number) Give N lines of context above the line
+ * with the match when prompting the user.
+ * -CBN (N is a number) Give N lines of context below the line
+ * with the match when prompting the user.
+ * -f filename
+ * The filename following the -f argument is one of the
+ * files qsubst should perform substitutions in.
+ * -F filename
+ * qsubst should read the named file to get the names of
+ * files to perform substitutions in. The names should
+ * appear one to a line.
+ *
+ * The default amount of context is -c2, that is, two lines above and
+ * two lines below the line with the match.
+ *
+ * Arguments not beginning with a - sign in the options field are
+ * implicitly preceded by -f. Thus, -f is really needed only when the
+ * file name begins with a - sign.
+ *
+ * qsubst reads its options in order and processes files as it gets
+ * them. This means, for example, that a -go will affect only files
+ * from -f or -F options appearing after the -go option.
+ *
+ * The most context you can get is ten lines each, above and below
+ * (corresponding to -c10).
+ *
+ * Str1 is limited to 512 characters; there is no limit on the size of
+ * str2. Neither one may contain a NUL.
+ *
+ * NULs in the file may cause qsubst to make various mistakes.
+ *
+ * If any other program modifies the file while qsubst is running, all
+ * bets are off.
+ *
+ * This program is in the public domain. Anyone may use it in any way
+ * for any purpose. Of course, it's also up to you to determine
+ * whether what it does is suitable for you; the above comments may
+ * help, but I can't promise they're accurate. It's free, and you get
+ * what you pay for.
+ *
+ * If you find any bugs I would appreciate hearing about them,
+ * especially if you also fix them.
+ *
+ * der Mouse
+ *
+ * mouse@rodents.montreal.qc.ca
+ */
+
+#include <stdio.h>
+#include <ctype.h>
+#include <errno.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <strings.h>
+#include <termios.h>
+#include <sys/file.h>
+#include <unused-arg.h>
+
+/* These belong in an include file, but which one? */
+extern int tgetent(char *, const char *);
+extern int tgetflag(const char *);
+extern const char *tgetstr(const char *, char **);
+extern void tputs(const char *, int, int (*)(char));
+
+extern const char *__progname;
+
+#define MAX_C_A 10
+#define MAX_C_B 10
+#define BUF_SIZ 1024
+
+static int debugging;
+static FILE *tempf;
+static long int tbeg;
+static FILE *workf;
+static char *str1;
+static char *str2;
+static int s1l;
+static int s2l;
+static long int nls[MAX_C_A+1];
+static char buf[(BUF_SIZ*2)+2];
+static char *bufp;
+static char *bufp0;
+static char *bufpmax;
+static int rahead;
+static int cabove;
+static int cbelow;
+static int wordmode;
+static int flying;
+static int flystate;
+static int allfly;
+static const char *nullstr = "";
+static int ul_;
+static char *current_file;
+static const char *beginul;
+static const char *endul;
+static char tcp_buf[1024];
+static char cap_buf[1024];
+static struct termios orig_tio;
+
+static void tstp_self(void)
+{
+ void (*old_tstp)(int);
+ int mask;
+
+ mask = sigblock(0);
+ kill(getpid(),SIGTSTP);
+ old_tstp = signal(SIGTSTP,SIG_DFL);
+ sigsetmask(mask&~sigmask(SIGTSTP));
+ signal(SIGTSTP,old_tstp);
+}
+
+static void sigtstp(UNUSED_ARG(int sig))
+{
+ struct termios tio;
+
+ if (tcgetattr(0,&tio) < 0)
+ { tstp_self();
+ return;
+ }
+ tcsetattr(0,TCSAFLUSH|TCSASOFT,&orig_tio);
+ tstp_self();
+ tcsetattr(0,TCSADRAIN|TCSASOFT,&tio);
+}
+
+static void limit_above_below(void)
+{
+ if (cabove > MAX_C_A)
+ { cabove = MAX_C_A;
+ }
+ if (cbelow > MAX_C_B)
+ { cbelow = MAX_C_B;
+ }
+}
+
+static int issymchar(char c)
+{
+ return( isascii(c) &&
+ ( isalnum(c) ||
+ (c == '_') ||
+ (c == '$') ) );
+}
+
+static int foundit(void)
+{
+ if (wordmode)
+ { return( !issymchar(bufp[-1]) &&
+ !issymchar(bufp[-2-s1l]) &&
+ !bcmp(bufp-1-s1l,str1,s1l) );
+ }
+ else
+ { return(!bcmp(bufp-s1l,str1,s1l));
+ }
+}
+
+static int putcharf(char c)
+{
+ putchar(c);
+ return(0); /* ??? */
+}
+
+static void put_ul(char *s)
+{
+ if (ul_)
+ { for (;*s;s++)
+ { printf("_\b%c",*s);
+ }
+ }
+ else
+ { tputs(beginul,1,putcharf);
+ fputs(s,stdout);
+ tputs(endul,1,putcharf);
+ }
+}
+
+static int getc_cbreak(void)
+{
+ struct termios tio;
+ struct termios otio;
+ char c;
+
+ if (tcgetattr(0,&tio) < 0) return(getchar());
+ otio = tio;
+ tio.c_lflag &= ~(ICANON|ECHOKE|ECHOE|ECHO|ECHONL);
+ tio.c_cc[VMIN] = 1;
+ tio.c_cc[VTIME] = 0;
+ tcsetattr(0,TCSANOW|TCSASOFT,&tio);
+ switch (read(0,&c,1))
+ { case -1:
+ break;
+ case 0:
+ break;
+ case 1:
+ break;
+ }
+ tcsetattr(0,TCSANOW|TCSASOFT,&tio);
+ return(c);
+}
+
+static int doit(void)
+{
+ long int save;
+ int i;
+ int lastnl;
+ int use_replacement;
+
+ if (flying)
+ { return(flystate);
+ }
+ use_replacement = 0;
+ save = ftell(workf);
+ do
+ { for (i=MAX_C_A-cabove;nls[i]<0;i++) ;
+ fseek(workf,nls[i],0);
+ for (i=save-nls[i]-rahead;i;i--)
+ { putchar(getc(workf));
+ }
+ put_ul(use_replacement?str2:str1);
+ fseek(workf,save+s1l-rahead,0);
+ lastnl = 0;
+ i = cbelow + 1;
+ while (i > 0)
+ { int c;
+ c = getc(workf);
+ if (c == EOF)
+ { clearerr(workf);
+ break;
+ }
+ putchar(c);
+ lastnl = 0;
+ if (c == '\n')
+ { i --;
+ lastnl = 1;
+ }
+ }
+ if (! lastnl) printf("\n[no final newline] ");
+ fseek(workf,save,0);
+ i = -1;
+ while (i == -1)
+ { switch (getc_cbreak())
+ { case ' ':
+ i = 1;
+ break;
+ case '.':
+ i = 1;
+ flying = 1;
+ flystate = 0;
+ break;
+ case 'n':
+ i = 0;
+ break;
+ case '\7':
+ i = 0;
+ flying = 1;
+ flystate = 0;
+ break;
+ case '!':
+ i = 1;
+ flying = 1;
+ flystate = 1;
+ break;
+ case ',':
+ use_replacement = ! use_replacement;
+ i = -2;
+ printf("(using %s string gives)\n",use_replacement?"new":"old");
+ break;
+ case '?':
+ printf("File is `%s'\n",current_file);
+ break;
+ default:
+ putchar('\7');
+ break;
+ }
+ }
+ } while (i < 0);
+ if (i)
+ { printf("(replacing");
+ }
+ else
+ { printf("(leaving");
+ }
+ if (flying)
+ { if (flystate == i)
+ { printf(" this and all the rest");
+ }
+ else if (flystate)
+ { printf(" this, replacing all the rest");
+ }
+ else
+ { printf(" this, leaving all the rest");
+ }
+ }
+ printf(")\n");
+ return(i);
+}
+
+static void add_shift(long int *a, long int e, int n)
+{
+ int i;
+
+ n --;
+ for (i=0;i<n;i++)
+ { a[i] = a[i+1];
+ }
+ a[n] = e;
+}
+
+static void process_file(char *fn)
+{
+ int i;
+ long int n;
+ int c;
+
+ workf = fopen(fn,"r+");
+ if (workf == NULL)
+ { fprintf(stderr,"%s: cannot read %s\n",__progname,fn);
+ return;
+ }
+ printf("(file: %s)\n",fn);
+ current_file = fn;
+ for (i=0;i<=MAX_C_A;i++)
+ { nls[i] = -1;
+ }
+ nls[MAX_C_A] = 0;
+ tbeg = -1;
+ if (wordmode)
+ { bufp0 = &buf[1];
+ rahead = s1l + 1;
+ buf[0] = '\0';
+ }
+ else
+ { bufp0 = &buf[0];
+ rahead = s1l;
+ }
+ if (debugging)
+ { printf("[rahead = %d, bufp0-buf = %d]\n",rahead,bufp0-&buf[0]);
+ }
+ n = 0;
+ bufp = bufp0;
+ bufpmax = &buf[sizeof(buf)-s1l-2];
+ flying = allfly;
+ flystate = 1;
+ while (1)
+ { c = getc(workf);
+ if (c == EOF)
+ { if (tbeg >= 0)
+ { if (bufp > bufp0) fwrite(bufp0,1,bufp-bufp0,tempf);
+ fseek(workf,tbeg,0);
+ n = ftell(tempf);
+ fseek(tempf,0L,0);
+ for (;n;n--)
+ { putc(getc(tempf),workf);
+ }
+ fflush(workf);
+ ftruncate(fileno(workf),ftell(workf));
+ }
+ fclose(workf);
+ return;
+ }
+ *bufp++ = c;
+ n ++;
+ if (debugging)
+ { printf("[got %c, n now %ld, bufp-buf %d]\n",c,n,bufp-bufp0);
+ }
+ if ((n >= rahead) && foundit() && doit())
+ { int wbehind;
+ if (debugging)
+ { printf("[doing change]\n");
+ }
+ wbehind = 1;
+ if (tbeg < 0)
+ { tbeg = ftell(workf) - rahead;
+ fseek(tempf,0L,0);
+ if (debugging)
+ { printf("[tbeg set to %d]\n",(int)tbeg);
+ }
+ wbehind = 0;
+ }
+ if (bufp[-1] == '\n') add_shift(nls,ftell(workf),MAX_C_A+1);
+ if ((n > rahead) && wbehind)
+ { fwrite(bufp0,1,n-rahead,tempf);
+ if (debugging)
+ { printf("[writing %ld from bufp0]\n",n-rahead);
+ }
+ }
+ fwrite(str2,1,s2l,tempf);
+ n = rahead - s1l;
+ if (debugging)
+ { printf("[n now %ld]\n",n);
+ }
+ if (n > 0)
+ { bcopy(bufp-n,bufp0,n);
+ if (debugging)
+ { printf("[copying %ld back]\n",n);
+ }
+ }
+ bufp = bufp0 + n;
+ }
+ else
+ { if (bufp[-1] == '\n') add_shift(nls,ftell(workf),MAX_C_A+1);
+ if (bufp >= bufpmax)
+ { if (tbeg >= 0)
+ { fwrite(bufp0,1,n-rahead,tempf);
+ if (debugging)
+ { printf("[flushing %ld]\n",n-rahead);
+ }
+ }
+ n = rahead;
+ bcopy(bufp-n,bufp0,n);
+ if (debugging)
+ { printf("[n now %ld]\n[copying %ld back]\n",n,n);
+ }
+ bufp = bufp0 + n;
+ }
+ }
+ }
+}
+
+static void process_indir_file(char *fn)
+{
+ char newfn[1024];
+ FILE *f;
+
+ f = fopen(fn,"r");
+ if (f == NULL)
+ { fprintf(stderr,"%s: cannot read %s\n",__progname,fn);
+ return;
+ }
+ while (fgets(newfn,sizeof(newfn),f) == newfn)
+ { newfn[strlen(newfn)-1] = '\0';
+ process_file(newfn);
+ }
+ fclose(f);
+}
+
+int main(int, char **);
+int main(int ac, char **av)
+{
+ int skip;
+ char *cp;
+
+ if (ac < 3)
+ { fprintf(stderr,"Usage: %s str1 str2 [ -w -! -noask -go -f file -F file ]\n",
+ __progname);
+ exit(1);
+ }
+ cp = getenv("TERM");
+ if (cp == 0)
+ { beginul = nullstr;
+ endul = nullstr;
+ }
+ else
+ { if (tgetent(tcp_buf,cp) != 1)
+ { beginul = nullstr;
+ endul = nullstr;
+ }
+ else
+ { cp = cap_buf;
+ if (tgetflag("os") || tgetflag("ul"))
+ { ul_ = 1;
+ }
+ else
+ { ul_ = 0;
+ beginul = tgetstr("us",&cp);
+ if (beginul == 0)
+ { beginul = tgetstr("so",&cp);
+ if (beginul == 0)
+ { beginul = nullstr;
+ endul = nullstr;
+ }
+ else
+ { endul = tgetstr("se",&cp);
+ }
+ }
+ else
+ { endul = tgetstr("ue",&cp);
+ }
+ }
+ }
+ }
+ { static char tmp[] = "/tmp/qsubst.XXXXXX";
+ int fd;
+ fd = mkstemp(&tmp[0]);
+ if (fd < 0)
+ { fprintf(stderr,"%s: cannot create temp file: %s\n",__progname,strerror(errno));
+ exit(1);
+ }
+ tempf = fdopen(fd,"w+");
+ }
+ if ( (access(av[1],R_OK|W_OK) == 0) &&
+ (access(av[ac-1],R_OK|W_OK) < 0) &&
+ (access(av[ac-2],R_OK|W_OK) < 0) )
+ { fprintf(stderr,"%s: argument order has changed, it's now: str1 str2 files...\n",__progname);
+ }
+ str1 = av[1];
+ str2 = av[2];
+ av += 2;
+ ac -= 2;
+ s1l = strlen(str1);
+ s2l = strlen(str2);
+ if (s1l > BUF_SIZ)
+ { fprintf(stderr,"%s: search string too long (max %d chars)\n",__progname,BUF_SIZ);
+ exit(1);
+ }
+ tcgetattr(0,&orig_tio);
+ signal(SIGTSTP,sigtstp);
+ allfly = 0;
+ cabove = 2;
+ cbelow = 2;
+ skip = 0;
+ for (ac--,av++;ac;ac--,av++)
+ { if (skip > 0)
+ { skip --;
+ continue;
+ }
+ if (**av == '-')
+ { ++*av;
+ if (!strcmp(*av,"debug"))
+ { debugging ++;
+ }
+ else if (!strcmp(*av,"w"))
+ { wordmode = ! wordmode;
+ }
+ else if ( (strcmp(*av,"!") == 0) ||
+ (strcmp(*av,"go") == 0) ||
+ (strcmp(*av,"noask") == 0) )
+ { allfly = 1;
+ }
+ else if ( (strcmp(*av,"nogo") == 0) ||
+ (strcmp(*av,"ask") == 0) )
+ { allfly = 0;
+ }
+ else if (**av == 'c')
+ { cabove = atoi(++*av);
+ cbelow = cabove;
+ limit_above_below();
+ }
+ else if (**av == 'C')
+ { ++*av;
+ if (**av == 'A')
+ { cabove = atoi(++*av);
+ limit_above_below();
+ }
+ else if (**av == 'B')
+ { cbelow = atoi(++*av);
+ limit_above_below();
+ }
+ else
+ { fprintf(stderr,"%s: -C must be -CA or -CB\n",__progname);
+ }
+ }
+ else if ( (strcmp(*av,"f") == 0) ||
+ (strcmp(*av,"F") == 0) )
+ { if (++skip >= ac)
+ { fprintf(stderr,"%s: -%s what?\n",__progname,*av);
+ }
+ else
+ { if (**av == 'f')
+ { process_file(av[skip]);
+ }
+ else
+ { process_indir_file(av[skip]);
+ }
+ }
+ }
+ }
+ else
+ { process_file(*av);
+ }
+ }
+ exit(0);
+}

0 comments on commit 3430639

Please sign in to comment.
Something went wrong with that request. Please try again.