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
})