Files
lush/issues/15-builtins.md
Cormac Shannon 4cc352cbec Implement shell builtins: cd, exec, umask (issue #15)
Add builtin dispatch in lcmd.c that checks __builtins table before
fork(), so cd/exec/umask operate on the shell process itself.
2026-03-04 19:20:42 +00:00

2.5 KiB

15 — Shell builtins

Status: done

Some commands must run inside the shell process itself to have any effect. An external cd binary exists on most systems but it spawns a subprocess, changes directory there, and exits — leaving lush's own working directory unchanged.

Lush only needs builtins for operations that cannot be implemented in pure Lua or via external binaries — things that require modifying the shell process state via syscalls with no Lua API.

Assessment

Every builtin from man 1 builtin was evaluated against two questions:

  1. Can it be done in Lua? (e.g. export$VAR = "val", sourcedofile(), exitos.exit())
  2. Does the external binary work? (e.g. /bin/pwd, /usr/bin/which, /usr/bin/kill)

If either answer is yes, it's not needed as a builtin. What remains:

Builtin Why it must be a builtin
cd chdir(2) must happen in the lush process. No Lua API.
exec execvp(2) replaces the current process. os.execute() spawns a subprocess. No Lua equivalent.
umask umask(2) is per-process. The external /usr/bin/umask is a shell script that calls the shell's own builtin. No Lua API.

Builtins

cd [dir]

Change working directory via chdir(2).

  • No argument: cd $HOME
  • cd -: change to $OLDPWD
  • Updates $PWD and $OLDPWD environment variables
  • Errors if directory doesn't exist or isn't accessible

exec command [args...]

Replace the shell process with the given command via execvp(2). Does not return on success. On failure, prints an error and the shell continues.

umask [mode]

Get or set the file creation mask via umask(2).

  • No argument: print current umask (octal)
  • With argument: set umask to the given octal value

Design considerations

  • Dispatch order: When a command is entered (backtick or ! prefix), check builtins before searching $PATH. A builtin named cd must intercept the command before it reaches execvp.
  • Implementation: Expose the syscalls to Lua (e.g. os.chdir(), os.exec(), os.umask()), then implement the builtins as Lua functions registered at startup. This keeps the C side minimal and lets users compose with the primitives directly.
  • Interaction with backticks: `cd /tmp` and !cd /tmp should both trigger the builtin, not spawn an external process.

Open questions

  • Should builtins be overridable by the user?
  • Should there be a builtin command to bypass user overrides?