Files
lush/issues/26-shell-abbreviations-and-user-builtins.md
Cormac Shannon 08df164692 Add issues #25, #26, #27; update #7 and #22 statuses
- #25: $VAR expansion in commands
- #26: Shell abbreviations and user-extensible builtins
- #27: $(cmd) subcommand syntax
- #22: Updated with implementation details (in progress)
- #7: Standardize status label
2026-03-15 19:33:19 +00:00

88 lines
3.6 KiB
Markdown

# 26 — Shell abbreviations and user-extensible builtins
**Status:** open
## 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.
### 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`)?