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

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? 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)?