root/usr.bin/qsubst/qsubst.c
/*      $NetBSD: qsubst.c,v 1.8 2004/11/01 21:36:11 dsl Exp $   */

/*
 * 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 <sys/cdefs.h>

#ifndef lint
__RCSID("$NetBSD: qsubst.c,v 1.8 2004/11/01 21:36:11 dsl Exp $");
#endif

#include <sys/file.h>

#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <termcap.h>
#include <termios.h>
#include <unistd.h>

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 tbeg;
static FILE *workf;
static char *str1;
static char *str2;
static int s1l;
static int s2l;
static long 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);
}

/* ARGSUSED */
static void 
sigtstp(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(unsigned 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(int c)
{
        return (putchar(c));
}

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, &otio);
        return (c);
}

static int 
doit(void)
{
        long 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 *a, long 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 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 = %ld]\n",
                    rahead, (long) (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 %ld]\n",
                            c, n, (long) (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 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);
}