- #25: $VAR expansion in commands - #26: Shell abbreviations and user-extensible builtins - #27: $(cmd) subcommand syntax - #22: Updated with implementation details (in progress) - #7: Standardize status label
This commit is contained in:
129
issues/27-subcommand-syntax.md
Normal file
129
issues/27-subcommand-syntax.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 27 — Subcommand syntax in commands
|
||||
|
||||
**Status:** open
|
||||
|
||||
## 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()`)
|
||||
Reference in New Issue
Block a user