Emacs Lisp: Cycle Replace Space Hyphen Underscore

Perm url with updates: http://xahlee.org/emacs/elisp_change_space-hyphen_underscore.html

Emacs Lisp: Cycle Replace Space Hyphen Underscore

Xah Recommends:
Amazon Kindle. Read books under the sun. Review

Xah Lee, 2010-10-05

This page shows a example of writing a emacs lisp command that changes space to underscore of the current line, or among hypen, underscore, space. If you don't know elisp, first take a look at Emacs Lisp Basics.

The Problem

I often need to change all underscore “_” characters to space, or hypen “-” to underscore, or any combination between them.

This is most often used on file name, or moving file name to article title.


In the beginning, i simply wrote these commands:

(defun space2underscore-region (start end)
  "Replace space by underscore in region."
  (interactive "r")
    (narrow-to-region start end)
    (goto-char (point-min))
    (while (search-forward " " nil t) (replace-match "_")) ) )

(defun underscore2space-region (start end)
  "Replace underscore by space in region."
  (interactive "r")
    (narrow-to-region start end)
    (goto-char (point-min))
    (while (search-forward "_" nil t) (replace-match " ")) ))

The code is very simple to understand.


After a few months, i find it annoying that i have to choose which command to call. So, i thought: why not create a function that simply toggles? So, i wrote this:

(defun replace-underscore-space-toggle ()
  "Replace underscore/space in the current region or line.
If the current line contains more “_” char than space,
then replace them to space, else replace space to _.
If there's a text selection, work on the selected text."
  (let (li bds)
    (setq bds
          (if (region-active-p)
              (cons (region-beginning) (region-end))
            (bounds-of-thing-at-point 'line)))
    (setq li (buffer-substring-no-properties (car bds) (cdr bds)))
    (if (> (count 32 li) (count 95 li))
        (progn (replace-string " " "_" nil (car bds) (cdr bds)))
      (progn (replace-string "_" " " nil (car bds) (cdr bds))))))

The code is smart. If you have a text selection, it works on the text selection, else the current line. Also, it looks at your text and count the number of occurrence of “_” and “ ”. If there are more “_” than “ ”, then it replaces it that way, else it does the other direction.


After using this for 1 year, today i also find that sometimes i need to replace hypen “-” to underscore. At first i just quickly wrote a “hypen2space-region”, but quickly realized that i've got too many special case functions. It would be great, to have a function that cycle between these chars. Here it is:

(defun cycle-hyphen-underscore-space ()
  "Replace underscore to space, or hypen, in region or current line.
If there's a text selection, apply on that, else, the current line.
When called repeatedly, this command cycles the “ ”, “_”, “-” characters."
  ;; this function sets a property “state”. Possible values are 0 to length of charList.
  (let (meat charList p1 p2 currentState nextState changeFrom
             changeTo startedWithRegion-p )

    (if (region-active-p)
          (setq startedWithRegion-p t )
          (setq p1 (region-beginning))
          (setq p2 (region-end))
      (progn (setq startedWithRegion-p nil ) 
             (setq p1 (line-beginning-position))
             (setq p2 (line-end-position)) ) )

    (setq charList (list " " "_" "-" ))

    (setq currentState
          (if (get 'cycle-hyphen-underscore-space 'state) 
              (get 'cycle-hyphen-underscore-space 'state) 0))
    (setq nextState (% (+ currentState (length charList) 1) (length charList)))

    (setq changeFrom (nth currentState charList))
    (setq changeTo (nth nextState charList))

    (setq meat (replace-regexp-in-string changeFrom changeTo (buffer-substring-no-properties p1 p2)) )
    (delete-region p1 p2)
    (insert meat)
    (message "Changed “%s” to “%s”" changeFrom changeTo )
    (put 'cycle-hyphen-underscore-space 'state nextState)

    (when startedWithRegion-p 
      (goto-char p2)
      (set-mark p1)
      (setq deactivate-mark nil) ) ) )

Here's the gist of how this function works.

The function sets up a state. So when called repeatedly, it knows which to cycle to.

The state is done in elisp as “properties”. In htm/xml, a tag can have several pairs of name/value attributes. This is similar to lisp's “properties”. Any elisp function can have a property. A property is basically name/value pairs. A function can have any number of properties, each with any name. The name's type can be a elisp “symbol”, “string”, or others. You can get and set properties using “get” and “put”. (info "(elisp) Property Lists")

In our function, first we set a character list like this 「(setq charList (list " " "_" "-" ))」. Our property name is “state”, and possible values are integers, 0, 1, 2, corresponding to the index of charList.

To cycle thru states, we just use modular arithmetics. For example, if current state is “n”, then next state is “mod(n+1,2)”. Here's the corresponding code:

(setq currentState
      (if (get 'cycle-hyphen-underscore-space 'state) 
          (get 'cycle-hyphen-underscore-space 'state) 0))
(setq nextState (% (+ currentState (length charList) 1) (length charList)))

The rest of the code is easy to understand.

When this command is called with a text selection, by default emacs will de-activate the text selection after a command is finished. However, for this command, we want the text selection to stay, because user might call the command again to cycle replace. So, if region is active, we set “startedWithRegion-p” to true. At the end of the code, we restore the region's active status. Emacs has a global variable “deactivate-mark” used to control whether the mark is automatically de-activated when a command is called (when “transient-mark-mode” is on). When emacs invokes a command, it sets this variable to true. When the command is finished, emacs checks this variable to see if it should de-activate the mark. So, in our code, at the end we put: 「(setq deactivate-mark nil)」.

You can define a hotkey for any of the above commands. See: How to Define Keyboard Shortcuts in Emacs.

Without these commands, the quickest way to replace underscore to space is by selecting a region then 【Alt+x replace-string】 then “_” Enter, “ ”, Enter. However, this is several keystrokes more, and requires that half a second of brainwork. When you need do this daily many times a day, a custome command with a brainless push of button makes it easier.

Emacs is fantastic!


How about writing a function that cycles your favorite fonts? (answer: How to Quickly Switch Fonts in Emacs). How about cycling among your most frequently used files? or perhaps your several org-mode files?

Was this page useful? If so, please do donate $3, thank you donors!

Popular posts from this blog

11 Years of Writing About Emacs

does md5 creates more randomness?

Google Code shutting down, future of ErgoEmacs