- #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
4.0 KiB
27 — Subcommand syntax in commands
Status: open
Problem
Running a command inside another command is unnecessarily verbose:
`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:
`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.
`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:
`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:
$VARin 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 ${}):
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 —
$VARexpansion in commands (also touches$handling inread_command_body())