From bbc0f24009dd0db45c5b92d5f42236edec843714 Mon Sep 17 00:00:00 2001 From: Cormac Shannon <> Date: Tue, 3 Mar 2026 22:59:51 +0000 Subject: [PATCH] Implement startup configuration file support (issue #08) Load ~/.config/lush/config and config.d/*.lua at REPL startup, respecting XDG_CONFIG_HOME. Suppressed by -E flag. --- issues/08-configuration.md | 2 +- lua.c | 82 +++++++++++++++++++- testes/lush/config.lua | 151 +++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 testes/lush/config.lua diff --git a/issues/08-configuration.md b/issues/08-configuration.md index 2e2a9ec4..c9a33564 100644 --- a/issues/08-configuration.md +++ b/issues/08-configuration.md @@ -1,6 +1,6 @@ # 08 — Configuration support -**Status:** open +**Status:** done Users should be able to programmatically configure the shell at startup. diff --git a/lua.c b/lua.c index 9576f271..4c5dc246 100644 --- a/lua.c +++ b/lua.c @@ -13,6 +13,8 @@ #include #include +#include +#include #include #include "lua.h" @@ -722,6 +724,79 @@ static void doREPL (lua_State *L) { #endif +/* +** {================================================================== +** Configuration file support +** =================================================================== +*/ + +static const char *get_config_dir (char *buf, size_t bufsize) { + const char *xdg = getenv("XDG_CONFIG_HOME"); + const char *home = getenv("HOME"); + if (xdg && xdg[0] != '\0') + snprintf(buf, bufsize, "%s/lush", xdg); + else if (home && home[0] != '\0') + snprintf(buf, bufsize, "%s/.config/lush", home); + else + return NULL; + return buf; +} + + +static int cmpstr (const void *a, const void *b) { + return strcmp(*(const char **)a, *(const char **)b); +} + + +static void run_config_dir (lua_State *L, const char *dirpath) { + char *names[256]; + int count = 0; + int i; + struct dirent *entry; + DIR *d = opendir(dirpath); + if (d == NULL) return; /* directory doesn't exist — skip silently */ + + /* collect .lua filenames */ + while ((entry = readdir(d)) != NULL && count < 256) { + size_t len = strlen(entry->d_name); + if (len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0) + names[count++] = strdup(entry->d_name); + } + closedir(d); + + /* sort lexicographically */ + qsort(names, (size_t)count, sizeof(char *), cmpstr); + + /* execute each file */ + for (i = 0; i < count; i++) { + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/%s", dirpath, names[i]); + dofile(L, path); /* errors reported but not fatal */ + free(names[i]); + } +} + + +static void handle_lushconfig (lua_State *L) { + char configdir[PATH_MAX]; + char path[PATH_MAX]; + + if (get_config_dir(configdir, sizeof(configdir)) == NULL) + return; /* no HOME or XDG_CONFIG_HOME — skip silently */ + + /* 1. Run ~/.config/lush/config if it exists */ + snprintf(path, sizeof(path), "%s/config", configdir); + if (access(path, R_OK) == 0) + dofile(L, path); /* errors are reported but not fatal */ + + /* 2. Run config.d/ lua files in sorted order */ + snprintf(path, sizeof(path), "%s/config.d", configdir); + run_config_dir(L, path); +} + +/* }================================================================== */ + + /* ** Main body of stand-alone interpreter (to be called in protected mode). ** Reads the options and handles them all. @@ -757,11 +832,16 @@ static int pmain (lua_State *L) { if (handle_script(L, argv + script) != LUA_OK) return 0; /* interrupt in case of error */ } - if (args & has_i) /* -i option? */ + if (args & has_i) { /* -i option? */ + if (!(args & has_E)) + handle_lushconfig(L); doREPL(L); /* do read-eval-print loop */ + } else if (script < 1 && !(args & (has_e | has_v))) { /* no active option? */ if (lua_stdin_is_tty()) { /* running in interactive mode? */ print_version(); + if (!(args & has_E)) + handle_lushconfig(L); doREPL(L); /* do read-eval-print loop */ } else dofile(L, NULL); /* executes stdin as a file */ diff --git a/testes/lush/config.lua b/testes/lush/config.lua new file mode 100644 index 00000000..4a36ddec --- /dev/null +++ b/testes/lush/config.lua @@ -0,0 +1,151 @@ +-- testes/lush/config.lua +-- Tests for configuration file support (issue #08). + +print "testing configuration" + +-- helper: get a unique temp directory +local tmpbase = os.tmpname() +os.remove(tmpbase) -- tmpname creates the file on some systems + +local function mkdir(path) + os.execute('mkdir -p "' .. path .. '"') +end + +local function writefile(path, content) + local f = assert(io.open(path, "w")) + f:write(content) + f:close() +end + +local function rmrf(path) + os.execute('rm -rf "' .. path .. '"') +end + +-- helper: run ./lua -i with XDG_CONFIG_HOME set, feed it a command via stdin +local function run_with_config(xdg_dir, input) + local cmd = string.format( + 'printf "%%s\\n" "%s" | XDG_CONFIG_HOME="%s" ./lua -i 2>&1', + input, xdg_dir) + local f = io.popen(cmd) + local output = f:read("*a") + f:close() + return output +end + +-- helper: run ./lua -E -i with XDG_CONFIG_HOME set +local function run_with_config_E(xdg_dir, input) + local cmd = string.format( + 'printf "%%s\\n" "%s" | XDG_CONFIG_HOME="%s" ./lua -E -i 2>&1', + input, xdg_dir) + local f = io.popen(cmd) + local output = f:read("*a") + f:close() + return output +end + + +-- ===== TEST 1: Main config file is loaded ===== +do + local tmpdir = tmpbase .. "_t1" + mkdir(tmpdir .. "/lush") + writefile(tmpdir .. "/lush/config", '_LUSH_TEST_MARKER = 42\n') + + local out = run_with_config(tmpdir, "print(_LUSH_TEST_MARKER)") + assert(out:find("42"), "config file should set global: " .. out) + + rmrf(tmpdir) +end + + +-- ===== TEST 2: config.d/ files are loaded in sorted order ===== +do + local tmpdir = tmpbase .. "_t2" + mkdir(tmpdir .. "/lush/config.d") + writefile(tmpdir .. "/lush/config.d/02-second.lua", + '_LUSH_ORDER = (_LUSH_ORDER or "") .. "B"\n') + writefile(tmpdir .. "/lush/config.d/01-first.lua", + '_LUSH_ORDER = (_LUSH_ORDER or "") .. "A"\n') + writefile(tmpdir .. "/lush/config.d/03-third.lua", + '_LUSH_ORDER = (_LUSH_ORDER or "") .. "C"\n') + + local out = run_with_config(tmpdir, "print(_LUSH_ORDER)") + assert(out:find("ABC"), "config.d files should load in order: " .. out) + + rmrf(tmpdir) +end + + +-- ===== TEST 3: Both config and config.d/ are loaded ===== +do + local tmpdir = tmpbase .. "_t3" + mkdir(tmpdir .. "/lush/config.d") + writefile(tmpdir .. "/lush/config", '_LUSH_SEQ = "M"\n') + writefile(tmpdir .. "/lush/config.d/01.lua", + '_LUSH_SEQ = _LUSH_SEQ .. "D"\n') + + local out = run_with_config(tmpdir, "print(_LUSH_SEQ)") + assert(out:find("MD"), "main config runs before config.d: " .. out) + + rmrf(tmpdir) +end + + +-- ===== TEST 4: -E suppresses config loading ===== +do + local tmpdir = tmpbase .. "_t4" + mkdir(tmpdir .. "/lush") + writefile(tmpdir .. "/lush/config", '_LUSH_SUPPRESSED = true\n') + + local out = run_with_config_E(tmpdir, "print(_LUSH_SUPPRESSED)") + assert(out:find("nil"), "-E should suppress config: " .. out) + + rmrf(tmpdir) +end + + +-- ===== TEST 5: Missing config dir doesn't error ===== +do + local tmpdir = tmpbase .. "_t5" + rmrf(tmpdir) -- make sure it doesn't exist + + -- should start normally without errors + local out = run_with_config(tmpdir, "print(42)") + assert(out:find("42"), "missing config dir should not error: " .. out) + + rmrf(tmpdir) +end + + +-- ===== TEST 6: Only .lua files in config.d/ are loaded ===== +do + local tmpdir = tmpbase .. "_t6" + mkdir(tmpdir .. "/lush/config.d") + writefile(tmpdir .. "/lush/config.d/good.lua", + '_LUSH_LOADED = "yes"\n') + writefile(tmpdir .. "/lush/config.d/skip.txt", + '_LUSH_SKIPPED = "bad"\n') + writefile(tmpdir .. "/lush/config.d/README", + '_LUSH_README = "bad"\n') + + local out = run_with_config(tmpdir, 'print(_LUSH_LOADED, _LUSH_SKIPPED)') + assert(out:find("yes"), "should load .lua files: " .. out) + assert(out:find("nil"), "should skip non-.lua files: " .. out) + + rmrf(tmpdir) +end + + +-- ===== TEST 7: Config error is non-fatal ===== +do + local tmpdir = tmpbase .. "_t7" + mkdir(tmpdir .. "/lush") + writefile(tmpdir .. "/lush/config", 'error("config boom")\n') + + local out = run_with_config(tmpdir, "print(99)") + assert(out:find("99"), "config error should be non-fatal: " .. out) + + rmrf(tmpdir) +end + + +print "OK"