diff --git a/issues/26-shell-abbreviations-and-user-builtins.md b/issues/26-shell-abbreviations-and-user-builtins.md index 9fa06a47..78f522cb 100644 --- a/issues/26-shell-abbreviations-and-user-builtins.md +++ b/issues/26-shell-abbreviations-and-user-builtins.md @@ -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. diff --git a/lcmd.c b/lcmd.c index 535d8de4..f486ab07 100644 --- a/lcmd.c +++ b/lcmd.c @@ -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; +} diff --git a/lcmd.h b/lcmd.h index c922346c..19d82775 100644 --- a/lcmd.h +++ b/lcmd.h @@ -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 diff --git a/linit.c b/linit.c index 209c9a65..0240c4b0 100644 --- a/linit.c +++ b/linit.c @@ -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 */ } diff --git a/lualib.h b/lualib.h index 068f60ab..7f7bbcb7 100644 --- a/lualib.h +++ b/lualib.h @@ -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); diff --git a/testes/lush/lushlib.lua b/testes/lush/lushlib.lua new file mode 100644 index 00000000..49c56577 --- /dev/null +++ b/testes/lush/lushlib.lua @@ -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"