Writing Shell Scripts with Lisp? This C++ Niche Tool is Amazing!

What You Think Shell Programming Is vs What Shell Programming Actually Is

Have you ever had this experience?

Working late at night, the server has a problem. You open the terminal and type a line of <span>bash</span> script:

if [ $? -eq 0 ]; then
    echo "success"
else
    echo "fail"; exit 1
fi

Then you look at the screen filled with <span>[ ]</span>, <span>$?</span>, <span>&&</span>, <span>||</span>, and backslashes <span>\</span>, suddenly questioning your life: “Am I a programmer or am I playing with character puzzles?”

Yes, brother, welcome to the world of Bash—a language universe where the syntax resembles ancient spells, debugging feels like opening blind boxes, and maintenance is akin to defusing bombs.

We use it every day for automating deployments, monitoring logs, batch processing files… but no one dares to say “I love Bash.” It’s like that old refrigerator in your house that has been buzzing for ten years: it works, but every time you open the door, you fear it might fall over.

Bringing Lisp into the World of Shell Scripts!

At that moment, it felt like my brain was struck by lightning—who knew scripting languages could be this elegant?

What is redstart? Lisp + Shell = A Divine Combination

Let’s start with the definition:

redstart is a lightweight Lisp interpreter written in C++, specifically designed for Linux Shell scripting.

Sounds unremarkable? Hold on, the key is in its positioning: it allows you to write Shell scripts using Lisp syntax.

What does that mean?

It means you no longer have to write this kind of “heavenly book” style Bash:

for file in *.txt; do
    if [[ -f "$file" ]]; then
        cp "$file" "/backup/$file.bak"
    fi
done

Instead, you can rewrite it in this fresh and unique Lisp style:

(for-each (lambda (file)
            (when (file? file)
              (sh cp file (strcat "/backup/" file ".bak"))))
          (glob "*.txt"))

Doesn’t it feel like you can breathe easier all of a sudden? There are many parentheses, but the structure is clear, and the logic is distinct, without the bizarre <span>$()</span> and double-quote traps.

This isn’t called progress; what is called revolution?

It’s not the first Lisp, nor the first Shell tool

Lisp was born in 1958, older than many programmers’ parents. It is known for its powerful macro system, functional programming capabilities, and minimal S-expressions. However, it has always been more of an academic darling, with limited industrial applications.

And Shell scripts? They represent the pinnacle of pragmatism, directly invoking system commands, piping, and redirection—simple, crude, and effective.

The brilliance of redstart lies in: it stitches together these two seemingly unrelated language paradigms, and it does so quite naturally.

Installation: One Command to Rule Them All, Faster than Ordering Takeout

The official installation method is just one line:

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

Seeing this command might make you tense: “Is it going to install a mining virus again?” Don’t worry, I checked the source code,<span>install.sh</span> is clean, and it does standard things:

  1. Check if <span>cmake</span> and <span>g++</span> are installed
  2. Clone the repository
  3. Compile and build
  4. Place the generated binary <span>rst</span> in <span>/usr/local/bin</span>

The whole process takes less than two minutes, and once completed, you can type <span>rst</span> to start its REPL (interactive environment).

Yes, it even comes with an interactive mode! Unlike some Shell script tools that can only run scripts and not debug.

$ rst
> (print "Hello, Redstart!")
Hello, Redstart!
=> nil

Did you see that? <span>=> nil</span> returns a style that is full of Lisp flavor.

Core Capabilities Demonstration: The Leap from “Usable” to “User-Friendly”

Let’s see how great redstart really is.

1. Basic Operation: Execute Commands & Get Output

In traditional Bash, if you want to get the output of a command, you would write:

content=$(cat my-file.txt)
echo "$content"

But in redstart, you only need:

(defvar content ($ (sh cat "my-file.txt")))
(print content)

Let’s explain:

  • <span>(sh ...)</span> indicates executing a shell command
  • <span>($ ...)</span> captures the standard output of that command
  • <span>defvar</span> defines a variable (Lisp style)
  • Strings must be enclosed in double quotes

It’s concise and clear, without the visual clutter brought by <span>$()</span> nesting.

Even better, it supports chaining operations. For example, if you want to pass the result of <span>ls</span> to <span>grep</span>, in Bash you would write:

ls -la | grep "\.txt"

In redstart, it’s written as:

(pipe (sh ls -la) (sh grep "\\.txt"))

Note that here we used the <span>pipe</span> function, which clearly expresses the intention of “piping,” rather than relying on the <span>|</span> symbol for implicit connection. The readability is significantly improved.

2. Variables and Context: Use <span>_</span><code><span> to Get the Last Result</span>

There’s a saying in the Lisp community: “Too many parentheses can be dizzying,” but redstart provides a very user-friendly feature: using <span>_</span><span> to reference the return value of the last expression</span>.

For example:

($ (sh ls))
; => "file1.txt\nfile2.log\nscript.sh"

(defvar files (split "\n" _))
; Equivalent to splitting the last output by newline into a list

Here, <span>_</span> is equal to the result of the previous <span>($ (sh ls))</span><code><span>. It’s a bit like Python’s </span><code><span>_</span> or Jupyter Notebook’s history variable?

This small design greatly enhances the interactive experience. When debugging, you don’t have to repeatedly copy and paste results; you can directly continue operations based on the last execution.

3. Conditional Judgments and Control Flow: Say Goodbye to <span>[ ]</span><span> Hell</span>

Bash’s conditional judgments are a prime example of anti-human design:

if [ "$status" -eq 0 ] &amp;&amp; [ -f "$filename" ]; then
    echo "OK"
fi

Spaces are crucial, variables must be quoted, numeric comparisons use <span>-eq</span>, and strings use <span>==</span>… a small mistake can lead to errors.

Redstart, on the other hand, uniformly uses Lisp-style logical expressions:

(and (= status 0)
     (file? filename))

Or the complete version:

(when (and (= status 0)
           (file? filename))
  (print "OK"))

All operators are consistently in prefix form, eliminating the need to memorize special rules. <span>=</span> can be used for both numeric and string comparisons, and <span>file?</span> checks if a file exists, making it clear and intuitive.

4. Function Definitions: Supports Recursion, Closures, and Higher-Order Functions

This is where the true charm of Lisp lies.

Recursive Functions Made Easy

For example, calculating factorial:

(defun factorial (n)
  (if (= n 0)
      1
      (* n (factorial (- n 1)))))

(factorial 5) ; => 120

This code is not only highly readable but also fully conforms to the mathematical definition. Try writing recursion in Bash? Just the function declaration with <span>function factorial()</span> and local variables <span>local n</span> is enough to give you a headache.

Closures are also included!

(defun make-adder (a)
  (lambda (b) (+ a b)))

(defvar add5 (make-adder 5))
(add5 3) ; => 8

Did you see that? <span>make-adder</span> returns an anonymous function (lambda) and binds the external variable <span>a</span>. This is a typical closure.

This means you can write highly abstract utility functions, such as dynamically generating SSH executors with different permission levels or creating different deployment strategies based on environment configurations.

5. Practical Case: One-Click Upload of Multiple Files to the Server

Suppose you want to upload a bunch of <span>.tar.gz</span> files from your local machine to a remote server. The conventional approach is to write a Bash script that loops calling <span>scp</span>.

But in redstart, you can write it like this:

(defun upload (source)
  (let ((user "root")
        (host "example.com")
        (target-dir "/var/www/backups/")
        (target (strcat user "@" host ":" target-dir)))
    (sh scp source target)))

; Get all tar.gz files
(defvar files (filter (lambda (f) (ends-with f ".tar.gz"))
                      (glob "*")))

; Batch upload
(for-each upload files)

Let’s break down the highlights:

  • <span>glob "*"</span> is similar to wildcard matching, returning a list of files
  • <span>filter</span> combined with <span>lambda</span> filters out specific suffixes
  • <span>for-each</span> iterates to execute the <span>upload</span> function
  • <span>strcat</span> concatenates strings, avoiding manual concatenation errors

The entire code logic is clear and modular, making future expansions easy. For example, if you want to add a compression step, just insert <span>(sh tar -czf ...)</span> at the front.

Technical Principles Analysis: How C++ Creates an Embedded Lisp

Now let’s change perspectives and see how redstart is implemented from a developer’s point of view.

The project structure is as follows:

redstart/
├── CMakeLists.txt         # Build configuration
├── src/                   # Core source code
│   ├── interpreter.cpp    # Main logic of the interpreter
│   ├── parser.cpp         # S-expression parsing
│   ├── eval.cpp           # Evaluation engine
│   └── builtin.cpp        # Built-in functions like sh, pipe, $ etc.
├── libs/                  # Third-party dependencies (possibly linenoise for beautifying REPL)
├── tests/                 # Unit tests
└── tools/                 # Installation scripts and other tools

The entire project is based on modern C++ (likely C++17 or above), adopting a modular design for easier maintenance and expansion.

Key Technical Point 1: S-expression Parsing

The core of Lisp is S-expressions (Symbolic Expressions), which represent tree-structured data or code using parentheses.

For example:

(pipe (sh ls) (sh grep ".txt"))

This will be parsed into a syntax tree:

       pipe
      /      \
    sh(ls)   sh(grep ".txt")

The task of parser.cpp is to convert text into such an AST (Abstract Syntax Tree). Typically, a recursive descent parser is used, analyzing character by character the levels of parentheses and atomic types (symbol, number, string).

Key Technical Point 2: Evaluation Model (Eval)

With the AST in hand, the next step is evaluation (evaluate). redstart uses the classic eval-apply loop, which is the soul mechanism of Lisp.

In simple terms:

  • <span>eval</span> is responsible for evaluating expressions
  • <span>apply</span> is responsible for calling functions and passing parameters

For example, when encountering <span>(sh ls)</span>, the interpreter will:

  1. Check if <span>sh</span> is a built-in function
  2. Pass <span>ls</span> as a parameter
  3. Call the system <span>execvp("ls", ...)</span> to execute the command
  4. Return the process handle or output result

For user-defined functions (like those defined with <span>defun</span>), it also needs to maintain an environment to store variable bindings and scopes.

Key Technical Point 3: How is Shell Integration Achieved?

The core question arises: How can Lisp seamlessly call Shell commands?

The answer is: by encapsulating <span>fork()</span> + <span>exec()</span> system calls.

The pseudocode is roughly as follows:

Value builtin_sh(std::vector<Value> args) {
    pid_t pid = fork();
    if (pid == 0) {
        // Child process: execute command
        execvp(args[0].c_str(), &args[0]);
        exit(1);
    } else {
        // Parent process: wait and collect results
        int status;
        waitpid(pid, &status, 0);
        return (WIFEXITED(status) && WEXITSTATUS(status) == 0);
    }
}

Additionally, to support piping (<span>pipe</span>), it also needs to use the <span>pipe(2)</span> system call to create anonymous pipes and redirect the stdout of the previous command to the stdin of the next command.

This part involves knowledge of the operating system’s lower levels, but redstart encapsulates it well, so users are completely unaware of the complexity.

Comparing Other Solutions: Why Not Use Python or Node.js?

You might ask: “Since I want to write advanced scripts, why not use Python? Can’t it also call <span>subprocess</span>?”

It certainly can, but each has its applicable scenarios.

Solution Advantages Disadvantages
Bash Ready to use, no installation required Ugly syntax, hard to maintain
Python Powerful features, rich ecosystem Slow startup, many dependencies
Node.js Asynchronous friendly, universal JS High redundancy, not suitable for lightweight scripts
redstart Fast startup, elegant syntax, specifically designed for Shell Small ecosystem, slightly higher learning curve

The biggest advantage of redstart is: Lightweight + Fast + Focused on Shell Scenarios.

Its binary file is usually only a few MB, with startup speeds in the millisecond range, making it suitable for CI/CD pipelines, scheduled tasks, and container initialization scripts.

While Python scripts are more powerful, they require loading the interpreter, importing libraries, and setting up virtual environments each time… which is overkill for simple deployment scripts.

More importantly, the DSL (Domain-Specific Language) characteristics of redstart make it closer to Shell thinking. For example:

  • <span>(sh cmd)</span><span> directly corresponds to command line</span>
  • <span>($ ...)</span><span> clearly indicates "I want to get the output"</span>
  • <span>(pipe a b)</span><span> clearly reflects the direction of data flow</span>

These are not strengths of general-purpose languages.

Mind Upgrade: From “Command Concatenation” to “Programmatic Operations”

redstart is not just a tool; it represents a shift in mindset.

Traditional Shell scripts are essentially “command sequences”:

cd /app
git pull
npm install
npm run build
systemctl restart app

This is a linear, procedural way of thinking.

In contrast, redstart encourages you to think in a functional + structured manner:

(defun deploy ()
  (and (chdir "/app")
       (sh git pull)
       (sh npm install)
       (sh npm run build)
       (restart-service "app")))

(when (network-online?)
  (deploy))

You can:

  • Encapsulate repetitive logic into functions
  • Use <span>map</span> and <span>filter</span> to handle batch tasks
  • Use <span>cond</span> to replace nested <span>if-elif</span>
  • Utilize <span>let</span> to create local scopes and prevent pollution

This transforms scripts from being “one-off glue code” into maintainable programs with engineering qualities.

Limitations & Future Prospects

Of course, redstart is still a young project (created in 2021, latest update in 2025), and it currently has some shortcomings:

Current Limitations

  1. Small Community: Only 14 stars, limited documentation, hard to find answers when encountering issues
  2. Weak Standard Library: Lacks support for modern needs like JSON parsing, HTTP requests, etc.
  3. Poor Cross-Platform Support: Primarily aimed at Linux, macOS may be compatible, but Windows is basically out of the picture
  4. Weak Debugging Tools: Lacks features like breakpoints, stack tracing, etc.

Possible Development Directions

If the author is willing to continue investing, I believe it can develop in several directions:

  1. Add a REPL Debugger: Support <span>(debug expr)</span><span> to view the evaluation process</span>
  2. Built-in Network Module: Add functions like <span>(http-get url)</span><span>, </span><code><span>(json-parse str)</span><span>, etc.</span>
  3. Open Macro System: Allow users to define syntax extensions
  4. Compile into Standalone Binaries: Similar to Go, package scripts as single-file executable programs

Once achieved, redstart could very well become a candidate for the next generation of DevOps scripting languages.

My Advice: When to Try redstart?

After all this, should you immediately abandon Bash and fully switch to redstart?

Not so fast, let’s be rational.

I suggest considering it in the following scenarios:

Situations Suitable for Using redstart:

  • You need to write complex automation scripts (like deployment, backup, monitoring)
  • Your team has some background in Lisp or functional programming
  • Your project is sensitive to startup speed and resource usage
  • You are tired of the syntax torture of Bash

Situations Not Recommended:

  • Simple one-off commands (Bash is faster)
  • Need to be widely compatible with legacy systems (redstart requires manual installation)
  • All team members are Shell novices, making the learning curve too steep
  • Must run on Windows

In other words: redstart is not meant to replace Bash, but to surpass Bash.

In Conclusion: The Charm of Technology Lies in Constantly Breaking Boundaries

The redstart project reminds me of a saying:

“When you only have a hammer, everything looks like a nail.”— Abraham Maslow

Bash is the “hammer” in our hands, but we forget that there are screwdrivers, electric drills, and laser cutters in the world.

The significance of redstart is not just providing a new tool, but reminding us:

Scripting languages can also have beauty, and automation can be elegant.

It’s like a little red-tailed bird, gracefully flying through the jungle of Unix commands, leaving behind a series of crisp parentheses sounds.

It may never become mainstream, but it proves one thing:

In this era dominated by JavaScript and Python, there are still people willing to write a small Lisp interpreter in C++ just to make the world have one less line of incomprehensible <span>$()?!”@#$%&*</span>.

This dedication deserves a cup of tea.

Appendix: Quick Reference Manual (Cheatsheet)

Function Bash Syntax redstart Syntax
Execute Command <span>ls -la</span> <span>(sh ls -la)</span>
Get Output <span>$(ls)</span> <span>($ (sh ls))</span>
Pipe <span>ls \\| grep txt</span> <span>(pipe (sh ls) (sh grep "txt"))</span>
Define Variable <span>name="Alice"</span> <span>(defvar name "Alice")</span>
Conditional Judgment <span>[ $x -eq 5 ]</span> <span>(= x 5)</span>
Loop Through <span>for f in *; do ...</span> <span>(for-each fn (glob "*"))</span>
String Concatenation <span>"hello $name"</span> <span>(strcat "hello " name)</span>
Last Result N/A <span>_</span>

What You Can Do Next?

  1. Open the terminal, run the installation command, and try <span>rst</span>
  2. Write an automated backup script and compare its readability with the Bash version
  3. Give redstart a star on GitHub 👉 https://github.com/gue-ni/redstart[1]
  4. If interested, try contributing a piece of documentation or test case

After all, every great open-source project starts with one person’s curiosity.

And today, this little red-tailed bird is perched on the edge of your command line window, waiting for you to type the first line <span>(print "Hello World")</span>.

Are you going to give it a try?

References

[1]

https://github.com/gue-ni/redstart: https://github.com/gue-ni/redstart

Leave a Comment