Writing emacs commands

TL;DR: It took me 13 years to write an emacs lisp command.

I've been a long-time emacs user but, having come from vi, I did miss the "g" command that vi offered:

  :[range]g/pattern/command

My main use case for the "g" command was to replace a string where the line that contained the string was identified by a different string. This, I figured, could be easily performed by an emacs lisp command.

I started to develop the function in 2004, according to the initial comment. It has taken me up to September, 2017 to get it right (I think). When I encounted a problem in using the command, it was usually in the heat of something, and I didn't have time to investigate the cause. Defects manifested themselves as a hang in an endless loop.

A few days ago, I had a few hours spare and decided to spend some time fixing the one obvious defect. It involved replacing regexps containing the end-of-line character "$". As usual, it was boundary conditions that proved the main stumbling block. I found that if the new end-of-line character position was the same as point, processing for the line should end in the while loop searching for the replace-re. Since the loop must be executed at least once, a do/while loop would be good here, but emacs lisp doesn't have that. I could have considered adding an additional condition to the while loop, such that the loop would execute at least once, terminating if point was the same as the end-of-line character position. In the end, I used the catch/throw capability.

  ;;;;
  ;; global-replace is an attempt to simulate the function of the vi "g"
  ;; command.  Considers the whole buffer and replaces all occurances of
  ;; replace-re.
  ;;
  ;; mpw 2004/01/13

  (defun get-eol ()
    "Return position of end of current line."
    (save-excursion
      (end-of-line)
      (point)))

  (defun global-replace(filter-re replace-re replacement-str)
    "Emulate vi g command"
    (interactive "MFilter regexp: \nMReplace regexp: \nMReplace with: ")
    (if (and (string= filter-re "") (string= replace-re ""))
        (error "Filter and replace regexps cannot both be empty")
      (if (string= filter-re "")
          (setq filter-re replace-re))
      (if (string= replace-re "")
          (setq replace-re filter-re))
      (save-excursion
        (goto-char (point-min))
        (let ((cnt 0) (save-case case-fold-search) end)
          (setq case-fold-search '())       ; case sensitive
          (while (and (not (eobp)) (re-search-forward filter-re nil t))
            (setq end (get-eol))
            (beginning-of-line)
            (catch 'at-eol
              (while (re-search-forward replace-re end t)
                (replace-match replacement-str)
                (setq cnt (1+ cnt))
                (setq end (get-eol))
                (when (= end (point))
                  (throw 'at-eol nil))))
            (forward-line))
          (setq case-fold-search save-case)
          (message "Replaced %d strings" cnt)))))