Files
lush/issues/26-shell-abbreviations-and-user-builtins.md
Cormac Shannon 768de9fe8b Expose lush global as a standard library (issue #26)
Register luaopen_lush() in stdlibs so the lush table is available as a
global. Users can now extend the shell by adding functions to
lush.builtins. The OP_LUSH VM access via integer keys is preserved.
2026-03-18 09:52:56 +00:00

97 lines
3.9 KiB
Markdown

# 26 — Shell abbreviations and user-extensible builtins
**Status:** done
## Problem
Users cannot define shell abbreviations or custom commands. For example, there's no way to alias `gs` to `git status` or add a custom `mkcd` that creates a directory and cd's into it.
## Current architecture
Commit `f88b1795` moved all shell internals (`__command`, `__interactive`, `__getenv`, `__setenv`) out of `_G` and into a hidden registry table at `LUA_RIDX_LUSH`. Builtins (`cd`, `exec`, `umask`) live at `LUA_RIDX_LUSH.builtins`. The dispatch path in `try_builtin()` (`lcmd.c:875`) already does a table lookup:
```c
lua_getfield(L, -1, "builtins") /* get builtins table */
lua_getfield(L, -1, pa->argv[0]) /* look up command name */
```
This means builtins are **already data-driven** — if a function appears in the table, it gets called. But the table is invisible to user code, so there's no way to add entries.
## Proposal: expose `lush.builtins` to user code
Expose the `LUA_RIDX_LUSH` table (or a curated view of it) as a global like `lush` or `__lush`. Users extend the shell by adding functions to `lush.builtins`:
```lua
-- abbreviation
lush.builtins.gs = function(cmd)
!git status
end
-- custom builtin
lush.builtins.mkcd = function(cmd, dir)
!mkdir -p ${dir}
!cd ${dir}
end
-- override (with access to original)
local orig_cd = lush.builtins.cd
lush.builtins.cd = function(cmd, dir)
orig_cd(cmd, dir)
print("now in: " .. $PWD)
end
```
This requires no new dispatch mechanism — `try_builtin()` already does the right thing. The only change is making the table accessible.
## Design considerations
### What to expose
Option A: Expose the entire `LUA_RIDX_LUSH` table as `lush`:
```lua
lush.builtins -- cd, exec, umask, user additions
lush.command -- __command (backtick execution)
lush.interactive -- __interactive (! execution)
lush.getenv -- __getenv ($VAR read)
lush.setenv -- __setenv ($VAR write)
```
This gives power users access to the shell primitives directly, which is useful for building abstractions. But it also means users could break internals.
Option B: Expose only `lush.builtins` — safer, sufficient for the abbreviation use case.
Consider the clarity of these endpoints. Would an alternative naming scheme be clearer? i.e.
```lua
lush.commands -- cd, exec, umask, user additions
lush.run -- __command (backtick execution)
lush.interactive -- __interactive (! execution) -- is there an idiotmatic name for this?
lush.getenv -- __getenv ($VAR read)
lush.setenv -- __setenv ($VAR write)
```
### Relationship to f88b1795
That commit deliberately hid these from `_G` to avoid polluting the namespace. Exposing a single `lush` global is a middle ground: one clean entry point instead of scattered `__double_underscore` globals, and users can only extend via a structured API rather than accidentally shadowing internals.
### Abbreviations vs builtins
Shell abbreviations (fish-style text expansion) and builtins (function dispatch) are different features in traditional shells. But in lush, a builtin that calls `!git status` achieves the same effect as an abbreviation — the `!` prefix runs the command interactively with terminal inheritance. So a single mechanism covers both use cases.
### Builtin protocol
Current builtins receive `(cmd_name, arg1, arg2, ...)` and return a result table `{code, stdout, stderr}`. User builtins should follow the same protocol. Document this as the contract.
## Files likely affected
| File | Changes |
|------|---------|
| `linit.c` | Expose `LUA_RIDX_LUSH` table as `lush` global |
| `lcmd.c` | No changes — `try_builtin()` already works via table lookup |
| `lbuiltin.c` | No changes — existing builtins stay as-is |
## Open questions
- Name: `lush`, `shell`, `__shell`? `lush` is clean and matches the project name.
- Should `lush.command` / `lush.interactive` be exposed or kept hidden?
- Should there be a `builtin` command to bypass user overrides (like bash's `builtin cd`)?