A Lisp Interpreter for Linux Shell Scripting

Content

Motivation

A few weeks ago, I started learning about Lisp and decided to write my own Lisp interpreter, mostly to understand the language and it’s concepts better. Once I got the interpreter working, I natually wanted to do something useful with it. But writing a full-scale project entirely in my own Lisp sounds like a recipe for disaster!

Then I thought about smaller shell scripts, which I often need to write for work or personal projects. Could I implement shell scripting features in my Lisp - things like executing shell commands, piping, and capturing output?

Well, as it turns out, the answer is yes!

You can find the source on GitHub and try it out yourself! Please be aware this project is still very much in alpha, so there will be bugs.

What is Lisp?

Lisp is one of the oldest high-level programming languages, originating in the late 1950s. It’s known for its distinctive use of parentheses, prefix notation, and S-expressions, which elegantly represent both code and data. Although Lisp has fallen somewhat out of favor in recent decades, it pioneered many foundational programming concepts - such as garbage collection - and has profoundly influenced countless modern languages. In fact, functions such as map, filter and reduce were all introduced by Lisp.

Lisp Cycles

Using Lisp

Getting Started

My Lisp supports the classic Lisp features, such as functions, lambdas, closures and even powerful macros.

; add 2 and 3
(+ 2 3)

; print to terminal
(defvar my-name "Jakob")
(println "Hello " my-name)

; define a function
(defun sq (x)
  (* x x))

; apply the function 'sq' to every element of the list
(map sq (list 1 2 3 4))
; => (1 4 9 16)

; returns a list of all elements larger than 3
(filter (lambda (x) (> x 3)) (list 1 2 3 4 5))
; => (4 5)

Shell Scripting

My Lisp doesn’t just stop at the traditional language features, it also comes with some neat tools for shell scripting. This means you can mix the elegance of Lisp with the power of your Linux command line.

Here are a few examples to show what it can do:

; pipe output of 'ls' to 'grep'
(pipe (sh ls -la) (sh grep "*.txt"))

; save the output of 'cat' to variable 'content'
(defvar content ($ (sh cat "my-file.txt")))

; pipe the output of the function 'hello' into the 'rev' command
(defun hello (name)
  (strcat "Hello " name "!"))

(pipe (<<< (hello "Jakob")) (sh rev))
; => !bokaJ olleH

; eqivalent to 'sudo apt update && sudo apt upgrade -y' in bash
(and (sh sudo apt update) (sh sudo apt upgrade -y))

; define a function that uses 'scp' to upload some files to a server
(defun upload (source)
  (let ((user "root")
        (host "example.com")
        (target-dir "/var/www/files/")
        (target (strcat user "@" host ":" target-dir)))
    (sh scp source target)))

(upload "my-file.txt")

Because everything is a Lisp expression, you can easily combine, nest, or even generate shell commands programmatically. For example, you could build dynamic command pipelines or iterate over files, all without ever leaving Lisp syntax.

By adding the shebang #!/usr/bin/lisp to the top of your lisp scripts and by making them executeable with chmod +x my-script.lsp, you can run your Lisp program just like any other shell script.

Try it yourself!

You can easily build lisp from source by cloning the GitHub repo or, if you are brave enough, install the pre-built binary:

bash <(curl -s https://raw.githubusercontent.com/gue-ni/redstart/refs/heads/master/tools/install.sh)

You can also find a more detailed reference at the repo.