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.
3.9 KiB
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:
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:
-- 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:
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.
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?lushis clean and matches the project name. - Should
lush.command/lush.interactivebe exposed or kept hidden? - Should there be a
builtincommand to bypass user overrides (like bash'sbuiltin cd)?