From 8a61a47543a43c2415f629e49e5c5d6c486e2da5 Mon Sep 17 00:00:00 2001 From: Cormac Shannon <> Date: Fri, 20 Mar 2026 00:46:12 +0000 Subject: [PATCH] Add(plan): aliases --- issues/28-lush-aliases.md | 129 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 issues/28-lush-aliases.md diff --git a/issues/28-lush-aliases.md b/issues/28-lush-aliases.md new file mode 100644 index 00000000..52e48da3 --- /dev/null +++ b/issues/28-lush-aliases.md @@ -0,0 +1,129 @@ +# 28 — Shell aliases via `lush.aliases` + +**Status:** open + +## Problem + +Issue #26 exposed `lush.builtins` for user-defined commands, but wrapping external commands (like making `ls` always pass `-G`) is awkward. The builtin function receives parsed argv, so to delegate to the real command it must call `lush.command()` — which hits `try_builtin()` again, causing infinite recursion. The workaround (nil out self, call, restore) is fragile and non-obvious. + +Aliases are a separate concept from builtins: they rewrite the command string before dispatch, rather than replacing dispatch entirely. + +## Proposal: `lush.aliases` table + +Add a `lush.aliases` table checked in `try_builtin()` *before* the builtins lookup. When `argv[0]` matches an alias key, the value (a string) replaces `argv[0]` and the command is re-parsed and re-dispatched. No recursion risk because the alias is consumed on substitution — it is not checked again. + +### User interface + +```lua +-- simple alias: gs → git status +lush.aliases.gs = "git status" + +-- add default flags: ls always gets -G +lush.aliases.ls = "ls -G" + +-- multi-word: ll → ls -lah +lush.aliases.ll = "ls -lah" +``` + +Then: +``` +!gs → runs: git status +!ls foo/ → runs: ls -G foo/ +!ll → runs: ls -lah +`gs` → captures: git status +``` + +### Semantics + +- Alias values are strings. The value replaces `argv[0]` textually, and remaining args are appended. +- Alias expansion happens once (no recursive/chained alias expansion) to keep things predictable. +- Builtins take priority over external commands but aliases take priority over builtins. This lets users alias a builtin name to something else if desired. +- `lush.aliases` is a plain Lua table — users can inspect, modify, and iterate it. + +## Implementation + +### 1. `luaopen_lush()` in `lcmd.c` — create the aliases subtable + +After creating the main lush table, add an empty `aliases` subtable: + +```c +lua_createtable(L, 0, 4); +lua_setfield(L, -2, "aliases"); +``` + +### 2. `try_alias()` in `lcmd.c` — new function + +Add a function that checks `lush.aliases[argv[0]]`. If found: +- Get the alias string value +- Prepend it to the remaining argv (replacing argv[0]) +- Re-parse with `parse_argv()` and return the new command string + +```c +/* +** Check if argv[0] has an alias. If so, build a new command string +** by replacing argv[0] with the alias value and appending remaining args. +** Returns a malloc'd string (caller frees), or NULL if no alias. +*/ +static const char *try_alias(lua_State *L, ParsedArgs *pa); +``` + +### 3. Call site in `lushCmd_command()` and `lushCmd_interactive()` + +After `parse_argv()` and the empty-command check, before `try_builtin()`: + +```c +/* check for alias */ +const char *expanded = try_alias(L, &pa); +if (expanded != NULL) { + free_argv(&pa); + /* restart with the expanded command string */ + lua_pushstring(L, expanded); + lua_replace(L, 1); + free((void *)expanded); + cmd = lua_tostring(L, 1); + /* re-parse and continue (goto or restructure) */ +} +``` + +The simplest approach: push the expanded string back onto the stack and recurse into `lushCmd_command()`/`lushCmd_interactive()`. Since aliases don't expand recursively (the expanded command won't be alias-checked again because the recursive call skips alias lookup — or we set a flag), this is a single level of recursion. + +Alternatively, use a `goto restart` pattern to avoid recursion. + +### 4. Recursion prevention + +To prevent `lush.aliases.foo = "foo -x"` from looping, alias expansion should happen at most once per command invocation. Two options: + +- **Flag parameter**: add an `int noalias` parameter to the internal dispatch path. +- **Name check**: after expansion, don't re-check aliases (simpler — just restructure the function so alias check is skipped on the re-parse path). + +The `goto restart` pattern with a flag is cleanest: + +```c +int aliased = 0; +restart: + /* ... parse_argv ... */ + if (!aliased) { + const char *expanded = try_alias(L, &pa); + if (expanded) { + aliased = 1; + /* replace cmd, free old pa, goto restart */ + } + } + /* ... try_builtin, execvp ... */ +``` + +## Files + +| File | Changes | +|------|---------| +| `lcmd.c` | Add `try_alias()`, create `aliases` subtable in `luaopen_lush()`, add alias check in `lushCmd_command()` and `lushCmd_interactive()` | +| `testes/lush/lushlib.lua` | Add alias tests | + +## Tests + +1. Simple alias: `lush.aliases.gs = "git status"` then `` `gs` `` returns git status output +2. Alias with extra args: `lush.aliases.ls = "ls -1"` then `` `ls /` `` passes `-1 /` to ls +3. Alias does not recurse: `lush.aliases.echo = "echo hello"` then `` `echo world` `` produces `hello world` (not infinite loop) +4. Builtin via alias: `lush.aliases.home = "cd"` then `` `home` `` changes to $HOME +5. Alias removed: after `lush.aliases.gs = nil`, `gs` falls through to external command lookup +6. Interactive mode: `!gs` works the same as backtick