User commands run in forked children like external commands, so they support piping, redirection, and capture seamlessly.
70 lines
3.7 KiB
Markdown
70 lines
3.7 KiB
Markdown
# 30 — User-defined commands should behave like real shell commands
|
|
|
|
**Status:** open
|
|
|
|
**Related:** #26 (user builtins), #06 (piping), #15 (builtins)
|
|
|
|
## Problem
|
|
|
|
User-defined builtins registered via `lush.builtins.mycmd = function(name, arg, ...) end` are called directly as Lua functions in the parent process. This means:
|
|
|
|
1. **No stdout/stderr capture** — `print()` inside the function writes directly to the terminal, not into the `{code, stdout, stderr}` result table. Backtick invocation returns `nil` instead of a result table.
|
|
2. **No pipe support** — piping to/from a user builtin fails because `exec_pipeline()` forks children that call `execvp()`, which can't find the function as an external command.
|
|
3. **No redirection support** — for the same reason, `>`, `>>`, `2>` etc. won't work.
|
|
|
|
Current behavior:
|
|
|
|
```
|
|
$ pat hello -- prints "hello", returns nil (no result table)
|
|
$ `pat hello` -- prints "hello", returns nil (no .stdout)
|
|
$ !pat hello | nvim -- "pat: No such file or directory"
|
|
```
|
|
|
|
In bash, user-defined functions work exactly like external commands at the call site — they can be piped, redirected, and captured. Lush should match this.
|
|
|
|
## Proposal
|
|
|
|
### Restructure the lush table
|
|
|
|
Separate built-in commands from user-defined commands:
|
|
|
|
- **`lush.builtins`** — reserved for C builtins (`cd`, `exec`, `umask`) that must run in-process because they modify parent process state. Read-only / not user-extensible.
|
|
- **`lush.commands`** — user-defined commands. These are Lua functions that behave like shell commands: they run in a forked subprocess, their stdout/stderr are captured, and they work with pipes and redirection.
|
|
|
|
Dispatch order: alias expansion → `lush.commands` → `lush.builtins` → `$PATH` lookup.
|
|
|
|
### Fork user commands into a subprocess
|
|
|
|
When dispatching a `lush.commands` entry:
|
|
|
|
1. Set up stdout/stderr capture pipes (same as external command execution)
|
|
2. `fork()`
|
|
3. **Child**: wire stdout/stderr to the pipe write ends, call the Lua function, `_exit()` with the return code
|
|
4. **Parent**: read captured output, `waitpid()`, build and return the `{code, stdout, stderr}` result table
|
|
|
|
This makes user commands compatible with `exec_pipeline()` — each pipeline stage already forks a child, so the child just needs to check `lush.commands` before falling through to `execvp()`.
|
|
|
|
**Important caveat**: because user commands run in a forked child, they **cannot** modify parent Lua state. Setting a global variable inside a `lush.commands` function will not be visible after the command returns. This matches how bash functions behave when used in a pipeline (bash also forks subshells for pipeline stages). This trade-off should be clearly documented.
|
|
|
|
### Return protocol
|
|
|
|
User commands should follow the same return convention as C builtins:
|
|
|
|
- Return a table `{code=int, stdout=string, stderr=string}`, OR
|
|
- Return an integer exit code (0 = success), OR
|
|
- Return nothing (implies exit code 0)
|
|
|
|
The forked child captures whatever the function `print()`s as stdout, and uses the return value for the exit code.
|
|
|
|
## Files
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `lcmd.c` | Split `try_builtin()` into `try_builtin()` (in-process, `lush.builtins` only) and `try_user_command()` (fork+capture, `lush.commands`); add `lush.commands` check in pipeline child processes |
|
|
| `lbuiltin.c` | Register C builtins under `lush.builtins`; create empty `lush.commands` table for user use |
|
|
|
|
## Open questions
|
|
|
|
- Should user commands receive stdin naturally (child inherits the pipe fd, `io.read()` works) or as a string argument? Leaning toward natural stdin inheritance — matches bash and the child's stdin is already wired.
|
|
- Naming: `lush.commands` vs `lush.functions` vs `lush.cmds`? `commands` is clearest.
|