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

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.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:

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 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:

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