Search code examples
lispcommon-lispracketevaldsl

Which is the easiest way to extend a Lisp with a small correction in the evaluation?


I would like to try extending some Lisp (Scheme, Racket, Clojure, any) to run external commands as follows:

; having
(define foo ...)
(define bar ...)
; on command
(ls (foo bar) baz)
; this lisp should evaluate (foo bar) as usual, with result "foobar", then
(ls foobar baz)
; here "ls" is not defined
; instead of rising "undefined identifier" exception
; it must look for "ls" command in the directories
; in the "PATH" environment variable
; and launch the first found "ls" command
; with strings "foobar" and "baz" on input

I just want to run it anyhow, without carrying about correct conversion from lisp's data structures to strings or handling the exit code and the output of the command in stdout/stderr.

I think there is no way to extend it within normal environment (like catching the "undefined" exception all the time). The eval procedure of the interpreter itself must be changed.

Which Lisp is the best to extend it like this and how is it done? Maybe there already exists a project performing something similar?


Solution

  • In racket you may override #%top:

    #lang racket
    
    (provide
     (combine-out
      (except-out (all-from-out racket) #%top)
      (rename-out [shell-curry #%top])))
    
    (require racket/system)
    
    (define (stringify a)
      (~a (if (cmd? a) (cmd-name a) a)))
    
    (struct cmd (name proc)
      #:property prop:procedure
      (struct-field-index proc)
      #:transparent
      #:methods gen:custom-write
      [(define (write-proc x port mode)
         (display (string-append "#<cmd:" (stringify x) ">") port))])
    
    (define (shell name)
      (define (cmd-proxy . args)
        (define cmd
          (string-join (map stringify (cons name args))
                       " "))
        (system cmd))
      cmd-proxy)
    
    (define-syntax shell-curry
      (syntax-rules ()
        ((_ . id)
         (cmd 'id (shell 'id)))))
    

    Save this as shell.rkt and make this runner.rkt in the same directory:

    #lang s-exp "shell.rkt"
    
    (define test (list /bin/ls /usr/bin/file))
    (second test) ; ==> #<cmd:/usr/bin/file>
    (first test)  ; ==> #<cmd:/bin/ls>
    ((second test) (first test)) 
    ; ==> t (prints that /bin/ls is an executable on my system)
    

    Now from here to make it a #lang myshell or something like that is pretty easy.