diff --git a/README.md b/README.md index ee4ad26f..8001ba2a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,246 @@ # Lush -Lush is a shell built on [Lua 5.5.0](https://www.lua.org/). It extends Lua with shell-oriented features like command execution, pipelines, and built-in shell commands while maintaining full compatibility with the Lua 5.5 ecosystem. +Lush is a shell built on [Lua 5.5.0](https://www.lua.org/). 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](https://www.lua.org/) by R. Ierusalimschy, L. H. de Figueiredo, W. Celes (PUC-Rio). + +## Quick Start + +Build and run: + +```sh +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. + +```lua +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 `_`. + +```lua +!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. + +```lua +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:** + +```lua +local name = "world" +`echo ${name}` -- hello world +`echo ${40 + 2}` -- 42 +`echo ${name:upper()}` -- WORLD +``` + +`**$NAME` — Environment variable:** + +```lua +`echo $HOME` +`echo $USER` +``` + +`**$(cmd)` — Subcommand substitution:** + +```lua +`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: + +```lua +-- 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: + +```lua +`echo *.lua` -- expands to matching files +`echo src/**/*.c` +`echo file?.txt` -- single-character wildcard +`echo config.[ch]` -- bracket patterns +``` + +Brace expansion: + +```lua +`echo {a,b,c}` -- a b c +`echo file.{lua,c,h}` -- file.lua file.c file.h +``` + +Tilde expansion: + +```lua +`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): + +```lua +`echo 'hello world'` +`echo 'no $expansion here'` +``` + +Double quotes allow escapes and interpolation: + +```lua +`echo "hello world"` +`echo "tab:\there"` +`echo "path is $HOME"` +`echo "sum is ${1 + 2}"` +``` + +Backslash outside quotes escapes the next character: + +```lua +`echo hello\ world` -- single argument: hello world +``` + +### Builtins + +Built-in commands that affect the shell process: + +```lua +`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: + +```lua +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: + +```lua +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: + +```lua +lush.builtins.hello = function(cmd, name) + return {code = 0, stdout = "hi " .. (name or "") .. "\n", stderr = ""} +end + +`hello world` -- hi world +``` + +**Aliases:** + +```lua +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: + +```lua +-- In config.lua: +_PROMPT = "lush> " +_PROMPT2 = "...> " + +-- Dynamic prompt: +_PROMPT = setmetatable({}, { + __tostring = function() + return os.date("%H:%M") .. " > " + end +}) +``` +