Add language guide to README covering all lush features
This commit is contained in:
243
README.md
243
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
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user