Archive for March, 2008

Wrapping FTP in Emacs

March 29, 2008

Emacs provides some very nice abstractions for working with remote files. With emacs package ange-ftp find-file can access remote files transparently. You simply enter (for example) /ftp:<user>@<host>:/www/blog/index.php and behind the scenes emacs handles setting up an ftp connection to the remote server and downloading the file. When you save emacs uploads the file to the server. If the ftp connection has timed out then it reconnects automatically. All this is done transparently and it is almost like working with a local file. It is yet another reason to use emacs. However, sometimes that is not exactly what you want.

If you are working with an external website usually you have a development version running locally. After tweaking a page and seeing how it looks then you upload it to your remote server. Wouldn’t it be nice if you could upload a file to the correct location with a single key rather than resorting to an external program? Of course, you could do all the development remotely without too much pain using ange-ftp as described above, but working locally is faster and saves a lot of bandwidth.

ange-ftp provides some nice functions for connecting to an ftp server and assigning a password to particular user/host combination.

For a reminder on how to find useful emacs functions go here

(require 'ange-ftp)

We set up some variables for the username, hostname and password and also a variable to reference the ftp process.

(defvar *ftp-process* nil)
(defvar *ftp-user* "some-user")
(defvar *ftp-host* "ftp.some-host.com")
(defvar *ftp-password* "...")

I tried using the ftp client supplied with windows without much success so eventually I downloaded the ftp client mentioned at the ange-ftp wikipage
ftp://ftp.gnu.org/old-gnu/emacs/windows/contrib/ftp-for-win32.zip

(setq ange-ftp-ftp-program-name "c:/bin/ftp.exe")

(ange-ftp-set-passwd *ftp-host* *ftp-user* *ftp-password*)

The first function we need is one that connects to the remote server. This is a simple wrapper around ange-ftp-get-process. We check if we are already connected before trying to reconnect. How do we know if we are already connected? We could check the status of the process – if it is ‘run then the process is running. The problem with this strategy is that with many ftp-clients, when a connection is timed out, the ftp process is not disconnected until it tries to send another command. This means that we need to track timeouts ourselves.

Add a constant for how long it takes the remote server to disconnect us if there is no activity and a variable to store when we last did something.

(defconst *ftp-max-timeout* 100)

(defvar *ftp-last-action* 0)

Then we need some functions to check if we have timed out and set when we last sent a command to the server.

(defun ftp-has-timed-out-p ()
(> (- (time-to-seconds (current-time)) *ftp-last-action*)
*ftp-max-timeout*))

(defun ftp-set-last-action ()
(setq *ftp-last-action* (time-to-seconds (current-time))))

Add a predicate to check if we are connected and then we can easily implement the ftp-connect function.

(defun ftp-connected-p (proc)
(and proc
(equal (process-status proc) 'run)
(ftp-has-timed-out-p)))

(defun ftp-connect (host user)
(when (ftp-has-timed-out-p)
(ftp-kill-process host user *ftp-process*)
(sit-for 0)
(sleep-for 1))
(when (not (ftp-connected-p *ftp-process*))
(setq *ftp-process*
(ange-ftp-get-process host user))
(ange-ftp-set-binary-mode host user))
(ftp-set-last-action)
*ftp-process*)

If we have timed out then we kill the existing process, wait for the display to update with (sit-for 0) and wait for a second before trying to reconnect. You might notice we used an ftp-kill-process function that hasn’t yet been defined.

(defun ftp-kill-process (host user proc)
(when (ftp-connected-p proc)
(kill-process proc)
(setq proc nil)
(set-buffer (get-buffer (ange-ftp-ftp-process-buffer host user)))
(insert "\nTerminated"))
nil)

Finally we provide a wrapper around ange-ftp-raw-send-cmd that connects us if we are disconnected and then sets the last-action to the current time and add commands to make directories and copy files.

(defun ftp-raw-send-cmd-wrapper (cmd)
(let ((proc (ftp-connect *ftp-host* *ftp-user*)))
(ange-ftp-raw-send-cmd proc cmd)
(ftp-set-last-action))
t)

(defun ftp-copy-file (from to)
(ftp-raw-send-cmd-wrapper
(format "put %s %s" (expand-file-name from) to)))

(defun ftp-mkdir (dir)
(ftp-raw-send-cmd-wrapper (format "mkdir %s" dir)))

Hopefully you get the impression from the above code that writing elisp is similar to writing any other code – you decide on the abstractions you want and then you implement them. So why bother learning it? Firstly, there is a huge body of code that implements a lot of functionality already written in elisp. For example, if I had to implement everything that ange-ftp provides, that would have been a lot of code. Secondly, if I wrote the ftp-wrapper in perl then I would have to execute the script manually (or perhaps write an elisp binding anyway) which be more inconvenient than simply pressing a key while in my editor.

In the second part, we will see how to add some very simple functions to upload files to remote locations based on where the local file is.

The final version of the ftp-wrapper is linked from the
original version of this page.

Advertisements

describe-key and apropos

March 28, 2008

Emacs is often described as a self-documenting text editor. Part of that is because of the superb manuals that are built-in but another part is that you can ask the editor to describe itself using C-h k (describe key) and M-x apropos.

describe-key tells you which function is invoked when you press a certain key chord. For example, I wanted to know what C-h k invoked so I pressed C-h k C-h k. Whilst writing a script I needed to know which function positions the cursor at the beginning of the line. C-h k C-a gives the answer – (move-beginning-of-line arg). Okay, so that is useful, but what if you are looking for a command that isn’t bound to a key combination. That is where apropos comes in.

apropos enables you to list all the functions in emacs that match a regular expression. For example, say you are not sure which function returns the current time. You might try M-x apropos time$<RET> and search through the list of functions that are returned or perhaps M-x apropos current.*time which quickly points you at (current-time). However, current-time returns a list of three integer values which isn’t very useful. Maybe you can convert it into an integer?

M-x apropos integer<RET>
C-s (isearch-forward) time - no matches

Maybe not… How about a float?

M-x apropos float<RET>
C-s time

Both of the following look to be suitable for my purposes. Of course, your purposes may be different!

(float-time)
(float-time (current-time))

Right, that is quite enough about time. How about a search for all the hooks?

M-x apropos hook$

This returns 155 hooks in my emacs. One interesting hook is post-command-hook which runs after every command. I will be using that in a later post.

Right, that should have given you an idea of how to search for the functions you need.

JG

Intro

March 28, 2008

Ah yes, the introductory ramble; the part that no-one ever reads. Oh well, at least I will be able to refer to it myself if I forget what this blog is supposed to be about.

Firstly, a little bit about me: I’m a developer and I work on financial software (hi!) Until recently, my tools of choice were C++, Perl and Apache. I’m still paid to work with the first two. For a number of reasons I’m not particularly enamoured of C++ anymore (damn useless tools) and Perl is also losing its shine for me. Apache is still awesome though! I may write more about why later but for now it is not important.

I have a couple of ideas for series that I am going to publish here. The first is going to be about customising Emacs using elisp. The second will be about setting up a blog. I will be attempting to drum up a few readers as feedback is nice. Primarily though, it is going to be for the purposes of getting my notes in one place. I frequently forget how I did something so if that happens in future I will simply be able to refer to my blog.

That’s it for now…

JG