130 lines
4.8 KiB
Markdown
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
|