4.8 KiB
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
-- 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.aliasesis 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:
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
/*
** 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():
/* 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 noaliasparameter 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:
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
- Simple alias:
lush.aliases.gs = "git status"then`gs`returns git status output - Alias with extra args:
lush.aliases.ls = "ls -1"then`ls /`passes-1 /to ls - Alias does not recurse:
lush.aliases.echo = "echo hello"then`echo world`produceshello world(not infinite loop) - Builtin via alias:
lush.aliases.home = "cd"then`home`changes to $HOME - Alias removed: after
lush.aliases.gs = nil,gsfalls through to external command lookup - Interactive mode:
!gsworks the same as backtick