root/usr.bin/mg/re_search.c
/*      $OpenBSD: re_search.c,v 1.38 2026/02/28 18:43:42 op Exp $       */

/* This file is in the public domain. */

/*
 *      regular expression search commands for Mg
 *
 * This file contains functions to implement several of gnuemacs's regular
 * expression functions for Mg.  Several of the routines below are just minor
 * re-arrangements of Mg's non-regular expression search functions.  Some of
 * them are similar in structure to the original MicroEMACS, others are
 * modifications of Rich Ellison's code.  Peter Newton re-wrote about half of
 * them from scratch.
 */

#ifdef REGEX
#include <sys/queue.h>
#include <sys/types.h>
#include <regex.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>

#include "def.h"
#include "macro.h"

#define SRCH_BEGIN      (0)             /* search sub-codes                 */
#define SRCH_FORW       (-1)
#define SRCH_BACK       (-2)
#define SRCH_NOPR       (-3)
#define SRCH_ACCM       (-4)
#define SRCH_MARK       (-5)

#define RE_NMATCH       10              /* max number of matches            */
#define REPLEN          256             /* max length of replacement string */

char    re_pat[NPAT];                   /* regex pattern                    */
int     re_srch_lastdir = SRCH_NOPR;    /* last search flags                */
int     casefoldsearch = TRUE;          /* does search ignore case?         */

static int       re_doreplace(RSIZE, char *);
static int       re_forwsrch(void);
static int       re_backsrch(void);
static int       re_readpattern(char *);
static int       killmatches(int);
static int       countmatches(int);

/*
 * Search forward.
 * Get a search string from the user and search for it starting at ".".  If
 * found, move "." to just after the matched characters.  display does all
 * the hard stuff.  If not found, it just prints a message.
 */
int
re_forwsearch(int f, int n)
{
        int     s;

        if ((s = re_readpattern("RE Search")) != TRUE)
                return (s);
        if (re_forwsrch() == FALSE) {
                dobeep();
                ewprintf("Search failed: \"%s\"", re_pat);
                return (FALSE);
        }
        re_srch_lastdir = SRCH_FORW;
        return (TRUE);
}

/*
 * Reverse search.
 * Get a search string from the user, and search, starting at "."
 * and proceeding toward the front of the buffer. If found "." is left
 * pointing at the first character of the pattern [the last character that
 * was matched].
 */
int
re_backsearch(int f, int n)
{
        int     s;

        if ((s = re_readpattern("RE Search backward")) != TRUE)
                return (s);
        if (re_backsrch() == FALSE) {
                dobeep();
                ewprintf("Search failed: \"%s\"", re_pat);
                return (FALSE);
        }
        re_srch_lastdir = SRCH_BACK;
        return (TRUE);
}

/*
 * Search again, using the same search string and direction as the last search
 * command.  The direction has been saved in "srch_lastdir", so you know which
 * way to go.
 *
 * XXX: This code has problems -- some incompatibility(?) with extend.c causes
 * match to fail when it should not.
 */
int
re_searchagain(int f, int n)
{
        if (re_srch_lastdir == SRCH_NOPR) {
                dobeep();
                ewprintf("No last search");
                return (FALSE);
        }
        if (re_srch_lastdir == SRCH_FORW) {
                if (re_forwsrch() == FALSE) {
                        dobeep();
                        ewprintf("Search failed: \"%s\"", re_pat);
                        return (FALSE);
                }
                return (TRUE);
        }
        if (re_srch_lastdir == SRCH_BACK)
                if (re_backsrch() == FALSE) {
                        dobeep();
                        ewprintf("Search failed: \"%s\"", re_pat);
                        return (FALSE);
                }

        return (TRUE);
}

/* Compiled regex goes here-- changed only when new pattern read */
static regex_t          regex_buff;
static regmatch_t       regex_match[RE_NMATCH];

/*
 * Re-Query Replace.
 *      Replace strings selectively.  Does a search and replace operation.
 */
int
re_queryrepl(int f, int n)
{
        int     rcnt = 0;               /* replacements made so far     */
        int     plen, s;                /* length of found string       */
        char    news[NPAT];             /* replacement string           */

        if ((s = re_readpattern("RE Query replace")) != TRUE)
                return (s);
        if (eread("Query replace %s with: ", news, NPAT,
            EFNUL | EFNEW | EFCR, re_pat) == NULL)
                return (ABORT);
        ewprintf("Query replacing %s with %s:", re_pat, news);

        /*
         * Search forward repeatedly, checking each time whether to insert
         * or not.  The "!" case makes the check always true, so it gets put
         * into a tighter loop for efficiency.
         */
        while (re_forwsrch() == TRUE) {
retry:
                update(CMODE);
                switch (getkey(FALSE)) {
                case ' ':
                        plen = regex_match[0].rm_eo - regex_match[0].rm_so;
                        if (re_doreplace((RSIZE)plen, news) == FALSE)
                                return (FALSE);
                        rcnt++;
                        break;

                case '.':
                        plen = regex_match[0].rm_eo - regex_match[0].rm_so;
                        if (re_doreplace((RSIZE)plen, news) == FALSE)
                                return (FALSE);
                        rcnt++;
                        goto stopsearch;

                case CCHR('G'):                         /* ^G */
                        (void)ctrlg(FFRAND, 0);
                        goto stopsearch;
                case CCHR('['):                         /* ESC */
                case '`':
                        goto stopsearch;
                case '!':
                        do {
                                plen = regex_match[0].rm_eo - regex_match[0].rm_so;
                                if (re_doreplace((RSIZE)plen, news) == FALSE)
                                        return (FALSE);
                                rcnt++;
                        } while (re_forwsrch() == TRUE);
                        goto stopsearch;

                case CCHR('?'):                         /* To not replace */
                        break;

                default:
                        ewprintf("<SP> replace, [.] rep-end, <DEL> don't, [!] repl rest <ESC> quit");
                        goto retry;
                }
                if (re_pat[0] == '^' && news[0] == '\0')
                        gotoeol(FFRAND, 1);
        }

stopsearch:
        curwp->w_rflag |= WFFULL;
        update(CMODE);
        if (!inmacro) {
                if (rcnt == 0)
                        ewprintf("(No replacements done)");
                else if (rcnt == 1)
                        ewprintf("(1 replacement done)");
                else
                        ewprintf("(%d replacements done)", rcnt);
        }
        return (TRUE);
}

int
re_repl(int f, int n)
{
        int     rcnt = 0;               /* replacements made so far     */
        int     plen, s;                /* length of found string       */
        char    news[NPAT];             /* replacement string           */

        if ((s = re_readpattern("RE Replace")) != TRUE)
                return (s);
        if (eread("Replace %s with: ", news, NPAT,
            EFNUL | EFNEW | EFCR, re_pat) == NULL)
                return (ABORT);

        while (re_forwsrch() == TRUE) {
                plen = regex_match[0].rm_eo - regex_match[0].rm_so;
                if (re_doreplace((RSIZE)plen, news) == FALSE)
                        return (FALSE);
                if (re_pat[0] == '^' && news[0] == '\0')
                        gotoeol(FFRAND, 1);
                rcnt++;
        }

        curwp->w_rflag |= WFFULL;
        update(CMODE);
        if (!inmacro)
                ewprintf("(%d replacement(s) done)", rcnt);
        
        return(TRUE);
}

/*
 * Routine re_doreplace calls lreplace to make replacements needed by
 * re_query replace.  Its reason for existence is to deal with \1, \2. etc.
 *  plen: length to remove
 *  st:   replacement string
 */
static int
re_doreplace(RSIZE plen, char *st)
{
        int      j, k, s, more, num, state;
        struct line     *clp;
        char     repstr[REPLEN];

        clp = curwp->w_dotp;
        more = TRUE;
        j = 0;
        state = 0;
        num = 0;

        /* The following FSA parses the replacement string */
        while (more) {
                switch (state) {
                case 0:
                        if (*st == '\\') {
                                st++;
                                state = 1;
                        } else if (*st == '\0')
                                more = FALSE;
                        else {
                                repstr[j] = *st;
                                j++;
                                if (j >= REPLEN)
                                        return (FALSE);
                                st++;
                        }
                        break;
                case 1:
                        if (*st >= '0' && *st <= '9') {
                                num = *st - '0';
                                st++;
                                state = 2;
                        } else if (*st == '\0')
                                more = FALSE;
                        else {
                                repstr[j] = *st;
                                j++;
                                if (j >= REPLEN)
                                        return (FALSE);
                                st++;
                                state = 0;
                        }
                        break;
                case 2:
                        if (*st >= '0' && *st <= '9') {
                                num = 10 * num + *st - '0';
                                st++;
                        } else {
                                if (num >= RE_NMATCH)
                                        return (FALSE);
                                k = regex_match[num].rm_eo - regex_match[num].rm_so;
                                if (j + k >= REPLEN)
                                        return (FALSE);
                                bcopy(&(clp->l_text[regex_match[num].rm_so]),
                                    &repstr[j], k);
                                j += k;
                                if (*st == '\0')
                                        more = FALSE;
                                if (*st == '\\') {
                                        st++;
                                        state = 1;
                                } else {
                                        repstr[j] = *st;
                                        j++;
                                        if (j >= REPLEN)
                                                return (FALSE);
                                        st++;
                                        state = 0;
                                }
                        }
                        break;
                }               /* switch (state) */
        }                       /* while (more)   */

        repstr[j] = '\0';
        s = lreplace(plen, repstr);
        return (s);
}

/*
 * This routine does the real work of a forward search.  The pattern is
 * sitting in the external variable "pat".  If found, dot is updated, the
 * window system is notified of the change, and TRUE is returned.  If the
 * string isn't found, FALSE is returned.
 */
static int
re_forwsrch(void)
{
        int              re_flags, tbo, tdotline, error;
        struct line     *clp;

        clp = curwp->w_dotp;
        tbo = curwp->w_doto;
        tdotline = curwp->w_dotline;

        if (tbo == clp->l_used)
                /*
                 * Don't start matching past end of line -- must move to
                 * beginning of next line, unless at end of file.
                 */
                if (clp != curbp->b_headp) {
                        clp = lforw(clp);
                        tdotline++;
                        tbo = 0;
                }
        /*
         * Note this loop does not process the last line, but this editor
         * always makes the last line empty so this is good.
         */
        while (clp != (curbp->b_headp)) {
                re_flags = REG_STARTEND;
                if (tbo != 0)
                        re_flags |= REG_NOTBOL;
                regex_match[0].rm_so = tbo;
                regex_match[0].rm_eo = llength(clp);
                error = regexec(&regex_buff, ltext(clp) ? ltext(clp) : "",
                    RE_NMATCH, regex_match, re_flags);
                if (error != 0) {
                        clp = lforw(clp);
                        tdotline++;
                        tbo = 0;
                } else {
                        curwp->w_doto = regex_match[0].rm_eo;
                        curwp->w_dotp = clp;
                        curwp->w_dotline = tdotline;
                        curwp->w_rflag |= WFMOVE;
                        return (TRUE);
                }
        }
        return (FALSE);
}

/*
 * This routine does the real work of a backward search.  The pattern is sitting
 * in the external variable "re_pat".  If found, dot is updated, the window
 * system is notified of the change, and TRUE is returned.  If the string isn't
 * found, FALSE is returned.
 */
static int
re_backsrch(void)
{
        struct line             *clp;
        int              tbo, tdotline;
        regmatch_t       lastmatch;

        clp = curwp->w_dotp;
        tbo = curwp->w_doto;
        tdotline = curwp->w_dotline;

        /* Start search one position to the left of dot */
        tbo = tbo - 1;
        if (tbo < 0) {
                /* must move up one line */
                clp = lback(clp);
                tdotline--;
                tbo = llength(clp);
        }

        /*
         * Note this loop does not process the last line, but this editor
         * always makes the last line empty so this is good.
         */
        while (clp != (curbp->b_headp)) {
                regex_match[0].rm_so = 0;
                regex_match[0].rm_eo = llength(clp);
                lastmatch.rm_so = -1;
                /*
                 * Keep searching until we don't match any longer.  Assumes a
                 * non-match does not modify the regex_match array.  We have to
                 * do this character-by-character after the first match since
                 * POSIX regexps don't give you a way to do reverse matches.
                 */
                while (!regexec(&regex_buff, ltext(clp) ? ltext(clp) : "",
                    RE_NMATCH, regex_match, REG_STARTEND) &&
                    regex_match[0].rm_so <= tbo) {
                        memcpy(&lastmatch, &regex_match[0], sizeof(regmatch_t));
                        regex_match[0].rm_so++;
                        regex_match[0].rm_eo = llength(clp);
                }
                if (lastmatch.rm_so == -1) {
                        clp = lback(clp);
                        tdotline--;
                        tbo = llength(clp);
                } else {
                        memcpy(&regex_match[0], &lastmatch, sizeof(regmatch_t));
                        curwp->w_doto = regex_match[0].rm_so;
                        curwp->w_dotp = clp;
                        curwp->w_dotline = tdotline;
                        curwp->w_rflag |= WFMOVE;
                        return (TRUE);
                }
        }
        return (FALSE);
}

/*
 * Read a pattern.
 * Stash it in the external variable "re_pat". The "pat" is
 * not updated if the user types in an empty line. If the user typed
 * an empty line, and there is no old pattern, it is an error.
 * Display the old pattern, in the style of Jeff Lomicka. There is
 * some do-it-yourself control expansion.
 */
static int
re_readpattern(char *re_prompt)
{
        static int      dofree = 0;
        int             flags, error, s;
        char            tpat[NPAT], *rep;

        if (re_pat[0] == '\0')
                rep = eread("%s: ", tpat, NPAT, EFNEW | EFCR, re_prompt);
        else
                rep = eread("%s (default %s): ", tpat, NPAT,
                    EFNUL | EFNEW | EFCR, re_prompt, re_pat);
        if (rep == NULL)
                return (ABORT);
        if (rep[0] != '\0') {
                /* New pattern given */
                (void)strlcpy(re_pat, tpat, sizeof(re_pat));
                if (casefoldsearch)
                        flags = REG_EXTENDED | REG_ICASE;
                else
                        flags = REG_EXTENDED;
                if (dofree)
                        regfree(&regex_buff);
                error = regcomp(&regex_buff, re_pat, flags);
                if (error != 0) {
                        char    message[256];
                        regerror(error, &regex_buff, message, sizeof(message));
                        dobeep();
                        ewprintf("Regex Error: %s", message);
                        re_pat[0] = '\0';
                        return (FALSE);
                }
                dofree = 1;
                s = TRUE;
        } else if (rep[0] == '\0' && re_pat[0] != '\0')
                /* Just using old pattern */
                s = TRUE;
        else
                s = FALSE;
        return (s);
}

/*
 * Cause case to not matter in searches.  This is the default.  If called
 * with argument cause case to matter.
 */
int
setcasefold(int f, int n)
{
        if (f & FFARG) {
                casefoldsearch = FALSE;
                ewprintf("Case-fold-search unset");
        } else {
                casefoldsearch = TRUE;
                ewprintf("Case-fold-search set");
        }

        /*
         * Invalidate the regular expression pattern since I'm too lazy to
         * recompile it.
         */
        re_pat[0] = '\0';
        return (TRUE);
}

/*
 * Delete all lines after dot that contain a string matching regex.
 */
int
delmatchlines(int f, int n)
{
        int     s;

        if ((s = re_readpattern("Flush lines (containing match for regexp)"))
            != TRUE)
                return (s);

        s = killmatches(TRUE);
        return (s);
}

/*
 * Delete all lines after dot that don't contain a string matching regex.
 */
int
delnonmatchlines(int f, int n)
{
        int     s;

        if ((s = re_readpattern("Keep lines (containing match for regexp)"))
            != TRUE)
                return (s);

        s = killmatches(FALSE);
        return (s);
}

/*
 * This function does the work of deleting matching lines.
 */
static int
killmatches(int cond)
{
        int      s, error;
        int      count = 0;
        struct line     *clp;

        clp = curwp->w_dotp;
        if (curwp->w_doto == llength(clp))
                /* Consider dot on next line */
                clp = lforw(clp);

        while (clp != (curbp->b_headp)) {
                /* see if line matches */
                regex_match[0].rm_so = 0;
                regex_match[0].rm_eo = llength(clp);
                error = regexec(&regex_buff, ltext(clp) ? ltext(clp) : "",
                    RE_NMATCH, regex_match, REG_STARTEND);

                /* Delete line when appropriate */
                if ((cond == FALSE && error) || (cond == TRUE && !error)) {
                        curwp->w_doto = 0;
                        curwp->w_dotp = clp;
                        count++;
                        s = ldelete(llength(clp) + 1, KNONE);
                        clp = curwp->w_dotp;
                        curwp->w_rflag |= WFMOVE;
                        if (s == FALSE)
                                return (FALSE);
                } else
                        clp = lforw(clp);
        }

        ewprintf("%d line(s) deleted", count);
        if (count > 0)
                curwp->w_rflag |= WFMOVE;

        return (TRUE);
}

/*
 * Count lines matching regex.
 */
int
cntmatchlines(int f, int n)
{
        int     s;

        if ((s = re_readpattern("Count lines (matching regexp)")) != TRUE)
                return (s);
        s = countmatches(TRUE);

        return (s);
}

/*
 * Count lines that fail to match regex.
 */
int
cntnonmatchlines(int f, int n)
{
        int     s;

        if ((s = re_readpattern("Count lines (not matching regexp)")) != TRUE)
                return (s);
        s = countmatches(FALSE);

        return (s);
}

/*
 * This function does the work of counting matching lines.
 */
int
countmatches(int cond)
{
        int      error;
        int      count = 0;
        struct line     *clp;

        clp = curwp->w_dotp;
        if (curwp->w_doto == llength(clp))
                /* Consider dot on next line */
                clp = lforw(clp);

        while (clp != (curbp->b_headp)) {
                /* see if line matches */
                regex_match[0].rm_so = 0;
                regex_match[0].rm_eo = llength(clp);
                error = regexec(&regex_buff, ltext(clp) ? ltext(clp) : "",
                    RE_NMATCH, regex_match, REG_STARTEND);

                /* Count line when appropriate */
                if ((cond == FALSE && error) || (cond == TRUE && !error))
                        count++;
                clp = lforw(clp);
        }

        if (cond)
                ewprintf("Number of lines matching: %d", count);
        else
                ewprintf("Number of lines not matching: %d", count);

        return (TRUE);
}
#endif  /* REGEX */