From: Tamas K Papp on
Hi,

I had to write a simple script, and decided to do it in CL. It is the
language I know best, and I wanted to learn how to generate standalone
executables in ECL (it was dead simple -- the whole thing is 38K).

I would appreciate if people could comment on the code. It is nothing
complicated and runs fine, I just want to learn the idiomatic way to
use CL for scripting. For example, is line-by-line (see below) the
recommended way to redirect a stream to a file?

For the curious, the script is used to open the body of an e-mail
message from mutt in the browser. I use this to translate the text of
e-mails I get in foreign languages using firefox plugins. Easier than
manual extraction of text & copy-paste. The mutt snippet looks like this:

macro index,pager <F7> "\
<enter-command> set pipe_decode=yes<enter>\
<enter-command> set my_wait_key=\$wait_key wait_key=no<enter>\
<pipe-message>/home/tpapp/software/misc/savebody<enter>\
<enter-command> set wait_key=\$my_wait_key &my_wait_key<enter>\
<enter-command> set pipe_decode=no<enter>\
" "view message body in browser"

Anyhow, here is the Lisp code:


;;;; This simple scripts saves standard input to a randomly generated
;;;; file (starting with *base-path*) and then calls a browser to
;;;; open this file (current setup obviously Ubuntu/Debian specific).

;; working with strings instead of pathnames
(defparameter *base-path* "/tmp/savebody")

;; browser
(defparameter *browser* "sensible-browser")

(defun random-string (length &optional (allowed-chars
"01234567890abcdef"))
"Return a random string of given length, composed from allowed-chars."
(let* ((n (length allowed-chars))
(string (make-string length)))
(dotimes (i length)
(setf (aref string i) (aref allowed-chars (random n))))
string))

(defun open-unique (&key (base-path *base-path*)
(random-length 10)
(max-tries 100))
"Return stream and path."
(tagbody
top
(when (minusp max-tries)
(error "could not open random file - this should not happen"))
(let* ((random-path (concatenate 'string base-path (random-string
random-length)))
(stream (open random-path :direction :output :if-exists nil
:if-does-not-exist :create)))
(unless stream
(decf max-tries)
(go top))
(return-from open-unique (values stream random-path)))))

(defun copy-stream (input output)
"Copy input to output. No error handling."
;; is this the right way to do it?
(tagbody
top
(multiple-value-bind (line missing-newline-p) (read-line input
nil nil)
(when line
(if missing-newline-p
(write-string line output)
(write-line line output))
(go top)))))

(multiple-value-bind (stream path) (open-unique)
;; open file and copy stdin
(unwind-protect
(copy-stream *standard-input* stream)
(close stream))
;; open browser
(let ((args (list (concatenate 'string "file://" path))))
#+sbcl (sb-ext:run-program *browser* args
:search t :wait nil)
#+ecl (ext:run-program *browser* args)))

From: Pillsy on
On Dec 4, 5:34 am, Tamas K Papp <tkp...(a)gmail.com> wrote:
[...]
> I would appreciate if people could comment on the code.  It is nothing
> complicated and runs fine, I just want to learn the idiomatic way to
> use CL for scripting.

You really like those TAGBODYs and GOs don't you? :)

> For example, is line-by-line (see below) the recommended way to redirect
> a stream to a file?

That's how I've always done it. I never even realized that READ-LINE
had that second return value; that almost makes up for the way it
mixes optional and keyword arguments.
[...]
Cheers,
Pillsy
From: fortunatus on
You could replace the tagbody with simple LOOP and LOOP-FINSH, or you
could use the complex LOOP keywords like DO and WHILE. But frankly I
find your TAGBODY and GO to be quite clear as it is, even probably
more clear than the complex loop keywords would be. Otherwise I think
your code is great!

By the way, would you mind posting what you did for your ECL exec?
Thanks!

(loop
:do (multiple-value-bind ... )
:while line
:do (if missing-new-line (...) (...))
From: fortunatus on
On Dec 4, 10:20 am, fortunatus <daniel.elia...(a)excite.com> wrote:
> You could replace the tagbody with simple LOOP and LOOP-FINSH, or you
> could use the complex LOOP keywords like DO and WHILE.  But frankly I
> find your TAGBODY and GO to be quite clear as it is, even probably
> more clear than the complex loop keywords would be.  Otherwise I think
> your code is great!
>
> By the way, would you mind posting what you did for your ECL exec?
> Thanks!
>
> (loop
>   :do (multiple-value-bind ... )
>   :while line
>   :do (if missing-new-line (...) (...))

Obviously I meant "LOOP-FINISH" and "multiple-value-setq"... But one
drawback is you need to make variables separately. So LOOP-FINISH or
just leave your tagbody as is.
From: Tamas K Papp on
On Fri, 04 Dec 2009 07:20:07 -0800, fortunatus wrote:

> You could replace the tagbody with simple LOOP and LOOP-FINSH, or you
> could use the complex LOOP keywords like DO and WHILE. But frankly I
> find your TAGBODY and GO to be quite clear as it is, even probably more
> clear than the complex loop keywords would be. Otherwise I think your
> code is great!
>
> By the way, would you mind posting what you did for your ECL exec?
> Thanks!
>
> (loop
> :do (multiple-value-bind ... )
> :while line
> :do (if missing-new-line (...) (...))

See this nice writeup: http://blog.s21g.com/articles/1649

I just fired up an ECL, and used

(compile-file "savebody.lisp" :system-p t)
(c:build-program "savebody" :lisp-files '("savebody.o"))

but apparently you can do much more complex stuff. And the whole
executable is really small. ECL just rocks.

Tamas