# 28 — Shell aliases via `lush.aliases` **Status:** done ## 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