Files
lush/README.md
2026-03-23 22:05:45 +00:00

5.7 KiB

Lush

Lush is a shell built on Lua 5.5.0. It extends Lua with shell-oriented features — backtick commands, pipes, globbing, interpolation, environment variables — while remaining a fully compatible Lua 5.5 interpreter.

Based on Lua by R. Ierusalimschy, L. H. de Figueiredo, W. Celes (PUC-Rio).

Quick Start

Build and run:

make
./lush

Language Guide

Lush is Lua with shell extensions. Every valid Lua program runs unchanged. The additions are described below.

Backtick Commands

Execute shell commands with backticks. Each command runs in its own child process. Stdout and stderr are captured and returned as a table with code, stdout, and stderr fields.

local r = `echo hello`
print(r.stdout)   -- "hello\n"
print(r.code)     -- 0

local r = `ls nonexistent`
print(r.code)     -- non-zero
print(r.stderr)   -- error message

Empty backticks return {code=0, stdout="", stderr=""}. A command killed by a signal returns exit code 128 + signal number (e.g. 137 for SIGKILL).

Interactive Commands

Prefix a line with ! to run a command in a child process with stdout and stderr inherited from the terminal. The result is stored in _.

!ls -la
print(_.code)   -- 0

!sh -c "exit 42"
print(_.code)   -- 42

Works anywhere a statement is valid — inside functions, loops, if-blocks.

Pipes

Use | to connect commands in a pipeline. Each stage runs in its own process with stdout piped to the next stage's stdin.

local r = `echo hello | tr a-z A-Z`
print(r.stdout)   -- "HELLO\n"

!ps aux | grep lush | head -5

The exit code comes from the last stage. Stderr from the last stage is captured (in backtick mode); stderr from middle stages goes to the terminal. Quoting ("a|b", 'a|b', a\|b) treats | as literal.

String Interpolation

Three forms of interpolation inside backtick and interactive commands:

**${expr} — Lua expression:**

local name = "world"
`echo ${name}`              -- hello world
`echo ${40 + 2}`            -- 42
`echo ${name:upper()}`      -- WORLD

**$NAME — Environment variable:**

`echo $HOME`
`echo $USER`

**$(cmd) — Subcommand substitution:**

`echo today is $(date +%Y-%m-%d)`
`echo $(echo $(echo nested))`      -- nested substitution

Escape with backslash: \$HOME produces the literal string $HOME.

Environment Variables

Read and write environment variables directly from Lua using $NAME syntax:

-- Read
local home = $HOME
local path = $PATH

-- Write (visible to child processes)
$MY_VAR = "hello"
`echo $MY_VAR`          -- hello

-- Unset
$MY_VAR = nil

-- Numbers are coerced to strings
$PORT = 8080

Globbing

Shell glob patterns are expanded inside backtick commands:

`echo *.lua`                -- expands to matching files
`echo src/**/*.c`
`echo file?.txt`            -- single-character wildcard
`echo config.[ch]`          -- bracket patterns

Brace expansion:

`echo {a,b,c}`             -- a b c
`echo file.{lua,c,h}`      -- file.lua file.c file.h

Tilde expansion:

`echo ~`                    -- /home/user
`echo ~/projects`           -- /home/user/projects

Quoting suppresses expansion: "*.lua", '*.lua', and \*.lua are all literal.

Quoting

Single quotes are literal (no escapes):

`echo 'hello world'`
`echo 'no $expansion here'`

Double quotes allow escapes and interpolation:

`echo "hello world"`
`echo "tab:\there"`
`echo "path is $HOME"`
`echo "sum is ${1 + 2}"`

Backslash outside quotes escapes the next character:

`echo hello\ world`        -- single argument: hello world

Builtins

Built-in commands that affect the shell process:

`cd /tmp`                   -- change directory
`cd -`                      -- previous directory
`cd`                        -- home directory

`umask`                     -- query current umask
`umask 0077`                -- set umask

The lush Library

Programmatic API for shell operations:

lush.capture("echo hello")        -- like backticks, returns {code, stdout, stderr}
lush.run("echo hello")            -- returns stdout with trailing newline stripped
lush.interactive("ls")            -- like !, returns result and sets _

lush.envget("HOME")               -- read env var
lush.envset("MY_VAR", "value")    -- write env var

Extensibility

Custom commands run in a child process (like any external command), so their output is captured and they support piping:

lush.commands.greet = function(name, ...)
  print("hello " .. (({...})[1] or "world"))
end

`greet lush`                -- hello lush
`greet lush | tr a-z A-Z`  -- HELLO LUSH

Custom builtins run in the current Lua process, so they can modify shell state (like cd does). They receive the raw command table and return a result table:

lush.builtins.hello = function(cmd, name)
  return {code = 0, stdout = "hi " .. (name or "") .. "\n", stderr = ""}
end

`hello world`               -- hi world

Aliases:

lush.aliases.ll = "ls -la"
`ll`                         -- runs: ls -la

lush.aliases.ll = nil        -- remove alias

Configuration

Lush loads config files on interactive startup (suppressed with -E):

  • $XDG_CONFIG_HOME/lush/config.lua — main config
  • $XDG_CONFIG_HOME/lush/config.d/*.lua — additional configs, loaded alphabetically

Use config files to set up aliases, custom commands, prompts, and environment.

Prompt

Customize the interactive prompt:

-- In config.lua:
_PROMPT = "lush> "
_PROMPT2 = "...> "

-- Dynamic prompt:
_PROMPT = setmetatable({}, {
  __tostring = function()
    return os.date("%H:%M") .. " > "
  end
})