If you have never used wgrep with rg.el to rename a function in several files, try it | that will blow your mind
Hey Emacsers,
Have you ever needed to rename a function that appears in several files?
Let's see how we can do this with Emacs.
In the post the fantastic rg.el, we've seen that rg.el
is a nice Emacs
interface to the cli ripgrep
which lets us do searches for regexp in
files interactively with rg
command, get the results in a dedicated
buffer *rg*
(by default), browse those matches, modify the searches
parameters and modify the matched regexps, all from within the
dedicated buffer *rg*
.
In this post we see how to rename interactively a function that appears in several files using rg.el and wgrep!
Let's go ;)
Initial state
Let assume that we are working on the org-mode
code base
git clone https://git.savannah.gnu.org/git/emacs/org-mode.git
and we want to rename the function org-link-expand-abbrev
(that
replaces link abbreviations in a given org link, read its dedicated
section in the post search options and link abbreviations for more
details) into org-link-RENAMED
like this:
org-link-expand-abbrev -> org-link-RENAMED
We use git
(in a terminal) to "monitor" our changes in the code base
and to revert back to the initial state at the end of this
"demonstration".
First, running git status
tells us that we are on the branch main
, we
have nothing to commit and our working tree is clean:
git status
prints:
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
We can obtain the current commit (on which I'm running the example, your ouptuts might differ a little bit if you're checked out at another commit) by running this following command:
git rev-parse --short HEAD
that prints:
685d78f63
Now that we are clear about the initial state, we can continue.
Call wgrep-change-to-wgrep-mode, make changes and abort changes with wgrep-abort-changes
Let's search for the regexp org-link-expand-abbrev
(that exactly matches
the string org-link-expand-abbrev
) in org-mode
directory using rg.el
:
M-x rg
,write
org-link-expand-abbrev
,select the directory where
org-mode
source code is,choose
all
as type file.
We get the following buffer named *rg*
(in the mode rg-mode
) that
shows that we've matched org-link-expand-abbrev
twice, once in the
file lisp/ol.el and once in the file lisp/org-element.el:
-*- mode: rg; default-directory: "/tmp/org-mode/" -*-
rg started at Mon Apr 18 13:03:59
/usr/bin/rg [...]
File: [ol.el] lisp/ol.el
1011 (defun org-link-expand-abbrev (link)
File: [org-element.el] lisp/org-element.el
3497 (setq raw-link (org-link-expand-abbrev
rg finished (2 matches found) at Mon Apr 18 13:03:59
Now in the buffer *rg*
, we press e
(bound to
wgrep-change-to-wgrep-mode
) and two things happens:
the matched lines are now editable in the buffer
*rg*
and,the keymap
wgrep-mode-map
becomes the local map.
Then, in *rg*
buffer, we transform org-link-expand-abbrev
into
org-link-RENAMED
the way we prefer (we have all the Emacs power, some
of us might use query-replace
, other might use multiple-cursors.el,
other iedit, etc.). And so *rg*
buffer looks like this:
-*- mode: rg; default-directory: "/tmp/org-mode/" -*-
rg started at Mon Apr 18 13:03:59
/usr/bin/rg [...]
File: [ol.el] lisp/ol.el
1011 (defun org-link-RENAMED (link)
File: [org-element.el] lisp/org-element.el
3497 (setq raw-link (org-link-RENAMED
rg finished (2 matches found) at Mon Apr 18 13:03:59
Now that we've finished editing the buffer *rg*
, we change our mind
and finally decide that we no longer want to apply those changes to
the corresponding files.
No problem, we just have to hit C-c C-k
(bound to wgrep-abort-changes
)
to abort the changes. We're back to the "normal" *rg*
buffer where
nothing is editable and none of our changes have been taken into
account:
-*- mode: rg; default-directory: "/tmp/org-mode/" -*-
rg started at Mon Apr 18 13:03:59
/usr/bin/rg [...]
File: [ol.el] lisp/ol.el
1011 (defun org-link-expand-abbrev (link)
File: [org-element.el] lisp/org-element.el
3497 (setq raw-link (org-link-expand-abbrev
rg finished (2 matches found) at Mon Apr 18 13:03:59
At that point maybe you should (must) stop me and ask:
Are we really 'back to normal'?
How can I be sure that my files haven't been compromised?
Could you prove it?
As we started with a clean working tree in a git
repository with
nothing to commit, we just have to run the command:
git status
that prints:
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
This way, we can be sure that none of our files have been modified.
Note that when we are editing the buffer *rg*
, until we explicitly
run a command (like wgrep-abort-changes
) of wgrep
package, nothing is
reflected in the file system (neither in the buffers that are visiting
files that could be modified by wgrep
, for instance in our case
lisp/ol.el and lisp/org-element.el).
Changes applied to the file system: wgrep-finish-edit and wgrep-save-all-buffers
Now, let's modify again the *rg*
buffer, the same way as before
(starting by pressing e
(bound to wgrep-change-to-wgrep-mode
) to make
the buffer editable):
-*- mode: rg; default-directory: "/tmp/org-mode/" -*-
rg started at Mon Apr 18 13:03:59
/usr/bin/rg [...]
File: [ol.el] lisp/ol.el
1011 (defun org-link-RENAMED (link)
File: [org-element.el] lisp/org-element.el
3497 (setq raw-link (org-link-RENAMED
rg finished (2 matches found) at Mon Apr 18 13:03:59
This time we want to save those changes in the buffer *rg*
and want
to see them reflected in the corresponding files.
To do so, we press C-x C-s
(bound to wgrep-finish-edit
) and we see in
the echo area:
Successfully finished. (2 changed)
We might think that those changes have been reflected in the file
sytem but this is not the case by default and we can check it as we
did before by running the command git status
.
In the buffer *rg*
that is no longer editable and that took into
account those changes, we can do two things:
navigate between the matched lines that we've changed pressing
n
orp
. We see the changes reflected in the buffersol.el
(visiting the file lisp/ol.el) andorg-element.el
(visiting the file lisp/org-element.el). We also observe that those modifications are not saved in the buffers. And if we change our mind again and we no longer want those changes to be applied, in each buffer we can "manually" undo those changes.if we want those changes to be reflected in the file system, we can call the command
wgrep-save-all-buffers
.
We decide to save all the buffers, and so we run:
M-x wgrep-save-all-buffers
This time our our changes have been reflected in the file system and we can check it by running the following command:
git status
that prints:
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lisp/ol.el
modified: lisp/org-element.el
no changes added to commit (use "git add" and/or "git commit -a")
So the files lisp/ol.el and lisp/org-element.el have been modified.
To be sure that those modifications correspond to our renaming, we
can run the following command that prints the git
difference between
the last commit and the unstaged modified files:
git diff
diff --git a/lisp/ol.el b/lisp/ol.el
index 1b2bb9a9a..642dcb5da 100644
--- a/lisp/ol.el
+++ b/lisp/ol.el
@@ -1008,7 +1008,7 @@ and then used in capture templates."
if store-func
collect store-func))
-(defun org-link-expand-abbrev (link)
+(defun org-link-RENAMED (link)
"Replace link abbreviations in LINK string.
Abbreviations are defined in `org-link-abbrev-alist'."
(if (not (string-match "^\\([^:]*\\)\\(::?\\(.*\\)\\)?$" link)) link
diff --git a/lisp/org-element.el b/lisp/org-element.el
index 28339c1b8..cbfcfe074 100644
--- a/lisp/org-element.el
+++ b/lisp/org-element.el
@@ -3494,7 +3494,7 @@ Assume point is at the beginning of the link."
;; (e.g., insert [[shell:ls%20*.org]] instead of
;; [[shell:ls *.org]], which defeats Org's focus on
;; simplicity.
- (setq raw-link (org-link-expand-abbrev
+ (setq raw-link (org-link-RENAMED
(org-link-unescape
(replace-regexp-in-string
"[ \t]*\n[ \t]*" " "
If we were in a refactoring phase in our development where we've
decided to rename org-link-expand-abbrev
by org-link-RENAMED
, the next
step would be to commit those changes.
As this is not our case (and also to demonstrate how to revert back ALL the changes not commited that we've made in a git repository) we prefer to revert back to the last commit by running the following command:
git checkout .
And we can verify that we're back to our original state by running the
following commands git status
and git rev-parse --short HEAD
as we did
at the beginning of this post.
Make the changes automatic with wgrep-auto-save-buffer
As written in the documentation of wgrep
, if we want to save the
buffers automatically when we call wgrep-finish-edit
(and so apply
the changes in the file system), we can set the variable
wgrep-auto-save-buffer
to t
like this:
(setq wgrep-auto-save-buffer t)
We could have used sed to do it non interactively
Renaming a function like we did before with rg.el
and wgrep
could also
be done using the cli sed
(that can search some regexp in files
(not only) and replace matches in-place with another string) combined
with eiter find
or grep
to list the files we want to modify which are
"passed" to sed
using the utility xargs
.
Specifically, in org-mode
directory, we can replace the occurences of
org-link-expand-abbrev
by org-link-RENAMED
, by running the following
command line (in a terminal):
find . -type f -print0 | xargs -0 sed -i 's/org-link-expand-abbrev/org-link-RENAMED/g'
-print0
tellsfind
to separate file names with the null character,-0
tellsxargs
that arguments are separated by the null character,-i
command line flag tellssed
to do the substitions (commandsd
ofsed
) oforg-link-expand-abbrev
byorg-link-RENAMED
in-place and,the flag
g
(in's/.../.../g'
) tellssed
to apply the replacement to all matches not just the first.
Instead of using find
, we could have use grep
to list not all the
files in org-mode
directory but only those that contains
org-link-expand-abbrev
. And doing so, we would have made the same
replacements. Here is the full command line to run in a terminal
that produces the same result:
grep -rlZ 'org-link-expand-abbrev' | xargs -0 sed -i 's/org-link-expand-abbrev/org-link-RENAMED/g'
r
flag tellsgrep
to search recursively in the current directory,l
flag tellsgrep
to print only file names (not the matches),Z
flag tells grep to print the null characher after each file names,after the pipe
|
, it's the same as before.
WE ARE DONE!!!