Files
lush/issues/27-subcommand-syntax.md
Cormac Shannon 1766e40a68 Implement $(cmd) subcommand syntax in commands (issue #27)
Add $() for inline subcommand substitution that runs a command and
inserts its stdout (trailing newlines stripped) into the outer command
string. Supports nesting, and mixing with ${expr} and $NAME.
2026-03-18 09:29:51 +00:00

130 lines
4.0 KiB
Markdown

# 27 — Subcommand syntax in commands
**Status:** done
## Problem
Running a command inside another command is unnecessarily verbose:
```lua
`ls ${`pwd`.stdout}`
```
The inner backtick returns a result table (`{code, stdout, stderr}`), so `.stdout` is required to extract the string. This is clunky compared to other shells.
## Proposed syntax: `$(cmd)`
Use `$()` for inline subcommands, consistent with the existing `$` interpolation family:
```lua
`ls $(pwd)`
!echo $(whoami)
`tar -czf $(date +%F).tar.gz src/`
```
This is consistent with existing lush syntax:
| Syntax | Context | Meaning |
|--------|---------|---------|
| `$VAR` | Lua code | `getenv("VAR")` |
| `${expr}` | command body | interpolate Lua expression |
| **`$(cmd)`** | command body | **run subcommand, insert stdout** |
### Comparison with other shells
| Shell | Syntax |
|-------|--------|
| bash | `$(cmd)` |
| fish | `(cmd)` |
| lush current | `` `ls ${`pwd`.stdout}` `` |
| **lush proposed** | `` `ls $(pwd)` `` |
## Behavior
`$(cmd)` runs a shell command (same as backtick) and inserts its stdout into the outer command, with trailing newline stripped.
```lua
`ls $(pwd)` -- list files in pwd's output
!echo $(whoami)@$(hostname) -- multiple subcommands
`echo $(ls $(pwd))` -- nested: inner runs first
```
`$(cmd)` is **not** for Lua expressions — that's what `${expr}` is for. `$()` runs a shell command; `${}` evaluates Lua.
### Nesting
Nested subcommands are supported:
```lua
`echo $(ls $(pwd))`
```
This works naturally because `$(cmd)` enters command parsing, which can itself contain `$()`.
## Syntax clash analysis
Currently in `read_command_body()` (`llex.c:508`), `$` followed by anything other than `{` saves a literal `$`. Adding `(` as a second trigger alongside `{` is a minimal change. No conflicts:
- `$VAR` in command mode is currently a literal `$` + `VAR` (see issue #25) — not affected
- `${expr}` continues to work unchanged
- Literal `$(` in commands is not meaningful today (falls through as literal text)
## Implementation sketch
### Lexer (`llex.c`)
In `read_command_body()`, extend the `$` case to also trigger on `(`. This starts a new command body parse (not a Lua expression like `${}`):
```c
case '$': {
next(ls);
if (ls->current == '{') {
next(ls); /* skip '{' */
/* existing ${expr} interpolation path */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->saved_cmd_mode = ls->cmd_mode;
return interactive ? TK_INTERACTIVE : TK_COMMAND;
}
else if (ls->current == '(') {
next(ls); /* skip '(' */
/* subcommand: start a new command parse, closed by ')' */
seminfo->ts = luaX_newstring(ls, luaZ_buffer(ls->buff),
luaZ_bufflen(ls->buff));
ls->saved_cmd_mode = ls->cmd_mode;
/* signal parser that this is a subcommand, not a Lua expr */
...
return interactive ? TK_INTERACTIVE : TK_COMMAND;
}
else {
save(ls, '$');
}
break;
}
```
The `$()` body is parsed as a command (like backtick), terminated by `)` instead of `` ` ``. The result is run via `lushCmd_command` and `.stdout` is extracted with trailing newline stripped.
### Parser (`lparser.c`)
The parser needs to distinguish `$()` from `${}`:
- `${expr}` → parse Lua expression, `tostring()` the result (existing behavior)
- `$(cmd)` → parse as a command (like backtick), run it, extract `.stdout`, strip trailing `\n`
### Lexer state (`llex.h`)
Track whether the current interpolation is a subcommand (`$(`) or expression (`${`) so the parser knows which path to take.
## Files affected
| File | Changes |
|------|---------|
| `llex.h` | Add field to `LexState` to distinguish `$()` from `${}` |
| `llex.c` | Extend `$` case in `read_command_body()`, handle `)` as command terminator |
| `lparser.c` | Add subcommand path: parse as command, extract `.stdout` |
## Related
- Issue #25 — `$VAR` expansion in commands (also touches `$` handling in `read_command_body()`)