Skip to content

Commit d3a732f

Browse files
authored
Stream merge redirects (#276)
Previously, our paired redirections only worked on redirections to *files*. We did not have the concept of redirection to *file descriptors* like bash. We now added tokens 1>&2 and 2>&1 for that semantics. This also cleaned up some discrepancies between the type checker and runtime with redirections on quotes.
1 parent 3cd3dd4 commit d3a732f

46 files changed

Lines changed: 812 additions & 93 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Stream merge redirects `2>&1` (stderr to stdout's destination) and `1>&2`
13+
(stdout to stderr's destination). Each is a single token with no internal
14+
spaces, and works on command lists, pipeline stages, and quotations.
15+
Unlike POSIX, they are not order-sensitive fd duplication: the merged
16+
stream follows the other stream's *final* destination, so `2>&1 *` captures
17+
both streams interleaved and `[[make] 2>&1 [grep err]] |;` sends stderr
18+
through the pipe, cross-platform.
19+
20+
### Changed
21+
22+
- Each of stdout/stderr now has exactly one destination: combining two
23+
destinations on the same stream (e.g. capture `*` plus file redirect `>`,
24+
`&>` plus `2>`, a second `>`, or a merge plus anything else on that stream)
25+
is now an error, caught both by the static type checker and at runtime.
26+
Previously the extra redirect was silently ignored (capture won over file
27+
redirects) or last-wins. `2>&1` combined with `<>` is also rejected.
28+
- `loop` quotations now honor stderr redirects, append mode, and merges, and
29+
inherit the enclosing quotation's redirected streams (previously a loop
30+
body wrote straight to the terminal even inside a redirected quotation).
31+
- Quotations accept only the redirects that don't change the stack: file
32+
redirects, stdin, and the merges. Captures (`*`, `*b`, `^`, `^b`) on a
33+
quotation now give a clear error at both layers instead of falling into
34+
the multiplication error, and the type checker now rejects `<>` on a
35+
quotation (the runtime always did). Capture individual command lists
36+
instead, e.g. `[[cmd1] [cmd2]] (* !) map`.
37+
38+
### Added
39+
1240
- CLI completions for `cargo`: subcommands (including installed third-party
1341
ones via `cargo --list`), per-subcommand options, and dynamic values for
1442
`--target`, `--features`, `-p`/`--package`, `--bin`/`--example`/`--test`/`--bench`

code/syntaxes/mshell.textmate.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"name": "constant.language.bool",
1313
"match": "\\b(true|false)\\b"
1414
},
15+
{
16+
"name": "keyword.operator.redirect.merge.mshell",
17+
"match": "2>&1|1>&2"
18+
},
1519
{
1620
"name": "string.quoted.single.mshell",
1721
"begin": "'",

doc/execution.inc.html

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,36 @@
142142
</code>
143143
</pre>
144144

145+
<h2 id="stream-merging">Merging one stream into the other</h2>
146+
147+
<p>
148+
Use <code>2>&amp;1</code> to send standard error to wherever standard output ends up,
149+
or <code>1>&amp;2</code> to send standard output to wherever standard error ends up.
150+
These are complete tokens: no spaces are allowed inside them.
151+
Unlike POSIX shells, they are <em>not</em> order-sensitive file descriptor duplication:
152+
a merge means "this stream goes to the other stream's <em>final</em> destination",
153+
so there is no <code>> file 2>&amp;1</code> vs <code>2>&amp;1 > file</code> ordering trap.
154+
Both streams share a single destination, preserving output order, and this works the same on every platform.
155+
</p>
156+
157+
<pre>
158+
<code><span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLITERAL">yourCommand</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellSTDERRTOSTDOUT">2&gt;&amp;1</span> <span class="mshellASTERISK">*</span> <span class="mshellBANG">!</span> <span class="mshellLINECOMMENT"># Captures stdout and stderr interleaved as one string.</span>
159+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLITERAL">make</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellSTDERRTOSTDOUT">2&gt;&amp;1</span> <span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLITERAL">grep</span> <span class="mshellLITERAL">-i</span> <span class="mshellLITERAL">error</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPIPE">|</span><span class="mshellEXECUTE">;</span> <span class="mshellLINECOMMENT"># stderr flows through the pipe.</span>
160+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLITERAL">yourCommand</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellSTDOUTTOSTDERR">1&gt;&amp;2</span> <span class="mshellEXECUTE">;</span> <span class="mshellLINECOMMENT"># stdout appears on stderr, e.g. visible even when the surrounding output is captured.</span>
161+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLITERAL">yourCommand</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPATH">`output.log`</span> <span class="mshellGREATERTHAN">&gt;</span> <span class="mshellSTDERRTOSTDOUT">2&gt;&amp;1</span> <span class="mshellBANG">!</span> <span class="mshellLINECOMMENT"># Both streams into output.log; equivalent to &amp;&gt;.</span>
162+
</code>
163+
</pre>
164+
165+
<h2 id="one-destination-per-stream">Each stream has exactly one destination</h2>
166+
167+
<p>
168+
A stream's destination can be set once: a file redirect, a capture, an in-place redirect, or a merge.
169+
Applying a second destination to the same stream is an error, caught both by the static type checker and at runtime.
170+
For example, <code>[cmd] * `f` ></code> (capture and file redirect on stdout), <code>[cmd] 2>&amp;1 ^</code> (merge and capture on stderr),
171+
and <code>[cmd] 2>&amp;1 1>&amp;2</code> (circular merge) are all rejected.
172+
<code>2>&amp;1</code> also cannot be combined with <code>&lt;&gt;</code>, since stderr text would be written back into the edited file.
173+
</p>
174+
145175
<table>
146176
<caption>Summary of external command operators</caption>
147177
<thead>
@@ -161,6 +191,8 @@
161191
<tr> <td><code>&amp;>></code></td> <td>Redirect both stdout and stderr to a file.</td> <td>Appends to the file.</td> </tr>
162192
<tr> <td><code>^</code></td> <td>Capture stderr to the stack.</td> <td>As a string.</td> </tr>
163193
<tr> <td><code>^b</code></td> <td>Capture stderr to the stack.</td> <td>As binary.</td> </tr>
194+
<tr> <td><code>2>&amp;1</code></td> <td>Merge stderr into stdout's destination.</td> <td>Single token, no spaces. Not order-sensitive: stderr follows stdout's final destination.</td> </tr>
195+
<tr> <td><code>1>&amp;2</code></td> <td>Merge stdout into stderr's destination.</td> <td>Single token, no spaces.</td> </tr>
164196
<tr> <td><code>&lt;</code></td> <td>Feed stdin from a value.</td> <td>String, path, or binary.</td> </tr>
165197
<tr> <td><code>&lt;&gt;</code></td> <td>In-place file modification.</td> <td>Reads file to stdin, writes stdout back on success.</td> </tr>
166198
<tr> <td><code>&amp;</code></td> <td>Mark the command list to run asynchronously.</td> <td>Marks the list; the trailing <code>;</code>/<code>!</code> starts the subprocess and returns immediately without waiting. Stdout and stderr default to discarded.</td> </tr>
@@ -170,11 +202,36 @@
170202
<h2 id="quotation-redirection">Redirection on quotations</h2>
171203

172204
<p>
173-
All of the redirection operators above also work on quotations.
205+
The redirection operators that don't change the stack also work on quotations:
206+
file redirects (<code>></code>, <code>>></code>, <code>2></code>, <code>2>></code>, <code>&amp;></code>, <code>&amp;>></code>),
207+
stdin (<code>&lt;</code>), and the merges (<code>2>&amp;1</code>, <code>1>&amp;2</code>).
174208
This is useful when you want to redirect the output of mshell code that uses <code>wl</code>, <code>wle</code>, or other built-in functions that write to stdout or stderr.
175209
It is also useful when you have many commands that you want to run while appending all the outputs to a single file, without having to put the redirection on each command invocation.
176210
</p>
177211

212+
<p>
213+
The captures (<code>*</code>, <code>*b</code>, <code>^</code>, <code>^b</code>) and the in-place redirect (<code>&lt;&gt;</code>) are <em>not</em> allowed on quotations,
214+
because they would change the quotation's stack effect.
215+
Capture the individual command lists inside the quotation instead — for example, run a list of commands and collect each stdout:
216+
</p>
217+
218+
<pre>
219+
<code><span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLITERAL">cmd1</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLITERAL">cmd2</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellLEFT_PAREN">(</span><span class="mshellASTERISK">*</span> <span class="mshellBANG">!</span><span class="mshellRIGHT_PAREN">)</span> <span class="mshellLITERAL">map</span> <span class="mshellLINECOMMENT"># Run each command, collect stdouts as a list of strings.</span>
220+
</code>
221+
</pre>
222+
223+
<p>
224+
Destination conflicts on quotations (e.g. two <code>></code> redirects, or <code>2>&amp;1</code> plus <code>2></code>) are caught at runtime.
225+
Since redirects never change a quotation's stack effect, they are invisible to the static type checker.
226+
</p>
227+
228+
<p>
229+
A quotation's redirect is resolved each time the quotation executes.
230+
So a <code>></code>-redirected quotation run repeatedly — stored and executed with <code>x</code> several times, or passed to <code>each</code> or <code>map</code> — truncates the file on every execution, leaving only the last run's output.
231+
Use <code>>></code> when a repeatedly-executed quotation should accumulate output.
232+
The exception is <code>loop</code>: the file is opened once when the loop starts, so all iterations write to the same open file.
233+
</p>
234+
178235
<pre>
179236
<code><span class="mshellLEFT_PAREN">(</span>
180237
<span class="mshellSTRING">"Hello from stdout"</span> <span class="mshellLITERAL">wl</span>

doc/mshell.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,30 @@ So the following are equivalent:
106106
[yourCommand] ^b *b !
107107
```
108108

109+
#### Merging one stream into the other
110+
111+
Use `2>&1` to send stderr to wherever stdout ends up, or `1>&2` to send stdout to wherever stderr ends up.
112+
These are complete tokens: no spaces are allowed inside them.
113+
Unlike POSIX shells, they are not order-sensitive fd duplication:
114+
a merge means "this stream goes to the other stream's *final* destination",
115+
so there is no `> file 2>&1` vs `2>&1 > file` ordering trap.
116+
Both streams share a single destination, preserving output order, on every platform.
117+
118+
```mshell
119+
[yourCommand] 2>&1 * ! # Captures stdout and stderr interleaved as one string.
120+
[[make] 2>&1 [grep -i error]] |; # stderr flows through the pipe.
121+
[yourCommand] 1>&2 ; # stdout appears on stderr.
122+
[yourCommand] `output.log` > 2>&1 ! # Both streams into output.log; equivalent to &>.
123+
```
124+
125+
#### Each stream has exactly one destination
126+
127+
A stream's destination can be set once: a file redirect, a capture, an in-place redirect, or a merge.
128+
Applying a second destination to the same stream is an error, caught both by the static type checker and at runtime.
129+
For example, ``[cmd] * `f` >`` (capture and file redirect on stdout), `[cmd] 2>&1 ^` (merge and capture on stderr),
130+
and `[cmd] 2>&1 1>&2` (circular merge) are all rejected.
131+
`2>&1` also cannot be combined with `<>`, since stderr text would be written back into the edited file.
132+
109133
Summary of external command operators:
110134

111135
Operator | Effect on external commands | Notes
@@ -123,13 +147,30 @@ Operator | Effect on external commands | Notes
123147
`&>>` | Redirect both stdout and stderr to a file. | Appends to the file.
124148
`^` | Capture stderr to the stack. | As a string.
125149
`^b` | Capture stderr to the stack. | As binary.
150+
`2>&1` | Merge stderr into stdout's destination. | Single token, no spaces. Not order-sensitive: stderr follows stdout's final destination.
151+
`1>&2` | Merge stdout into stderr's destination. | Single token, no spaces.
126152
`<` | Feed stdin from a value. | String, path, or binary.
127153
`<>` | In-place file modification. | Reads file to stdin, writes stdout back on success.
128154
`&` | Mark the command list to run asynchronously. | Marks the list; the trailing `;`/`!` starts the subprocess and returns immediately without waiting. Stdout and stderr default to discarded.
129155

130156
### Redirection on quotations
131157

132-
All of the redirection operators above also work on quotations. This is useful when you want to redirect the output of mshell code that uses `wl`, `wle`, or other built-in functions that write to stdout or stderr. It is also useful when you have many commands that you want to run while appending all the outputs to a single file, without having to put the redirection on each command invocation.
158+
The redirection operators that don't change the stack also work on quotations:
159+
file redirects (`>`, `>>`, `2>`, `2>>`, `&>`, `&>>`), stdin (`<`), and the merges (`2>&1`, `1>&2`).
160+
This is useful when you want to redirect the output of mshell code that uses `wl`, `wle`, or other built-in functions that write to stdout or stderr.
161+
It is also useful when you have many commands that you want to run while appending all the outputs to a single file, without having to put the redirection on each command invocation.
162+
163+
The captures (`*`, `*b`, `^`, `^b`) and the in-place redirect (`<>`) are *not* allowed on quotations,
164+
because they would change the quotation's stack effect.
165+
Capture the individual command lists inside the quotation instead, e.g. `[[cmd1] [cmd2]] (* !) map` to run each command and collect stdouts.
166+
167+
Destination conflicts on quotations (e.g. two `>` redirects, or `2>&1` plus `2>`) are caught at runtime.
168+
Since redirects never change a quotation's stack effect, they are invisible to the static type checker.
169+
170+
A quotation's redirect is resolved each time the quotation executes.
171+
So a `>`-redirected quotation run repeatedly — stored and executed with `x` several times, or passed to `each` or `map` — truncates the file on every execution, leaving only the last run's output.
172+
Use `>>` when a repeatedly-executed quotation should accumulate output.
173+
The exception is `loop`: the file is opened once when the loop starts, so all iterations write to the same open file.
133174

134175
```mshell
135176
(

0 commit comments

Comments
 (0)