feat: support interpolating a CommandBuilder into a template literal expression - #34
Merged
Merged
Conversation
- propagate an already-aborted signal to interpolated commands (linkChild only forwards future kills) - link input redirect interpolated commands to the evaluating command's signal and cancel their stream on dispose so they don't leak - fix CommandBuilder.then leaving awaiters hanging on a synchronous throw (ex. no command set) - unwrap the non-thenable proxy handed to beforeCommand callbacks when interpolated - avoid a per-invocation listener leak when the interpolated builder has its own signal - make an empty expanded command a no-op instead of crashing (pre-existing) - make CommandBuilderStateCommand.commandRefs optional for downstream consumers constructing the type - document word splitting and redirect-position semantics in the readme
Sentinel forgery: the parser echoes the command text back in its errors,
which disclosed the per-process nonce and made it possible to promote an
interpolated string to a command ref. Reject NUL in interpolated values and
in `command()` text (it's never valid in a command anyway), and resolve
sentinels in parse errors so they read as `$(...)` instead of raw NULs.
Input redirect parity: `< ${builder}` took a different route than argument
position and diverged from it. It now resolves async `beforeCommand` hooks
instead of hard-failing on them, routes the interpolated command's stderr to
the evaluating command's, reports failures through the shell (so `.noThrow()`
applies) with the interpolated command's exit code rather than the reading
command's, and only reports a status when the reading command consumed the
output to the end.
Process leaks: dispose the redirect pipe when the command throws rather than
completes, so an interpolated command isn't left running, and kill an
executable whose signal was already aborted when it started, since the
listener only receives future kills.
Also don't drop a `PipedBuffer` error or close that arrives before a listener
is attached (an early-failing command's stream never ended), treat a trailing
redirect operator within quotes as literal text rather than a redirect, and
route `inheritPiped` stderr like `inherit`.
… can't be streamed `child.stdout()` throws from `#assertBufferStreamable` before it reaches its own `this.catch()`, so a builder that can't expose a piped stdout (ex. one using `captureCombined()`) left the already-spawned `CommandChild` rejection unobserved. That surfaces as an unhandled rejection, which terminates the process by default in both Deno and Node.
`detectInputOrOutputRedirect` rescanned the whole accumulated command text once per interpolated expression, making building a command quadratic in its length. A template with a few megabyte-sized expressions took hundreds of milliseconds. The scan is now a stateful scanner that only walks the text appended since the previous expression, and the per-character `/\s/` test has an ascii fast path.
…equence The engine leaves a template's cooked segment undefined when its escape sequences aren't valid JavaScript, which the recently added NUL check then tripped over with `Cannot read properties of undefined`.
The empty command args case is currently only exercised through command builder interpolation, but the same path is reached by a command substitution or a variable expanding to nothing, which used to crash.
The catch-up paths that propagate a signal that was already aborted before their listener was attached defaulted to SIGTERM, so an abort by another signal (ex. SIGINT) lost its specific exit code. Retain the aborting signal on the state and replay it instead.
…ommand An interpolated command in an input redirect executes as `<&<fd>` to wire its stream, which is what `printCommand` and the tail header showed. Rewrite it back to `< $(...)` for display, matching how an interpolated command in an argument renders.
The only caller (`Context.getFdReader`) always supplies them, so the optional parameter left the reader factories guarding a case that couldn't occur.
…subshell isolation When a simple command's command word expands to nothing (ex. `FOO=1 $(true)`), its variable assignments now affect the current execution environment as a bare `FOO=1` assignment would, rather than being dropped. Making that correct surfaced that subshells didn't isolate their body's changes: `executeSubshell` ran the body in the parent context, so assignments, `cd`, and `set`/`shopt` inside `( ... )` leaked out. The body now runs in a clone, matching POSIX subshell semantics, and `Context.clone` copies the shell options so an option change in the clone no longer mutates the parent by reference.
Resolving the ephemeral publish-on-tag tool from the project directory added it (and `@std/semver`) to deno.lock, dirtying the tree so the `deno publish` it invokes aborted.
Adding the tool to the import map pins it in deno.lock, so resolving it at publish time no longer mutates the lockfile and dirties the tree. Reverts the --no-lock workaround.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements dsherret/dax#239.
Interpolating a
CommandBuilderinto a$template literal now substitutes its captured stdout, similar to$(...)command substitution:How it works
commandRefsmap on the command state and a sentinel token (\0<nonce>:<id>\0) is emitted into the command text. The nonce is per-process random so escaped user text can't forge one, and NUL passes through the parser as literal text in bare, single-quoted, and double-quoted contexts.replaceCommandRefSentinelswalks the AST and splits sentinel-bearing text parts into a newcommandRefword part (including inside quoted parts, braces, subshells, redirect words, and env var values). No changes to the Rust parser.commandRef, an invoker runs the builder with stdout piped. Evaluation is lazy and sequential, matching how words evaluate today —exit 1 && echo ${cmd}never runscmd.Semantics
$(...)"inherit"stderr routes to the evaluating command's stderrfailed evaluating interpolated command `exit 5`. Exited with code: 5— add.noThrow()to the interpolated command to tolerate failureKillControllerso kills only propagate outer → inner)printCommand/ tail headers display the sentinel as$(inner command text)Also fixes a pre-existing gap where a
ShellEvaluateError(e.g. failglob) thrown while evaluating a redirect word or a standaloneVAR=assignment escaped the executor instead of becoming a command error, and givesShellEvaluateErroran exit code so interpolated command failures propagate theirs.