Files
lush/issues/28-lush-aliases.md
2026-03-20 22:58:35 +00:00

130 lines
4.8 KiB
Markdown

# 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