3

How can I do the equivalent of query-replace-regexp for multiple regexps?

For instance, if regexp1 is matched, it should be replaced by text1, and if regexp2 is matched, it should be replaced by text2 (with interactive confirmation as with query-replace-regexp). The search and replace needs to be done in one pass for all the regexps (I don't want to use query-replace-regexp on the first regexp, then on the second regexp, and so on).

While querying could be done with a single regexp like regexp1\|regexp2, I don't know how I could do the replacement depending on whether regexp1 or regexp2 is matched. When query-replace-regexp is called interactively, \, can be used to execute a Lisp expression, which could do the selection, but this is not possible for code put in my .emacs, for instance.

Drew
  • 75,699
  • 9
  • 109
  • 225
vinc17
  • 183
  • 6
  • Note that you might get different results depending on the order of the replacements. IMO it is somewhat dangerous to try to do more than one, so I do them one at a time and check in-between to make sure that I got what I wanted. – NickD Apr 20 '21 at 13:08
  • @NickD In my case, I make sure that there cannot be any overlap. – vinc17 Apr 21 '21 at 20:57

1 Answers1

2

Try this. Enter an empty regexp interactively to stop adding regexp/replacement pairs, and start replacements.

(defun query-multi-replace-regexp (&rest pairs)
  "Query replace for each regexp and replacement string in PAIRS."
  (interactive
   (let (pairs regexp replacement)
     (while (and (setq regexp (read-regexp "Query replace regexp"))
                 (not (string= regexp "")))
       (push regexp pairs)
       (push (read-string (format "Query replace regexp %s with: " regexp))
             pairs))
     (nreverse pairs)))
  (let ((pos pairs)
        patterns)
    (while pos
      (push (pop pos) patterns)
      (pop pos))
    (perform-replace
     (concat "\\(?:" (mapconcat 'identity patterns "\\|") "\\)")
     (cons (lambda (pairs count)
             (catch 'replacement
               (while pairs
                 (let ((regexp (pop pairs))
                       (string (pop pairs)))
                   (when (string-match-p regexp (match-string 0))
                     (throw 'replacement string))))))
           pairs)
     :query :regexp nil)))

See the definition of query-replace-regexp if you want to support things like replacement in a region. It shows how to obtain the arguments for perform-replace.

phils
  • 48,657
  • 3
  • 76
  • 115
  • This works well with simple replacements, but has an issue with capture groups. For instance, instead of `(query-multi-replace-regexp "te\\(\\w*\\)" "A-\\1-" "\\(..\\)fer" "B-\\1-")`, one needs `(query-multi-replace-regexp "te\\(\\w*\\)" "A-\\2-" "\\(..\\)fer" "B-\\1-")` here. Worse than that, because of this issue, one quickly reaches the limit of 9 capture groups (for my needs, each regexp can have up to 5 capture groups). – vinc17 Apr 21 '21 at 20:52
  • This actually seems to work with explicitly numbered groups, by reusing the number from 1: `(query-multi-replace-regexp "te\\(?1:\\w*\\)" "A-\\1-" "\\(?1:..\\)fer" "B-\\1-")`. There's no ambiguity, so that this is safe. I'll do some more tests. – vinc17 Apr 21 '21 at 21:31
  • I suspect that's safe, yes. `C-h i g (elisp)Regexp Backslash` says that "you can have several groups with the same number in which case the last one to match (i.e., the rightmost match) will win." – phils Apr 21 '21 at 21:51
  • You can alternatively test changing `string-match-p` to `string-match` without the `-p` suffix. They're the same, except that the former does *not* change the match data, and the latter does. I was intentionally not changing the match data to ensure that I didn't mess with anything `perform-replace` was doing, but it might (a) be fine and (b) address your issue. Untested by me. – phils Apr 21 '21 at 21:55