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.
This commit is contained in:
Cormac Shannon
2026-03-18 09:52:56 +00:00
parent 1766e40a68
commit 768de9fe8b
6 changed files with 161 additions and 24 deletions

View File

@@ -1,6 +1,6 @@
# 26 — Shell abbreviations and user-extensible builtins
**Status:** open
**Status:** done
## Problem
@@ -60,6 +60,15 @@ This gives power users access to the shell primitives directly, which is useful
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.

31
lcmd.c
View File

@@ -1199,3 +1199,34 @@ int lushCmd_setenv (lua_State *L) {
}
return 0;
}
/* ===== lush standard library ===== */
static const luaL_Reg lushlib[] = {
{"command", lushCmd_command},
{"interactive", lushCmd_interactive},
{"getenv", lushCmd_getenv},
{"setenv", lushCmd_setenv},
{"subcmd", lushCmd_subcmd},
{NULL, NULL}
};
LUAMOD_API int luaopen_lush (lua_State *L) {
luaL_newlib(L, lushlib);
/* also store integer-keyed entries for OP_LUSH VM access */
lua_pushcfunction(L, lushCmd_command);
lua_rawseti(L, -2, LUSH_OP_COMMAND + 1);
lua_pushcfunction(L, lushCmd_interactive);
lua_rawseti(L, -2, LUSH_OP_INTERACTIVE + 1);
lua_pushcfunction(L, lushCmd_getenv);
lua_rawseti(L, -2, LUSH_OP_GETENV + 1);
lua_pushcfunction(L, lushCmd_setenv);
lua_rawseti(L, -2, LUSH_OP_SETENV + 1);
lua_pushcfunction(L, lushCmd_subcmd);
lua_rawseti(L, -2, LUSH_OP_SUBCMD + 1);
/* store in registry for OP_LUSH access */
lua_pushvalue(L, -1);
lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH);
return 1;
}

2
lcmd.h
View File

@@ -23,4 +23,6 @@ int lushCmd_getenv (lua_State *L);
int lushCmd_setenv (lua_State *L);
int lushCmd_subcmd (lua_State *L);
LUAMOD_API int luaopen_lush (lua_State *L);
#endif

26
linit.c
View File

@@ -39,30 +39,11 @@ static const luaL_Reg stdlibs[] = {
{LUA_STRLIBNAME, luaopen_string},
{LUA_TABLIBNAME, luaopen_table},
{LUA_UTF8LIBNAME, luaopen_utf8},
{LUA_LUSHLIBNAME, luaopen_lush},
{NULL, NULL}
};
/*
** Store shell functions in a registry table at LUA_RIDX_LUSH.
** Integer keys 1..5 hold the C functions (accessed by OP_LUSH).
** String key "builtins" holds the builtins table (set by luaopen_builtins).
*/
static void opencommand (lua_State *L) {
lua_createtable(L, LUSH_OP_COUNT, 1);
lua_pushcfunction(L, lushCmd_command);
lua_rawseti(L, -2, LUSH_OP_COMMAND + 1);
lua_pushcfunction(L, lushCmd_interactive);
lua_rawseti(L, -2, LUSH_OP_INTERACTIVE + 1);
lua_pushcfunction(L, lushCmd_getenv);
lua_rawseti(L, -2, LUSH_OP_GETENV + 1);
lua_pushcfunction(L, lushCmd_setenv);
lua_rawseti(L, -2, LUSH_OP_SETENV + 1);
lua_pushcfunction(L, lushCmd_subcmd);
lua_rawseti(L, -2, LUSH_OP_SUBCMD + 1);
lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH);
}
/*
** require and preload selected standard libraries
@@ -81,9 +62,8 @@ LUALIB_API void luaL_openselectedlibs (lua_State *L, int load, int preload) {
lua_setfield(L, -2, lib->name); /* add library to PRELOAD table */
}
}
lua_assert((mask >> 1) == LUA_UTF8LIBK);
lua_assert((mask >> 1) == LUA_LUSHLIBK);
lua_pop(L, 1); /* remove PRELOAD table */
opencommand(L); /* register __command global */
luaopen_builtins(L); /* register __builtins table */
luaopen_builtins(L); /* register builtins in lush table */
}

View File

@@ -54,6 +54,10 @@ LUAMOD_API int (luaopen_table) (lua_State *L);
#define LUA_UTF8LIBK (LUA_TABLIBK << 1)
LUAMOD_API int (luaopen_utf8) (lua_State *L);
#define LUA_LUSHLIBNAME "lush"
#define LUA_LUSHLIBK (LUA_UTF8LIBK << 1)
LUAMOD_API int (luaopen_lush) (lua_State *L);
/* open selected libraries */
LUALIB_API void (luaL_openselectedlibs) (lua_State *L, int load, int preload);

111
testes/lush/lushlib.lua Normal file
View File

@@ -0,0 +1,111 @@
-- testes/lush/lushlib.lua
-- Tests for the lush standard library (issue #26).
print "testing lush library"
-- === lush global exists and is a table ===
do
assert(type(lush) == "table", "lush should be a table")
end
-- === named functions are accessible ===
do
assert(type(lush.command) == "function", "lush.command missing")
assert(type(lush.interactive) == "function", "lush.interactive missing")
assert(type(lush.getenv) == "function", "lush.getenv missing")
assert(type(lush.setenv) == "function", "lush.setenv missing")
assert(type(lush.subcmd) == "function", "lush.subcmd missing")
end
-- === lush.command works like backtick ===
do
local r = lush.command("echo hello")
assert(r.code == 0, "lush.command failed")
assert(r.stdout == "hello\n", "expected 'hello\\n', got: " .. r.stdout)
end
-- === lush.getenv / lush.setenv ===
do
lush.setenv("_LUSH_TEST_VAR", "42")
assert(lush.getenv("_LUSH_TEST_VAR") == "42")
lush.setenv("_LUSH_TEST_VAR", nil)
assert(lush.getenv("_LUSH_TEST_VAR") == nil)
end
-- === lush.builtins exists and has cd ===
do
assert(type(lush.builtins) == "table", "lush.builtins missing")
assert(type(lush.builtins.cd) == "function", "lush.builtins.cd missing")
assert(type(lush.builtins.exec) == "function", "lush.builtins.exec missing")
assert(type(lush.builtins.umask) == "function", "lush.builtins.umask missing")
end
-- === user-defined builtin via backtick ===
do
lush.builtins.greet = function(cmd, name)
-- builtins return {code, stdout, stderr}
local t = {}
t.code = 0
t.stdout = "hello " .. (name or "world") .. "\n"
t.stderr = ""
return t
end
local r = `greet lush`
assert(r.code == 0, "user builtin failed")
assert(r.stdout == "hello lush\n",
"expected 'hello lush\\n', got: " .. r.stdout)
-- clean up
lush.builtins.greet = nil
end
-- === user-defined builtin via lush.command ===
do
lush.builtins.myecho = function(cmd, ...)
local args = {...}
local t = {}
t.code = 0
t.stdout = table.concat(args, " ") .. "\n"
t.stderr = ""
return t
end
local r = lush.command("myecho foo bar")
assert(r.code == 0)
assert(r.stdout == "foo bar\n",
"expected 'foo bar\\n', got: " .. r.stdout)
lush.builtins.myecho = nil
end
-- === OP_LUSH still works (backtick, $VAR, !cmd syntax) ===
do
-- backtick
local r = `echo works`
assert(r.stdout == "works\n")
-- $VAR expansion
lush.setenv("_LUSH_LIB_TEST", "yes")
local val = $_LUSH_LIB_TEST
assert(val == "yes", "expected 'yes', got: " .. tostring(val))
lush.setenv("_LUSH_LIB_TEST", nil)
end
print "OK"