Elisp: Replace String Based on File Name

By Xah Lee. Date: . Last updated: .

This page shows how to use elisp to do interactive find replace for all files in a directory, with file's name as a replacement string.

Problem

I want to create a navigation bar for online novel in HTML, by inserting {Next Chapter, Previous Chapter} links at the top. The chapter links need to be based on the current file.

Detail

I have many books in HTML form. 〔see Literature Classics〕 Usually, the file names have this pattern: {chap1.html, chap2.html, chap3.html, etc}.

Each file is a chapter of a book. In each file, I need to place a navigation bar, so that there's a Next Chapter and Previous Chapter links at the bottom of each page.

Normally, this can be done by writing a short Python or Ruby script. The script will open the file, parse the file name so that it knows which chapter this file is. If the current file is chapter3.html, then the script will generate a string like this:

<div class="navbar">
<a href="chapter2.html">PREVIOUS</a>|
<a href="index.html">TOP</a>|
<a href="chapter4.html">NEXT</a>
</div>

The job can be done in about 20 minutes. Basically, your script will traverse a directory and determine which files to process. For each file, it parses the file name and generate the nav bar string. Then, your script will open the file, insert the nav bar at the appropriate place, then close the file. Your script will need do a backup if you want it to be robust. You'll also have to make sure that the file's owner, group, permissions etc meta data are kept intact. In the end, some simple script can end up taking twice or trice the time you expected.

With elisp, it's much easier. You only need to write half of the code, because the file selection, file opening and reading, text decoding, backing up, saving and encoding, are all part of the emacs environment. All you really need to write is a elisp function that takes in a file name and returns the navigation bar string. This significantly saves your time. As a added benefit, you get to do this in a interactive, visual process (or batch if you want). So, errors are much reduced, and you don't have to worry about your code making a mistake erasing parts of the file or missing some files.

Solution

First, mark the files you want to process in dired. Then, use dired-do-query-replace-regexp to do a find replace operation on a particular string. For example, replace <body> with <body> navbar_string. (For a tutorial on using dired-do-query-replace-regexp, see: Emacs: Interactive Find Replace Text in Directory. )

The trick lies in your replacement string. You want to use a elisp function that returns the appropriate nav bar string for the chapter.

In emacs 22 (released in ), there's a new feature that allows you to put a elisp function as your replacement string. This is done by giving the replacement string this form \,(functionName), where “functionName” is the name of your function. So, if the function that returns the nav bar string is called “ff”, then in your replacement string you can say \,(ff).

Here is the function:

(defun ff ()
  "Returns a navigation bar string with Prev and Next links based on the current file name."
  (interactive)
  (let (fName navbarStr chapterNum )
    (setq fName (file-name-nondirectory (buffer-file-name)) )
    (setq chapterNum (string-to-number (substring (file-name-sans-extension fName) 4)))
    (setq navbarStr
          (concat "<div class=\"nav\"><a href=\"" "chap"
                  (number-to-string (- chapterNum 1))
                  ".html" "\">◀</a> <a href=\"index.html\">▲</a> <a href=\"" "chap"
                  (number-to-string (+ chapterNum 1))
                  ".html" "\">▶</a> Flatland</div>"))
    navbarStr
    ) )

In the above code, the buffer-file-name returns the full path of the file of the current buffer. The file-name-nondirectory truncates the path to just the file name. The line (string-to-number (substring (file-name-sans-extension fName) 4)) extracts the chapter number from the file name.

For the last chapter, the code will generate extraneous “Next Chapter” tag. We can put extra code to check for that case, but it's easier just to manually fix it. Similarly for first chapter.

For a example of a online book with Next/Previous navigation bar, see: Flatland.

Example 2

Today, i need to do similar again. I have a dir with names like this:

x001-1.html
x001-2.html
x002-1.html
x002-2.html
x003-1.html
x003-2.html
…

Note that the file names are a bit unusual. Each chapter has 2 pages. So, the previous chapter is not simply the current file chapter number minus one.

These are pages for the novel Journey To The West (Monkey King). The first part of the file name is the chapter number. Each chapter has 2 HTML pages, indicated in the second part of the file name.

In each page, there's a nav bar code like this:

<div class="nav">
<a href="monkey_king.html" title="up">▲</a>
<a href="x002-2.html" title="next">▶</a>
</div>

It is missing a nav bar button to go to the previous page. I'd like to fix it, so it should be like this:

<div class="nav">
<a href="x001-2.html" title="previous">◀</a>
<a href="monkey_king.html" title="up">▲</a>
<a href="x002-2.html" title="next">▶</a>
</div>

So, the task is to add this:

<a href="‹previous chapter file name›" title="previous">◀</a>

to every page, and the link depends on the current file name.

Solution

Here's what i do to solve it, in the quickest way i know possible with emacs.

First, use find replace on all files. Replace all occurrence of

<div class="nav">
<a href="monkey_king.html" title="up">▲</a>

with

<div class="nav">
<a href="htnshtns" title="previous">◀</a>
<a href="monkey_king.html" title="up">▲</a>

The htnshtns is just a random string. We use a fixed random string so that we can do multi-file find replace one more time, where the find string will be this fixed string, and the replace string will be generated by a lisp function that returns the right file name.

(If you don't know how to do find replace on multiple files, see: Emacs: Interactive Find Replace Text in Directory.)

Now, here's the lisp code quickly written:
(defun ff ()
  "…"
  (interactive)
  (let (fName myList chapterNum pageNum chapterNumNew pageNumNew )

    (setq fName (file-name-nondirectory (buffer-file-name)) )

    (setq myList (split-string (substring (file-name-sans-extension fName) 1) "-" ) )

    (setq chapterNum (string-to-number (nth 0 myList) ))
    (setq pageNum (string-to-number (nth 1 myList) ))

    (if (= pageNum 1)
        (progn
          (setq chapterNumNew (- chapterNum 1))
          (setq pageNumNew 2)
          )
      (progn
        (setq chapterNumNew chapterNum)
        (setq pageNumNew 1) ) )

    (concat "x" (format "%03d" chapterNumNew) "-" (format "%d" pageNumNew) ) ) )

So, with this code, i just call find/replace, with find string htnshtns, and replace value of \,(ff)

Function as Replacement String