Up: Code | [Related] «^» «T» |
Monday, March 19, 2001
Emacs Notepad
By Paul Ford
Make emacs tell time, open files quickly, and talk to Perl.
In this piece I describe several simple utility functions I wrote for the text editor emacs, and which I include in my .emacs configuration file. I use each of these functions several times a day, and believe they may useful to others.
If you don't use emacs, these will be of limited interest. Sorry. Scott Rahin promises to write more soon.
For the totally uninitiated, emacs is a text editor popular with programmers. It runs on the Unix, Mac, and Windows operating systems, and has a built in "extension" language based on LISP. Emacs is like a skyscraper - it's huge and monolithic and hard to get into unless you know where you're supposed to be going. But I really like editing text in emacs, and I've tried everything, from MSWord to ancient folding editors last updated in 1985.
Throughout this piece, I refer to the .emacs ("dot ee-macks") file. This file lives in your home directory and contains LISP code that emacs runs every time you start the program. Your .emacs file can be used to contain all the stuff that's specific to you and how you work.
Inserting the Time
While it provides many features for time-stamping documents, emacs lacks a flexible way to drop in a formatted date string at any point in a document. The answer is a very simple function:
(defun insert-time () (interactive) (insert (format-time-string "%Y-%m-%d-%R")))
Now, when I hit M-x insert-time, emacs inserts a string like this: 2001-03-19-02:10. For the unitiated, M-x means "Meta-X" or "Escape-X"; it's a command sequence. Here's what's going on in the code:
(defun insert-time ()
Define a function called insert-time. It takes no arguments.
(interactive)
This tells emacs that the function can be called while editing.
(insert (format-time-string "%Y-%m-%d-%R")))
insert tells the program to insert what's next; format-time-string performs the standard Unix formatting on the time. You can learn more about time formatting by typing man date in the Unix shell.
The Same Thing, Slightly Different
Let's look at another date formatting function.
(defun insert-ISO () (interactive) (insert (format-time-string "%Y%m%d")))
As you can see, it's the same idea - except now we're putting out an ISO-formatted YYYYMMDD year, like so: 20010319.
If you felt extravagant, you could create something like this:
(defun insert-date-verbose () (interactive) (insert (format-time-string "It is now second %S of minute %M of hour %H (%l %p) on day %d of %B in year %Y in time zone %Z. It is %A, and day %j, in week %U, of %Y. ")))
M-x insert-date-verbose produces: It is now second 41 of minute 44 of hour 02 ( 2 AM) on day 19 of March in year 2001 in time zone EST. It is Monday, and day 078, in week 11, of 2001.
Aliasing Frequently Used Files to Functions
When writing, I find myself opening the same files over and over again, and I get sick of typing in the full path of the file and tab-completing all over the place, trying to match my current mental space to a unique file name. For the files I use most often, I create aliases like so:
(defun go-story () (interactive) (find-file "~/www/source/xml/ftrain_story.xml")) (defun go-theory () (interactive) (find-file "~/www/source/xml/ftrain_theory.xml"))
Now, when I'm editing any document, I simply type M-x go-story and the file "~www/source/xml/ftrain_story.xml" is opened into my buffer and ready to go. Typing M-x go-theory takes me to a different file. And if I just type M-x go- and then hit tab, emacs will list all my "go-*" aliases in a new window, as a sort of reminder.
Opening a File and Altering it Automatically
I keep a linear journal on Ftrain, to hold onto passing thoughts, and I want to be able to get in and out of it quickly - preferably without having to add any "metadata" like titles, date, filename, and so forth.
So, to keep things simple, I decided that my journal would be one big text file split up by timestamps, and timestamps would start with "*". That is, two journal entries might look like this:
*2001-03-19-02:00
This is the journal entry for the 19th of March, 2 am. My dog got fleas.
*2001-03-19-18:00
And here's another for the 19th, at 6 pm. Fleas getting better.
*2001-03-20-02:00
And this one is for the next day at 2 am. Fleas is gone.
That's it - plain text. I split up the days, convert paragraphs, line breaks, URLs, etc via a Perl script.
When I began using this journal, I found that it was a real drag to open it up, scroll to the bottom of the file (the end-of-buffer), insert the time, and start typing. So I wrote another little function that:
- Opens the "journal" file;
- Goes to the bottom of the file and inserts a hard return;
- Inserts a time-stamp prefixed by an asterix;
- Inserts two hard-returns
(defun journal () (interactive) (find-file "~/www/journal.txt") (end-of-buffer) (insert "\n\n") (insert "*") (insert-time) (insert "\n\n") )
As long as you know that "\n" means "newline" in LISP, then you'll be able to figure out what's going on above. It's just a simple sequence of smaller functions, like insert and find-file adding up into a larger one.
As I wrote, when I'm in emacs, I call the journal with M-x journal. If I'm not in emacs, I can simply type emacs -f journal on the command line, and emacs will open and execute the journal function immediately. To speed things even more, I have the following line in my .bashrc file:
alias journal='emacs -nw -f journal'
With this, when I'm in the shell, I simply type journal and emacs opens up in journal mode without creating a new window. I type quickly, exit, and I'm done.
It seems a simple thing, but making the process of writing in my journal as free of resistance as possible has helped me catch hundreds of paragraphs before I moved onto something else. Also, since I write my email in emacs, when I write something that's worth saving, I simply copy it, type M-x journal, and paste into the journal, exit the journal file and go back to finish the email. Whenever I update Ftrain, the journal is sorted by day and auto-converted to XML, then integrated into the whole of the site.
I use a very similar function to open my private "messages" file, where I hold all my records of phone calls. I simply type "messages" at the shell prompt and begin taking notes; it's much faster than trying to open up a new file or clicking in a virtual "post-it" note.
Inserting a Block of XML Code into a Buffer
Here's what was going on: I wanted to insert a block of XML code representing a section into the file I was editing, with the proper "date" information, and, since every block needs a unique indentifier, I wanted the editor to leave the cursor right at the spot where I could type the unique ID.
That is, I wanted the editor to insert a block like this, automatically entering the correct ISO date:
<section date="2001-03-19" id=""> <title></title> <desc></desc> <key></key> <p></p> </section>
After it inserted the code, I wanted emacs to move the cursor up between the quotes of id="", so I could insert an ID immediately. Here is the function I wrote:
(defun section () (interactive) (insert "\n<section date=\"") (insert-ISO) (insert "\" id=\"\">" "\n <title></title>" "\n <desc></desc>" "\n <key></key>" "\n\n <p></p>" "\n\n</section>\n\n") (previous-line 9) (end-of-line) (backward-char 2) )
If you read the previous explanations, it should all be fairly straightforward, but there's one thing worth noting: everything in emacs, whether entering text, moving around in text, and searching, or sending mail, is bound to a LISP function - which means you can use these functions in other functions. For instance, the LISP function previous-line is called every time you hit the "up" cursor key. In this case, we do the following:
(previous-line 9)
This is equivalent to hitting the "up" cursor key 9 times.
(end-of-line)
Then we go to the end of the current line...
(backward-char 2)
...and move 2 characters backward. This puts us right in between the quotes of the id="" attribute of the section tag, which is where we want to be.
With "insert" and a few functions like:
- previous-line
- next-line
- forward-char
- backward-char
- end-of-buffer
- beginning-of-buffer
- end-of-line
- beginning-of-line
Using Macro Mode to Quickly Change Several Hundred Files
Emacs has a simple Macro mode which records all of your keystrokes for later playback. I often use it when I'm editing hundreds of files at once, making simple changes in all the files.
- To start recording a keyboard macro, hit C-x (
- To stop recording a keyboard macro, hit C-x )
So, let's say your company was just purchased, and you have 200 html files in three different nested directories, and you need to change the title portion of each HTML file from one company's name to another. So
<title>Ftrain.com: "About Us"</title>
needs to become
<title>ConsolidatedUndulatingProng.com: "About Us"</title>
And so forth. You could always create a regular expression in a Perl script, and run that on every relevant file, replacing every occurrence of Ftrain.com with ConsolidatedUndulatingProng.com. Actually, for such a simple 1-to-1 transformation, that would be a better approach. You could just write a one-line perl script, called replacer.pl in your home directoryx like so:
#!/usr/bin/perl -pi.bak s/Ftrain\.com/ConsolidatedUndulatingProng.com/gi;
and go into your top-level HTML directory and type:
$ perl ~/replacer.pl $(find -name "*.html")
That will, if memory serves, take the list of all HTML files in the current and all child directories, pipe them through replacer.pl, replace all the "Ftrain.com" references with "ConsolidatedUndulatingProng.com" references, and create .bak backup files for every file touched.
But enough digression - back to emacs. Let's just assume that you didn't want to deal with Perl for right now. Do the following:
- $ cd /home/html/ (or whatever your HTML directory is)
- $ emacs $(find -name "*.html")
- emacs will open in a split window, with a file listing in "Dired" mode on top and the first file in the listing open on the bottom. Your cursor should be in the file listing mode "Dired" window. Now we create the macro:
- C-x (
- [Return]
- C-s <title>ftrain.com
- [Delete the text "Ftrain.com"]
- [Type "ConsolidatedUndulatingProng.com"]
- C-x C-s
- C-x k
- [Cursor down]
- C-x )
Here's what's going on with all of that:
C-x (
We open the macro.
[Return]
We hit return. Because we're in Dired mode, we open the file that is listed at the cursor point
C-s <title>
We do a forward search for the <title> tag
C-s ftrain.com
Now we do a second forward search for the string "ftrain.com." This will find the first occurence of Ftrain.com after the <title> tag, regardless of spacing.
[Delete "Ftrain.com"]
Erase the string "Ftrain.com"; the macro will remember that you hit backspace 10 times, not what you erased.
[Type "ConsolidatedUndulatingProng.com"]
Enter then new string where Ftrain.com used to be.
C-x C-s
Save the file.
C-x k
Kill the buffer. This will bring you back to DIRED mode.
[Cursor down]
Go down to the next file.
C-x )
Close the macro.
Now, you can run the macro by typing C-x e. Do this a few times to test it out. You won't be able to see anything, but the cursor will continue to move down the file list. What happens is - the macro opens the currently selected file, makes the changes in the macro, saves the file, closes it, and moves to the next file in the list.
If you're confident the macro is doing what you want it to, simply type C-u 200 C-x e and the macro will be executed 200 times, moving down the list of files in your Dired buffer. If there aren't 200 files, it will break and beep, no worries.
I know it seems complicated, but play around. I found that emacs macros could save me dozens of hours of time, especially with regards to site-wide HTML editing. And it's often easier than dealing with a Perl regular expression because emacs allows you to jump over line breaks and do some very arbitrary stuff in a macro, whereas Perl makes you get things just right.
Naming Macros for Re-Use
- Create the macro (see above)
- M-x name-last-kbd-macro
- Give the macro a name when prompted.
- Open your .emacs file and scroll to the end
- M-x insert-kbd-macro
- Enter the name you just gave your macro
Using Perl to Interface with Emacs
There's a neat library called Emacs::Lisp written by a fellow named John Edwin Tobey that is very interesting; essentially, it lets you mix emacs and Perl functions together at will. There was a version of Emacs called Perlmacs, which had Perl built-in, but that was pure Unix craziness and not fit for mortals, while this is a simple Perl module, and everyone can play along.You need to install the Perl module and associated LISP files in the approproate places, as defined in the Emacs::Lisp install
docs. I'm not going to help you there, because if you know enough about Perl and Emacs to have read this far, you already
know how to install modules.
I tossed these two lines into my .emacs:
(load-library "perl") (perl-load-file "~/.emacs.pl")
So, given those lines in my .emacs and a Perl script in my home directory, called .emacs.pl, I can do some very neat things. Here's a short version of .emacs.pl:
#!/usr/local/bin/perl use Emacs::Lisp; use WWW::Search; defun (\*google_search, interactive("r"), sub { my $query = &read_string("Enter Google Search Term: "); my $search = new WWW::Search('Google'); $search->maximum_to_retrieve(15); $search->native_query(WWW::Search::escape_query($query)); while (my $result = $search->next_result()) { &insert($result->url, "\n"); } }); defun (\*google_search_href, interactive("r"), sub { my $accum; my $query = &read_string("Enter Google Search Term: "); my $search = new WWW::Search('Google'); $search->maximum_to_retrieve(15); $search->native_query(WWW::Search::escape_query($query)); $accum = "\n<ol>\n"; while (my $result = $search->next_result()) { my $url = xml_cleanup($result->url); my $title = xml_cleanup($result->title); $query = xml_cleanup($query); $accum .= qq{\n\t<li>\n\t\t<a href="$url" title="Found via Google.com search for: $query">$title</a>\n\t\t<br/>\n\t<a href="$url" title="Found via Google.com search for: $query">$url</a>\n\t</li>\n\n}; } $accum .= "\n</ol>\n"; &insert("$accum"); }); sub xml_cleanup { ($in) = (@_); $in =~ s/>/\>/g; $in =~ s/</\</; $in =~ s/\&/\&/; $in =~ s/\&/\&/; $in =~ s/\"/\"/; $in; }
What you see are two functions, one called google-search and the other called google-search-href. If things are set up properly, you can use them like any function, by typing M-x google-search or M-x google-search-href. Both query the Google search engine and spit out a list of 15 simple URLs; the first just prints the URLs and the latter prints them as fully qualified HTML HREFs in an ordered list. For instance, let's type in M-x google-search and when it asks Enter Google Search Term I'll type in "Paul Ford Ftrain":
http://www.ftrain.com/ http://www.ftrain.com/about_pef.html http://www.consolidatedundulatingprong.com/ http://www.metafilter.com/comments.mefi/689 http://www.metafilter.com/comments.mefi/688 http://www.clickey.com/search.cgi?keyword=Subway http://www.villagevoice.com/vls/170/dibbell.shtml http://www.pigsandfishes.org/links/weblog/archives.html?year=2000&month=02 http://glassdog.com/lance/wom/ http://www.ruth.co.uk/homes/roo/2000_07_01_archive.html http://www.diarist.net/cgi-bin/registry.cgi?author=P http://www.diarist.net/registry/byname/1800.shtml http://members.home.net/gwym/links.html http://www.cl.cam.ac.uk/~mn200/weblog/2000_04.html http://abakusz.matav.hu/erre/links/
Now the same, but with M-x google-search-href
Ftrain.com
http://www.ftrain.com/
Ftrain.com: "Paul.Ford"
http://www.ftrain.com/about_pef.html
Ftrain.com: "Ftrain.com"
http://www.consolidatedundulatingprong.com/
Metafilter | Comments on 689
http://www.metafilter.com/comments.mefi/689
Metafilter | Comments on 688
http://www.metafilter.com/comments.mefi/688
Clickey Search Results
http://www.clickey.com/search.cgi?keyword=Subway
Voice Literary Supplement: My Modem, Myself
http://www.villagevoice.com/vls/170/dibbell.shtml
P&F: Weblog Archives [02/2000]
http://www.pigsandfishes.org/links/weblog/archives.html?year=2000&month=02
glassdog.WORD OF MOUTH
http://glassdog.com/lance/wom/
roo.i.am
http://www.ruth.co.uk/homes/roo/2000_07_01_archive.html
Diary Registry: Search
http://www.diarist.net/cgi-bin/registry.cgi?author=P
Diary Registry: Browse by Author
http://www.diarist.net/registry/byname/1800.shtml
Go West, Young Man: List of Links
http://members.home.net/gwym/links.html
Michael Norrish's web-log - April 2000
http://www.cl.cam.ac.uk/~mn200/weblog/2000_04.html
Links
http://abakusz.matav.hu/erre/links/
Not too awful. In any case, I just installed this thing tonight, but I can definitely think of many nifty functions, especially as Perl has a host of useful linguistic analysis tools, better regular expressions than LISP, and modules for everything from mail-checking to Web-page-downloading. I'm thinking that I can write emacs/Perl routines to enter random poetry, define the current word via the Perl::Wordnet, the semantic word database, and automatically offer synonyms. Emacs = thesaurus! Emacs = dictionary! Emacs = all manner of stuff. I can spit my sentences out to link grammars, and call Figlet on my titles, and on, and on, and on until I explode into a huge ball of emacs-loving plasma.
So, that's it. Let me know if you find this sort of thing useful, and what I can add to it; it's fun to write it out, and forces me to ask questions of the mechanistic processes I'm using to create text, both inside the computer, and inside my head.