Add shell aliases via lush.aliases table (issue #28)

Aliases rewrite the command string before pipeline splitting, so alias
values may contain pipes (e.g. lush.aliases.foo = "echo hi |"). Expansion
happens once per dispatch to prevent recursion.
This commit is contained in:
Cormac Shannon
2026-03-20 22:58:01 +00:00
parent e115a061bc
commit 68a5273094
2 changed files with 151 additions and 0 deletions

70
lcmd.c
View File

@@ -897,6 +897,67 @@ static int try_builtin (lua_State *L, ParsedArgs *pa) {
}
/* ===== alias expansion ===== */
/*
** Extract the first whitespace-delimited word from cmd, look it up in
** lush.aliases. If found, replace the command string on the Lua stack
** (position 1) with alias_value + remaining_text, and update *cmd.
** Expansion happens on the raw string, before pipeline splitting,
** so alias values may contain pipes.
** Returns 1 if expanded, 0 if no alias.
*/
static int expand_alias (lua_State *L, const char **cmd) {
const char *s = *cmd;
const char *rest;
size_t wordlen;
const char *alias;
size_t alias_len, rest_len, total;
char *expanded;
/* skip leading whitespace */
while (*s == ' ' || *s == '\t') s++;
if (*s == '\0') return 0;
/* find end of first word */
rest = s;
while (*rest && *rest != ' ' && *rest != '\t') rest++;
wordlen = (size_t)(rest - s);
/* look up in lush.aliases */
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_LUSH);
lua_getfield(L, -1, "aliases");
lua_pushlstring(L, s, wordlen);
lua_gettable(L, -2);
if (!lua_isstring(L, -1)) {
lua_pop(L, 3);
return 0;
}
alias = lua_tolstring(L, -1, &alias_len);
rest_len = strlen(rest); /* includes leading space if any */
total = alias_len + rest_len;
expanded = (char *)malloc(total + 1);
if (expanded == NULL) {
lua_pop(L, 3);
return 0;
}
memcpy(expanded, alias, alias_len);
memcpy(expanded + alias_len, rest, rest_len);
expanded[total] = '\0';
lua_pop(L, 3);
/* replace command on Lua stack */
lua_pushstring(L, expanded);
lua_replace(L, 1);
free(expanded);
*cmd = lua_tostring(L, 1);
return 1;
}
/* ===== lushCmd_command ===== */
int lushCmd_command (lua_State *L) {
@@ -910,6 +971,9 @@ int lushCmd_command (lua_State *L) {
DynBuf buf_out, buf_err;
struct sigaction sa_old, sa_new;
/* expand alias before pipeline splitting */
expand_alias(L, &cmd);
/* try to split into pipeline stages */
if (split_pipeline(cmd, stages, &nstages) != 0)
return luaL_error(L, "invalid pipeline syntax in command");
@@ -1076,6 +1140,9 @@ int lushCmd_interactive (lua_State *L) {
struct sigaction sa_old_pipe, sa_old_int, sa_old_quit, sa_new;
DynBuf buf_out, buf_err;
/* expand alias before pipeline splitting */
expand_alias(L, &cmd);
/* try to split into pipeline stages */
if (split_pipeline(cmd, stages, &nstages) != 0)
return luaL_error(L, "invalid pipeline syntax in command");
@@ -1217,6 +1284,9 @@ static const luaL_Reg lushlib[] = {
LUAMOD_API int luaopen_lush (lua_State *L) {
luaL_newlib(L, lushlib);
/* create aliases subtable */
lua_createtable(L, 0, 4);
lua_setfield(L, -2, "aliases");
/* intern function name strings for OP_LUSH VM access */
lushname[LUSH_OP_COMMAND] = luaS_new(L, "command");
lushname[LUSH_OP_INTERACTIVE] = luaS_new(L, "interactive");

View File

@@ -93,6 +93,87 @@ do
end
-- === lush.aliases exists and is a table ===
do
assert(type(lush.aliases) == "table", "lush.aliases missing")
-- iterable (pairs works on it)
local count = 0
for k, v in pairs(lush.aliases) do count = count + 1 end
assert(count == 0, "aliases should start empty")
end
-- === simple alias ===
do
lush.aliases.myecho = "echo hello"
local r = `myecho`
assert(r.code == 0, "alias failed")
assert(r.stdout == "hello\n", "expected 'hello\\n', got: " .. r.stdout)
lush.aliases.myecho = nil
end
-- === alias with extra args ===
do
lush.aliases.myecho = "echo hello"
local r = `myecho world`
assert(r.code == 0, "alias with args failed")
assert(r.stdout == "hello world\n",
"expected 'hello world\\n', got: " .. r.stdout)
lush.aliases.myecho = nil
end
-- === alias does not recurse ===
do
lush.aliases.echo = "echo aliased"
local r = `echo extra`
assert(r.code == 0, "aliased echo failed")
assert(r.stdout == "aliased extra\n",
"expected 'aliased extra\\n', got: " .. r.stdout)
lush.aliases.echo = nil
end
-- === alias removed ===
do
lush.aliases.myecho = "echo hello"
lush.aliases.myecho = nil
-- myecho should now be a normal (nonexistent) command
local r = `myecho`
assert(r.code ~= 0, "removed alias should not work")
end
-- === alias with pipe in value ===
do
lush.aliases.grephello = "echo hello | grep"
local r = `grephello hello`
assert(r.code == 0, "alias with pipe failed")
assert(r.stdout == "hello\n",
"expected 'hello\\n', got: " .. r.stdout)
lush.aliases.grephello = nil
end
-- === alias works with lush.command ===
do
lush.aliases.myecho = "echo via-command"
local r = lush.command("myecho")
assert(r.code == 0)
assert(r.stdout == "via-command\n",
"expected 'via-command\\n', got: " .. r.stdout)
lush.aliases.myecho = nil
end
-- === OP_LUSH still works (backtick, $VAR, !cmd syntax) ===
do