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)))))