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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 08 — Configuration support
|
# 08 — Configuration support
|
||||||
|
|
||||||
**Status:** open
|
**Status:** done
|
||||||
|
|
||||||
Users should be able to programmatically configure the shell at startup.
|
Users should be able to programmatically configure the shell at startup.
|
||||||
|
|
||||||
|
|||||||
82
lua.c
82
lua.c
@@ -13,6 +13,8 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
||||||
#include "lua.h"
|
#include "lua.h"
|
||||||
@@ -722,6 +724,79 @@ static void doREPL (lua_State *L) {
|
|||||||
#endif
|
#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).
|
** Main body of stand-alone interpreter (to be called in protected mode).
|
||||||
** Reads the options and handles them all.
|
** 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)
|
if (handle_script(L, argv + script) != LUA_OK)
|
||||||
return 0; /* interrupt in case of error */
|
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 */
|
doREPL(L); /* do read-eval-print loop */
|
||||||
|
}
|
||||||
else if (script < 1 && !(args & (has_e | has_v))) { /* no active option? */
|
else if (script < 1 && !(args & (has_e | has_v))) { /* no active option? */
|
||||||
if (lua_stdin_is_tty()) { /* running in interactive mode? */
|
if (lua_stdin_is_tty()) { /* running in interactive mode? */
|
||||||
print_version();
|
print_version();
|
||||||
|
if (!(args & has_E))
|
||||||
|
handle_lushconfig(L);
|
||||||
doREPL(L); /* do read-eval-print loop */
|
doREPL(L); /* do read-eval-print loop */
|
||||||
}
|
}
|
||||||
else dofile(L, NULL); /* executes stdin as a file */
|
else dofile(L, NULL); /* executes stdin as a file */
|
||||||
|
|||||||
151
testes/lush/config.lua
Normal file
151
testes/lush/config.lua
Normal file
@@ -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"
|
||||||
Reference in New Issue
Block a user