1. Introduction

Riptide is a Lisp-like shell scripting language designed around the UNIX philosophy.

  • Executables in $PATH are first-class functions

  • Environment variables are first-class

  • Designed around piping

  • Strict evaluation model, with explicit lazy evaluation using functions

  • Functions are no different from commands

  • Built-in shell argument parsing. Functions receive arguments in the same way executables do.

2. Basic concepts

A Riptide script is a file or string containing valid Riptide expressions. To explore the syntax, let’s create a Riptide script as a file and explore its contents. Below is a very basic Riptide script:


println "Hello world!"

Running the above script yields this result:

Hello world!


Like most scripting languages, we can add comments to our script to add documentation or explanation to our script without affecting how it runs. Comments are indicated with a #.


# This is a comment
println "Hello world!"

A comment can appear anywhere on a line, and includes everything to the end of the line:


println "Hello world!"  # This is also a comment

The first line in the script is called the shebang line. In UNIX-like systems, this tells the operating system how to run the script. Since it does not affect the script itself, and it starts with #, Riptide just sees it as a comment and ignores it.

2.2. Whitespace

Generally, whitespace has no meaning in Riptide, except when used inside quotes to form a string. You are free to use whitespace however you like to format your scripts to make them readable.

2.3. Function calls

Even though Riptide is designed for shell scripting, its design is heavily inspired by functional languages like Lisp, Scheme, and Haskell. In Riptide, this means that we need to introduce function calls right away, since almost everything in Riptide is a function call.

Since function calls are so prevalent, they have a barebones syntax so that you don’t have to write a lot of boilerplate text. For example, to call a function named a with three arguments we would write:

a b c d

Like Haskell, function calls use prefix syntax, with the arguments following the function name. Generally, you can choose to drop the parentheses whenever a function call is by itself on its own line.

Should you choose to put more than one function call on a line, you may use a semicolon:

# This is two distinct function calls...
a b; c d

# or equivalently
a b
c d

It seems like a small and possibly useless thing to allow you to omit the parentheses, but it actually helps immensely with readability while maintaining the same general structure as Lisp. In fact, many simple scripts might not need to use any parentheses at all, easily dodging the problem of getting "Lost Ina Seaof Parentheses".

We’ll talk about how to create our own functions later.

Nested function calls

Oftentimes, you will need to pass the result of one function call as an argument to another.

For example, passing the result of one function as an argument to another requires use of parentheses:

# Pass the result of calling 'c' as an argument to 'a'
a b (c d)

2.4. Expressions

After function calls, the second most important thing in Riptide is expressions. It is important to realize that all syntactic forms in Riptide are all different types of expressions.

An expression is one of three things:

  1. A literal value.

  2. A list of expressions.

  3. A function call that results in an expression.

You can use both types in the exact same way. Let’s look at our hello world example again:


print "Hello world!"

In our function call to print, the first argument we give is "Hello world!". This is an example of a literal expression, in particular a literal string. Riptide offers a couple of data types, including numbers and booleans, that can be written as a literal expression.

When writing a literal, you can omit the quotes " if the literal does not contain any whitespace or characters that have other special meaning, like ). For example, "hello" and simply hello are equivalent. "hello world" and hello world are not equivalent; the latter will be interpreted as two separate literals

2.5. Lists

Technically, Lisp does not have lists, only "cells" and "atoms". This is interesting, but not really useful for our purposes. When we say that Riptide has "lists", we really mean it. Lists are built-in types, with many uses. Lists are an in-memory structure, and do not have a syntax of their own. Thankfully, there is a built-in function called list to help us create lists:

list 1 2 3

The result of the above function call will be a list containing the values 1, 2, and 3 in order.

2.6. Tables

table a=b c=d

2.7. Statements

2.8. Functions and blocks

In Riptide, functions are first-class values. In fact, a function is merely a sequence of expressions whose evaluation is delayed until called. Function syntax uses curly braces ({ and }) instead of parenthesis to enclose their body. The general syntax of a block is


Within a block, a statement is a standalone expression to be evaluated. Statements can be separated by newlines or by a semicolon ;.

Here is an example of defining a function called hello:

def hello {
    println "Hello World!"

Note that we’re using def again here. Functions by themselves do not have names, but they can be bound to a name in the same way as expressions to form variables.

Positional arguments

Unlike conventional scripting languages, all function calls are variadic; that is, they take any number of arguments. If any arguments are passed to a block, by default they are bound for you to a variable named $@, which contains all arguments as a list. For example, if we wanted to make an echo clone, we could write:

def echo {
    println ..$@

They are also accessible in variables named with an integer of the position, such as $0, $1, $2, etc.

Named arguments

Named arguments need some work. How can we implement flags?

Positional arguments are useful when accepting a sequence or list of like-values, but can become more difficult to read in a function where argument order matters. Instead of using argument positions, we can give our arguments names inside angle brackets (<>) proceeding the block:

def log <level message> {
    eprintln (str:upper $level)": $message"

log warn "Danger, Will Robinson!"

Named arguments can also be passed in by name using --name value syntax:

log --level warn "Danger, Will Robinson!"

Arguments specified this way can be given in any order:

log --message "Danger, Will Robinson!" --level warn

When an argument is bound to a name, it is removed from the $@ list. In this way, $@ can be used to collect all arguments that were unrecognized or extra.

2.9. Control flow

Unlike most imperative languages, Riptide has no special forms or cases for built-in language constructs. Instead, control structures use functions to apply conditional logic. (That’s why we covered functions before we talked about control structures.)


Take the humble if statement. In Riptide, an if statement looks like this:

if (= (+ 2 2) 4) {
    println "Hey, math works!"

This looks pretty similar to an imperative language, but don’t let that trip you up. if here is actually a built-in function bound to the name if. Here we call if with two arguments:

  • (= (+ 2 2) 4): This is a straightforward expression, which reduces to true.

  • { println "Hey, math works!" }: Hey, this is a function! if calls the second argument as a function if and only if the first expression given to it is truthy.

if can also take additional arguments to form "else if" and "else" cases:

if (= (+ 2 2) 4) {
    println "Hey, math works!"
} elseif (= (+ 2 2) 10) {
    println "In base 4, I'm fine!"
} else {
    println "Math must not work."


while {= (+ 2 2) 4} {
    println "Hey, math works!"
Note that the while condition is passed as block instead of in parentheses. Using parentheses would cause the loop condition to be evaluated only once, and while would either loop infinitely or not at all.


match $input {
    case "hello" {
        println "Hi"
    default {
        println "Unrecognized input"

2.10. Bindings

Now that you understand function calls, function blocks, and expressions, we can finally talk about bindings. First, recall the function call syntax:

a b c d

Originally I referred to a here as the "function name", but that was not entirely honest, though sufficient to explain the function call syntax. In the above code, the word a is actually the name of a binding. In many ways, a binding is like a variable in other languages.

def x 1
def y 2
def z (+ $x $y)

To distinguish between a string and a binding, the dollar sign, or binding sigil ($) is used. For example, we can bind the string "Hello world" to a name and then print it out later:

def message "Hello world"
println $message

When invoking a binding as a function, the sigil is optional. Thus the following programs are equivalent:

def main {
    println "Hello world"

def main {
    println "Hello world"


3. String interpolation

def foo world
println "Hello $foo"
println "Hello $(uppercase foo)"
println "Hello dynamic string: $({
    return $foo

# Format options
def a-float 3.14159
println "PI = ${a-float:.3}" # Prints "PI = 3.142"

4. Exceptions

try {
    throw "an exception"
} <exception> {
    println "exception caught: $exception"

5. Pipes and streams

An example:

send 1 2 3 | {
    loop {
        println "Received:" (recv)

The above should output:

Received: 1
Received: 2
Received: 3

6. Including files

include stuff.rf

7. Modules

require mymodule

8. Processes and concurrency

# Executed in the background
spawn {
    println "Hello world!"

9. Examples

Nested function application.

(((a) b) c) d

Statements in a block. Call a, then b, and then c.

    a; b

IO redirection:

# write to hello.txt
print hello | write hello.txt
# append to hello.txt
print world | write -a hello.txt

Map function using recursion:

def map <list callback> {
    if $list {
        callback (first $list)
        map (tail $list) $callback

Immediately Invoked Function Expression (IIFE):

    println $@
} a b c