10

When writing Common Lisp code, I use SLIME. In particular, I compile the buffer containing definitions of functions by using C-C C-k, and then switch to the REPL to run those functions. Putting executable code to run those functions in the buffer does not appear to work so well. If the code has bugs it can make a mess.

It would be handy to have a way to include code that doesn't get compiled in the buffer, but do get run from the command line, e.g. when doing

sbcl --script foo.lisp

If that were the case, I would not have to keep adding and removing code every time I wanted to run code from the command line. Does there exist such a condition?

This is analogous to the Python condition.

if __name__=='__main__':

which is false if a Python file is imported as a module, but true if it is run as a script.

This blog post entitled "Using SBCL for Common Lisp shell scripting", found by random Googling, has

;; If run from command line, the following if-test will succeed

(if (> (length sb-ext:*posix-argv*) 1)

    ;; Code that is executed from the command line only goes here

)

The code included indeed does not get run by the compiler inside SLIME, but it doesn't get run by sbcl --script either.

UPDATE: Thanks to Vsevolod Dyomkin for his helpful answer and the followups. Some notes about that answer follow, compiled from the comments to that answer. @Vsevolod, if you add these to your answer, I'll delete them.

  1. First, I asked for a way to run code from the command line, but not from the interpreter. The supplied solution does more; it also gives a way to run code from the interpreter but not the command line.

  2. The first step is to define a reader macro function for the macro character #!. As stated in the link "Upon encountering a macro character, the Lisp reader calls its reader macro function". The reader function is defined by the call to set-dispatch-macro-character. So, when the #! character is seen, the set-dispatch-macro-character causes the lambda function defined in the body to be called. This function then adds the keyword :noscript to the *features* variable. See also a discussion of what reader macros are good for in the SO question Read macros: what do you use them for?.

  3. Observe that the keyword :noscript is added to *features* precisely when the #! character is present. Furthermore, the #! character is present when the code is run inside the interpreter e.g. when using slime, but apparently is absent (removed) from program's
    text by sbcl --script is run. Therefore, :noscript is added to *features* when the code is run in the interpeter, but not when run as a
    script.

  4. We now use the builtin reader macros #-/#+, which as Vsevolod said, behave similarly to the to C's #IFDEF/#IFNDEF. They check for a symbol
    in *features*. In this case, #-:noscript checks for the absence of :noscript, and #+:noscript checks for the presence of :noscript.
    If those conditions are satisfied, it runs the corresponding code. To wrap a block of code, one can use progn like this: #-:noscript (progn <your code here>).

  5. Finally, one needs to call set-dispatch-macro-character before running code that uses this functionality. In the case of sbcl, one can put it in the initialization file ~/.sbclrc. Observe that this approach does not depend on the Common Lisp implementation being SBCL.

  6. A simpler alternative, as mentioned in the sbcl-devel list, is to use the fact that the keyword :SWANK appears when one types *features* in
    a REPL inside emacs using SLIME. SWANK is the server side of SLIME. SLIME should probably more accurately called SLIME/SWANK, as these two are the client/server components of a client-server architecture. I found this blog post called Understanding SLIME, which was helpful.

    So, one can use #-:swank and #+:swank just like #-:noscript and #+:noscript, except that one doesn't need to write any code. Of course, this then won't work if one is using the command line interpreter sbcl, for instance, since then :SWANK will not appear in *features*.

Community
  • 1
  • 1
Faheem Mitha
  • 6,096
  • 7
  • 48
  • 83

3 Answers3

13

You can use the following trick:

  1. Define a dispatch function for shebang:

    (set-dispatch-macro-character #\# #\!
        (lambda (stream c n)
          (declare (ignore c n))
          (read-line stream)
          `(eval-when (:compile-toplevel :load-toplevel :execute)
             (pushnew :noscript *features*))))
    
  2. In your script file use #-:noscript:

    #!/usr/local/bin/sbcl --script
    
    (defun test (a) (print a))
    
    (test 1)
    #-:noscript (test 2)
    #+:noscript (test 3)
    

    Executing ./test.lisp will print 1 and 2, while C-c C-k will output 1 and 3.

EDITS

This trick should work, because the shebang line is removed altogether by sbcl --script, but not removed, when the file is loaded through SLIME or other mechanisms.

The drawback of this approach is that we condition on absence of :noscript in features, and not presence of :script. To amend it, pushing of the appropriate feature should be done in sbcl --script processing itself.

Vsevolod Dyomkin
  • 9,343
  • 2
  • 31
  • 36
  • Looks interesting. Question: Where would the `set-dispatch-macro-character` invocation happen? And/or, could you just generally explain *how* (and/or *why*) this would work? I'm a CL Novice, and one of the things I notice here is that you seem to be referencing the characters `#` and `!`, but then there's logic for `#+` and `#-`... I'm sure I'm missing something, I'm just unsure as to how to figure out what I'm missing. :) (Though perhaps the original asker doesn't have this problem, I don't know.) – lindes Mar 21 '12 at 11:06
  • 3
    Shebang line is removed from program's text by `sbcl --script`, and not removed when you load the file any other way. `#-`/`#+` are standard read macros similar to C's `#IFDEF`/`#IFNDEF`. They check for a symbol in `*features*`. We define a read macro for `#!` to push `:noscript` to `*features*`. It should be defined before the file is `load`ed. Unfortunately you can't do it more proper and push `:script` in script invocation, because this line is absent at that time. You can read more about read macros' uses here: http://stackoverflow.com/questions/1351956/read-macros-what-do-you-use-them-for – Vsevolod Dyomkin Mar 21 '12 at 14:22
  • Hi Vsevolod. Thanks, that works. Could you outline in more detail and more simply. what is going on here? I know next to nothing about Lisp at the moment. To start with, where do you define the read macro(s) in your code? Are the macros in question `#-/#+`? I don't see a `defmacro`. On a more practical note, do I need to prefix every line that I want to be handled specially by `#-:noscript` or similar? I was hoping for something like an `if` statement which I could wrap around a whole block of code at once. – Faheem Mitha Mar 21 '12 at 15:24
  • 1
    Defining a reader macro is done with `set-dispatch-macro-character` or `set-macro-character` (see http://clhs.lisp.se/Body/02_add.htm). `#-`/`#+` are built-in ones, and we define `#!`. You don't have to prefix every line - only every form (you can wrap all your code in `progn`, like this `#-:noscript (progn )`. – Vsevolod Dyomkin Mar 21 '12 at 15:46
  • Thanks Vsevolod. That works. I'd like to eventually understand better how this all works, but for now I have something I can use. I don't get notified of your comment unless you stick a @username in front of your comment. BTW, nice blog. – Faheem Mitha Mar 21 '12 at 17:21
  • I think I'm still missing something. It was working from me, then I exited emacs and reloaded the file and restarted slime. Now I'm getting `no dispatch function defined for #\!`, pointing to the shebang line, namely `#!/usr/local/bin/sbcl --script`. I'm not sure what function the shebang line plays here, but I have it in the same file as the dispatch function. I am only using one file. I thought maybe the dispatch function needed to come before the shebang line, but I tried it both ways, and it makes no difference. If I remove the shebang line, recompile, and then put it back in, it works. – Faheem Mitha Mar 21 '12 at 20:14
  • This is probably how I (accidentally) got it working in the first place. BTW, as @claytontstanley says, it would be good to merge some of the elaborations in the comments back into the main post. – Faheem Mitha Mar 21 '12 at 20:17
  • @faheem-mitha you have to set dispatch macro character before you load the file in SLIME. For example in SLIME top-level or in your `.sbclrc` – Vsevolod Dyomkin Mar 21 '12 at 21:33
  • Ok. Can the `.sbclrc` contain just the dispatch function? – Faheem Mitha Mar 21 '12 at 22:18
  • Can the `.sbclrc` contain just the dispatch function - yes – Vsevolod Dyomkin Mar 21 '12 at 22:23
  • This works, but not perfectly. The first time I load the file with `C-c C-k`, the code after `#-:noscript (progn...` is still run, but not on subsequent occasions. It is seeing the .sbclrc, because if I rename or move .sbclrc, it doesn't work at all. Any suggestions about how to fix this? – Faheem Mitha Mar 22 '12 at 06:05
  • 1
    @faheem-mitha I've edited the solution to account for the case of first load: `(pushnew ...)` should be wrapped in `eval-when`, so that it always executes, just as the line is read. – Vsevolod Dyomkin Mar 22 '12 at 08:18
  • @VsevolodDyomkin: That works, thanks. BTW, if this is a portable solution, and it looks like it is, that is worth mentioning. – Faheem Mitha Mar 22 '12 at 11:38
  • Another solution, proposed in sbcl-devel mailing list, was to use #-swank in scripts – Vsevolod Dyomkin Mar 22 '12 at 12:08
  • 1
    @VsevolodDyomkin: Please elaborate on that solution if you wish. Also, I have added some notes on your answer to my question, Please feel free to issue corrections/comments. – Faheem Mitha Mar 22 '12 at 16:08
  • 1
    Thanks, that's a great summary. Regarding checking for `#-swank`, it's the similar approach, just more automatic, as `swank` is the name of SLIME backend, so it's added by SLIME itself. – Vsevolod Dyomkin Mar 22 '12 at 16:35
  • 2
    @VsevolodDyomkin thanks for this detailed answer and the follow up answers really learned a lot from this. – mcheema Jan 01 '13 at 22:22
5

Fare Rideau wrote a nifty unix utility CL-Launch, which enables executing lisp software from the command line. It has integrated Quicklisp support and works on most of the Common Lisp implementations.

Example script may look like this:

#!/usr/bin/cl -sp "hunchentoot" -Q -E main

(defun main (argv)
  (format t "~A: ~{~A ~}~%" (truename ".") argv)
  (hunchentoot:start
   (make-instance 'hunchentoot:acceptor
                  :document-root (truename ".")
                  :port 8080))
  (do ((x #\s (read-char)))
      ((char-equal x #\q) nil)))

After adding +x permissions to script and invoking it, it will start http server in the current directory. "-sp" flag indicates package you want to load, so it's fairly clean way of abstracting shell script from the package.

For more details refer: http://cliki.net/CL-Launch

  • There's also the newer [Roswell](https://github.com/roswell/roswell) which, besides being a script launcher like cl-launch, is also an implementation installer, a script manager/downloader, building utility etc. [Some lispers](http://eudoxia.me/article/common-lisp-sotu-2015) advise to build on Roswell, not cl-launch any more. – Ehvince Dec 26 '16 at 20:41
1

I had the same question and I have just stumbled upon this discussion. At least with sbcl it seems I can use (sb-ext:posix-getenv "_"). When run in slime it returns /usr/bin/emacs (or whatever the path to emacs is) and otherwise the command I use to invoke the script. So it is always possible to differentiate between slime and script invocation until you're an emacs contributor :)

If you want to get the full pathname of the script you are invoking, you can use (truename (sb-ext:posix-getenv "_")). However, when run from slime it will return the effective emacs pathname, e.g. /usr/bin/emacs-24.5, so this might be less convenient.

Andrei
  • 2,585
  • 1
  • 14
  • 17