Compare commits

...

4 Commits

9 changed files with 281 additions and 106 deletions

View File

@ -36,7 +36,9 @@ Transcript-based shell integration test runner. Rust.
- `...` inline = matches any characters on that line - `...` inline = matches any characters on that line
- `[N]` on last line of expected output = assert exit code N - `[N]` on last line of expected output = assert exit code N
- `[*]` = assert any non-zero exit code; default expects 0 - `[*]` = assert any non-zero exit code; default expects 0
- `$#` comment line = not executed, no output expected (e.g. `$# start the server`) - `#` at start of line = comment (not executed, no output expected)
- `$#` also works as comment (legacy syntax)
- `\#` in expected output = literal line starting with `#`
- `#` after a command = comment (stripped); `#` in expected output is literal - `#` after a command = comment (stripped); `#` in expected output is literal
- `@env KEY=VALUE` before first command = set environment variable - `@env KEY=VALUE` before first command = set environment variable
- `@teardown <command>` before first command = run command after all test commands - `@teardown <command>` before first command = run command after all test commands

View File

@ -30,16 +30,25 @@ ls: missing: No such file or directory
`[1]` after the expected output matches the exit code. `[1]` after the expected output matches the exit code.
`$#` is a comment line — not executed, no output expected: `#` at the start of a line is a comment — not executed, no output expected:
``` ```
$# start the server # start the server
$ my-server & $ my-server &
$# now test it # now test it
$ curl localhost:8080 $ curl localhost:8080
OK OK
``` ```
If expected output starts with `#`, escape it with `\#`:
```
$ echo "#hashtag"
\#hashtag
```
`#` elsewhere in expected output is literal (not a comment).
## Usage ## Usage
``` ```

View File

@ -68,9 +68,10 @@ fn strip_comment(line: &str) -> &str {
line line
} }
/// A line like `$# ...` or `$ # ...` — a comment, not a real command. /// A line like `$# ...`, `$ # ...`, or `# ...` — a comment, not a real command.
pub fn is_comment_line(line: &str) -> bool { pub fn is_comment_line(line: &str) -> bool {
line.starts_with("$#") line.starts_with('#')
|| line.starts_with("$#")
|| (line.starts_with("$ ") && strip_comment(&line[2..]).is_empty()) || (line.starts_with("$ ") && strip_comment(&line[2..]).is_empty())
} }
@ -269,6 +270,16 @@ pub fn parse(path: &str, content: &str) -> Result<ShoutFile, ParseError> {
} else if line.starts_with("\\$ ") && current.is_some() { } else if line.starts_with("\\$ ") && current.is_some() {
// Escaped dollar-space: literal expected output starting with "$ " // Escaped dollar-space: literal expected output starting with "$ "
current.as_mut().unwrap().expected.push(line[1..].to_string()); current.as_mut().unwrap().expected.push(line[1..].to_string());
} else if line.starts_with("\\#") && current.is_some() {
// Escaped hash: literal expected output starting with "#"
current.as_mut().unwrap().expected.push(line[1..].to_string());
} else if line.starts_with('#') {
// Comment line
if let Some(ref mut cmd) = current {
finalize_command(cmd);
commands.push(cmd.clone());
current = None;
}
} else if line.starts_with("$ ") { } else if line.starts_with("$ ") {
seen_command = true; seen_command = true;
if let Some(ref mut cmd) = current { if let Some(ref mut cmd) = current {

View File

@ -2,8 +2,8 @@ use crate::matching::match_output;
use crate::parse::{ShoutFile, is_comment_line, trim_trailing_empty}; use crate::parse::{ShoutFile, is_comment_line, trim_trailing_empty};
use crate::run::CommandResult; use crate::run::CommandResult;
fn escape_dollar(line: &str) -> String { fn escape_output(line: &str) -> String {
if line.starts_with("$ ") { if line.starts_with("$ ") || line.starts_with('#') {
format!("\\{line}") format!("\\{line}")
} else { } else {
line.to_string() line.to_string()
@ -44,7 +44,7 @@ pub fn rewrite_file(
// Skip past old expected output lines in the original // Skip past old expected output lines in the original
let mut j = i + 1; let mut j = i + 1;
while j < lines.len() while j < lines.len()
&& !is_comment_line(lines[j]) && !(is_comment_line(lines[j]) && !lines[j].starts_with("\\#"))
&& !(lines[j].starts_with("$ ") && !lines[j].starts_with("\\$ ")) && !(lines[j].starts_with("$ ") && !lines[j].starts_with("\\$ "))
{ {
j += 1; j += 1;
@ -88,7 +88,7 @@ pub fn rewrite_file(
} else { } else {
// Replace with actual output // Replace with actual output
for al in &result.actual { for al in &result.actual {
output.push(escape_dollar(al)); output.push(escape_output(al));
} }
// Re-add exit code marker if it existed // Re-add exit code marker if it existed
if let Some(marker) = old_exit_marker { if let Some(marker) = old_exit_marker {

View File

@ -3,3 +3,13 @@ hello
$ echo "keep # this" $ echo "keep # this"
keep # this keep # this
# this is a comment
$ echo "after comment"
after comment
$ echo "#hashtag"
\#hashtag
$ echo "https://google.com/#autoload"
https://google.com/#autoload

View File

@ -34,11 +34,12 @@ ln -s /path/to/shout/vim ~/.local/share/nvim/site/pack/shout/start/shout
| Pattern | Highlight | | Pattern | Highlight |
|---|---| |---|---|
| `$ command` | Statement (prompt as Special) | | `$ command` | Statement (prompt as Special) |
| `$# comment` | Comment | | `# comment` | Comment |
| `@env KEY=VALUE` | PreProc / Identifier / String | | `@env KEY=VALUE` | PreProc / Identifier / String |
| `@setup`, `@teardown` | PreProc | | `@setup`, `@teardown` | PreProc |
| Expected output | String | | Expected output | String |
| `...` (wildcard) | WarningMsg | | `...` (wildcard) | WarningMsg |
| `[N]`, `[*]` (exit code) | Constant | | `[N]`, `[*]` (exit code) | Constant |
| `\$ ...` (escaped dollar) | SpecialChar + String | | `\$ ...` (escaped dollar) | SpecialChar + String |
| `\# ...` (escaped hash) | SpecialChar + String |
| `# inline comment` | Comment | | `# inline comment` | Comment |

View File

@ -1,5 +1,5 @@
-- Buffer-local settings for .shout files -- Buffer-local settings for .shout files
vim.bo.commentstring = "$# %s" vim.bo.commentstring = "# %s"
vim.bo.shiftwidth = 0 vim.bo.shiftwidth = 0
vim.bo.tabstop = 2 vim.bo.tabstop = 2
vim.bo.expandtab = true vim.bo.expandtab = true

View File

@ -16,7 +16,8 @@ syn match shoutEnvValue /.*$/ contained
syn match shoutSetupDirective /^@setup\s\+.*$/ contains=shoutDirectiveKey syn match shoutSetupDirective /^@setup\s\+.*$/ contains=shoutDirectiveKey
syn match shoutTeardownDirective /^@teardown\s\+.*$/ contains=shoutDirectiveKey syn match shoutTeardownDirective /^@teardown\s\+.*$/ contains=shoutDirectiveKey
" Comment commands: $# ... or $ # ... " Comment lines: # ..., $# ..., or $ # ...
syn match shoutCommentCommand /^#.*$/
syn match shoutCommentCommand /^\$#.*$/ syn match shoutCommentCommand /^\$#.*$/
syn match shoutCommentCommand /^\$\s\+#.*$/ syn match shoutCommentCommand /^\$\s\+#.*$/
@ -25,9 +26,11 @@ syn match shoutPrompt /^\$\s/ contained
syn match shoutCommand /^\$\s.\+/ contains=shoutPrompt,shoutInlineComment syn match shoutCommand /^\$\s.\+/ contains=shoutPrompt,shoutInlineComment
syn match shoutInlineComment /\s\+#[^"']*$/ contained syn match shoutInlineComment /\s\+#[^"']*$/ contained
" Escaped dollar in expected output " Escaped dollar/hash in expected output
syn match shoutEscapedDollar /^\\\$/ contained syn match shoutEscapedChar /^\\\$/ contained
syn match shoutEscapedLine /^\\\$.*$/ contains=shoutEscapedDollar syn match shoutEscapedChar /^\\#/ contained
syn match shoutEscapedLine /^\\\$.*$/ contains=shoutEscapedChar
syn match shoutEscapedLine /^\\#.*$/ contains=shoutEscapedChar
" Wildcards " Wildcards
syn match shoutWildcardLine /^\.\.\.$/ syn match shoutWildcardLine /^\.\.\.$/
@ -55,7 +58,7 @@ hi def link shoutPrompt Special
hi def link shoutCommand Statement hi def link shoutCommand Statement
hi def link shoutInlineComment Comment hi def link shoutInlineComment Comment
hi def link shoutEscapedDollar SpecialChar hi def link shoutEscapedChar SpecialChar
hi def link shoutEscapedLine String hi def link shoutEscapedLine String
hi def link shoutWildcardLine WarningMsg hi def link shoutWildcardLine WarningMsg

View File

@ -11,13 +11,16 @@
:root { :root {
--bg: #fff; --bg: #fff;
--fg: #444; --fg: #444;
--bright: #1a1a1a; --bright: #000;
--green: #1a7f37; --green: #1a7f37;
--red: #cf222e; --red: #cf222e;
--dim: #888; --dim: #888;
--accent: #1a7f37; --accent: #1a7f37;
--code-bg: #f5f5f5; --code-bg: #f5f5f5;
--border: #ddd; --border: #ddd;
--yellow: #9a7200;
--install-bg: #f0faf2;
--install-border: #c5e4cc;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -31,83 +34,176 @@
--accent: #4ec966; --accent: #4ec966;
--code-bg: #111; --code-bg: #111;
--border: #222; --border: #222;
--yellow: #e5b567;
--install-bg: #0d1a10;
--install-border: #1a3d20;
} }
} }
html { font-size: 16px; }
body { body {
background: var(--bg); background: var(--bg);
color: var(--fg); color: var(--fg);
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', 'Consolas', monospace; font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', 'Consolas', monospace;
line-height: 1.6; line-height: 1.6;
padding: 0 1.5rem; padding: 0 24px;
max-width: 680px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
/* ---- Hero ---- */
header { header {
padding: 6rem 0 2.5rem; padding: 64px 0 0;
} }
h1 { h1 {
font-size: 2.5rem; font-size: 64px;
font-weight: 700; font-weight: 800;
color: var(--bright); color: var(--bright);
letter-spacing: -0.03em; letter-spacing: -0.04em;
line-height: 1;
display: flex;
align-items: baseline;
vertical-align: baseline;
} }
h1 span { h1 .dollar {
color: var(--accent); color: var(--accent);
margin-right: 10px;
} }
.tagline {
font-size: 1.1rem; .subtitle {
font-size: 20px;
color: var(--dim); color: var(--dim);
margin-top: 0.5rem; margin-top: 8px;
font-weight: 400;
} }
/* ---- Install ---- */
.install { .install {
margin-top: 2rem; margin-top: 32px;
display: inline-block; display: flex;
align-items: center;
justify-content: space-between;
background: var(--install-bg);
border: 1px solid var(--install-border);
padding: 11px 11px 11px 19px;
border-radius: 8px;
font-size: 14px;
color: var(--bright);
}
.install code {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.install .prompt { color: var(--dim); }
.install .cmd { color: var(--bright); }
.copy-btn {
background: var(--accent);
color: #fff;
border: none;
padding: 7px 14px;
border-radius: 5px;
font-family: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: opacity 0.15s;
flex-shrink: 0;
margin-left: 16px;
}
@media (prefers-color-scheme: dark) {
.copy-btn { color: #0a0a0a; }
}
.copy-btn:hover {
opacity: 0.85;
}
/* ---- Section boxes ---- */
.section-box {
background: var(--code-bg); background: var(--code-bg);
border: 1px solid var(--border); border: 1px solid var(--border);
padding: 0.6rem 1.2rem; border-radius: 8px;
border-radius: 4px; padding: 20px;
font-size: 0.9rem; padding-top: 16px;
color: var(--bright); margin-bottom: 24px;
cursor: pointer;
position: relative;
transition: border-color 0.15s;
} }
.install:hover { .two-col {
border-color: var(--accent); display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 32px;
align-items: start;
} }
.install .hint { .two-col .explain h2 {
color: var(--dim); margin-bottom: 16px;
font-size: 0.75rem;
margin-left: 1rem;
} }
section { .two-col .explain p {
padding: 0 0 2em 0; margin-bottom: 14px;
font-size: 15px;
}
.two-col .explain .feature {
margin-bottom: 11px;
font-size: 14px;
}
.two-col .explain .feature code {
padding: 2px 0px;
border-radius: 3px;
font-size: 13px;
}
.section-box pre {
background: var(--bg);
margin-bottom: 0;
}
.section-box pre + pre {
margin-top: 12px;
}
.section-box p:last-child {
margin-bottom: 0;
}
@media (max-width: 680px) {
.two-col {
grid-template-columns: 1fr;
gap: 24px;
}
.section-box {
padding: 19px;
}
} }
h2 { h2 {
font-size: 0.85rem; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--dim); color: var(--fg);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: 1.5rem; margin-bottom: 16px;
}
section {
padding: 0 0 32px 0;
} }
p { p {
margin-bottom: 1rem; margin-bottom: 16px;
font-size: 0.95rem; font-size: 15px;
} }
.bright { color: var(--bright); } .bright { color: var(--bright); }
@ -118,19 +214,19 @@
pre { pre {
background: var(--code-bg); background: var(--code-bg);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 4px; border-radius: 6px;
padding: 1.2rem 1.4rem; padding: 19px 22px;
overflow-x: auto; overflow-x: auto;
font-size: 0.85rem; font-size: 13px;
line-height: 1.7; line-height: 1.7;
margin-bottom: 1.5rem; margin-bottom: 24px;
} }
pre code { pre code {
color: var(--fg); color: var(--fg);
} }
.prompt { color: var(--dim); } .prompt { color: var(--yellow); }
.cmd { color: var(--bright); } .cmd { color: var(--bright); }
.output { color: var(--fg); } .output { color: var(--fg); }
.comment { color: var(--dim); font-style: italic; } .comment { color: var(--dim); font-style: italic; }
@ -138,12 +234,11 @@
.exit-code { color: var(--red); } .exit-code { color: var(--red); }
.pass { color: var(--green); } .pass { color: var(--green); }
footer { footer {
padding: 3rem 0; padding: 48px 0;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
color: var(--dim); color: var(--dim);
font-size: 0.8rem; font-size: 13px;
} }
footer a { footer a {
@ -165,27 +260,38 @@
} }
@media (max-width: 520px) { @media (max-width: 520px) {
header { padding: 3rem 0 2rem; } header { padding: 40px 0 0; }
h1 { font-size: 2rem; } h1 { font-size: 44px; }
section { padding: 2rem 0; } .subtitle { font-size: 16px; }
} }
</style> </style>
</head> </head>
<body> <body>
<header> <header>
<h1><span>$</span> shout</h1> <h1><span class="dollar">$</span>shout</h1>
<p class="tagline">shell output tester</p> <p class="subtitle">shell output tester</p>
<div class="install" onclick="navigator.clipboard.writeText('curl -fsSL https://because.sh/shout | sh')"> <div class="install">
<span class="prompt">$</span> <span class="cmd">curl -fsSL https://because.sh/shout | sh</span> <code><span class="prompt">$</span> <span class="cmd">curl -fsSL https://because.sh/shout | sh</span></code>
<span class="hint">click to copy</span> <button class="copy-btn" onclick="copyInstall(this)">Copy</button>
</div> </div>
</header> </header>
<section> <div style="padding: 40px 0 0;">
<h2>Write a test</h2> <div class="section-box">
<p>A <code>.shout</code> file is just a shell session. Commands start with <code class="bright">$</code>, everything else is expected output.</p> <div class="two-col">
<pre><code><span class="prompt">$</span> <span class="cmd">echo hello</span> <div class="explain">
<h2>✓ Write a test</h2>
<p>Each <code class="bright">.shout</code> file is a session.</p>
<p class="feature">Commands start with <code class="prompt">$</code>.</p>
<p class="feature">Everything else is expected output.</p>
<p class="feature"><code class="wildcard">...</code> matches anything &mdash; inline or across lines.</p>
<p class="feature"><code class="exit-code">[1]</code> asserts the exit code. Default expects 0.</p>
<p class="feature">Lines starting with <code class="dim">#</code> are comments.</p>
<p class="feature">(<code class="dim">\#</code> matches output starting with <code class="dim">#</code>.)</p>
</div>
<div class="example">
<pre><code><span class="prompt">$</span> <span class="cmd">echo hello</span>
<span class="output">hello</span> <span class="output">hello</span>
<span class="prompt">$</span> <span class="cmd">ls missing</span> <span class="prompt">$</span> <span class="cmd">ls missing</span>
@ -193,54 +299,79 @@
<span class="exit-code">[1]</span> <span class="exit-code">[1]</span>
<span class="prompt">$</span> <span class="cmd">brew --version</span> <span class="prompt">$</span> <span class="cmd">brew --version</span>
<span class="output">Homebrew 5</span><span class="wildcard">...</span></code></pre> <span class="output">Homebrew 5</span><span class="wildcard">...</span>
<p><code class="wildcard">...</code> matches anything &mdash; inline or across lines.</p>
<p><code class="exit-code">[1]</code> asserts the exit code.</p> <span class="comment"># start the server</span>
<p><code class="dim">$#</code> starts a comment line &mdash; not executed, no output expected.</p>
<pre><code><span class="prompt">$</span><span class="comment"># start the server</span>
<span class="prompt">$</span> <span class="cmd">my-server &amp;</span> <span class="prompt">$</span> <span class="cmd">my-server &amp;</span>
<span class="prompt">$</span><span class="comment"># now test it</span>
<span class="prompt">$</span> <span class="cmd">curl localhost:8080</span> <span class="prompt">$</span> <span class="cmd">curl localhost:8080</span>
<span class="output">OK</span></code></pre> <span class="output">OK</span></code></pre>
</section> </div>
</div>
</div>
</div>
<section> <div class="section-box">
<h2>Setup &amp; teardown</h2> <div class="two-col">
<p>Use <code class="bright">@setup</code> to share commands across test files. Use <code class="bright">@teardown</code> to clean up after tests &mdash; it runs regardless of pass/fail.</p> <div class="explain">
<pre><code><span class="comment"># setup.shout</span> <h2>✓ Setup &amp; teardown</h2>
<p>Use <code class="bright">@setup</code> to share commands across test files.</p>
<p>Use <code class="bright">@teardown</code> to clean up after tests &mdash; it runs regardless of pass/fail.</p>
<p><code>@teardown</code> can appear in both <code>.shout</code> files and setup files. Teardown failures produce warnings but don't affect test results.</p>
</div>
<div class="example">
<pre><code><span class="comment"># setup.shout</span>
<span class="output">export DB_URL=sqlite:data/test.db</span> <span class="output">export DB_URL=sqlite:data/test.db</span>
<span class="bright">@teardown</span> <span class="cmd">rm -f "$SHOUT_PROJECT_DIR/data/test.db"</span></code></pre> <span class="bright">@teardown</span> <span class="cmd">rm -f "$SHOUT_PROJECT_DIR/data/test.db"</span></code></pre>
<pre><code><span class="bright">@setup</span> <span class="cmd">setup.shout</span> <pre><code><span class="bright">@setup</span> <span class="cmd">setup.shout</span>
<span class="bright">@teardown</span> <span class="cmd">rm -f /tmp/extra-cleanup</span> <span class="bright">@teardown</span> <span class="cmd">rm -f /tmp/extra-cleanup</span>
<span class="prompt">$</span> <span class="cmd">create-db &amp;&amp; run-tests</span> <span class="prompt">$</span> <span class="cmd">create-db &amp;&amp; run-tests</span>
<span class="wildcard">...</span></code></pre> <span class="wildcard">...</span></code></pre>
<p><code>@teardown</code> can appear in both <code>.shout</code> files and setup files. Teardown failures produce warnings but don't affect test results.</p> </div>
</section> </div>
</div>
<section> <div class="section-box">
<h2>Macros</h2> <div class="two-col">
<p>Use <code class="bright">@def</code> to define reusable command macros.</p> <div class="explain">
<pre><code><span class="bright">@def</span> <span class="cmd">greet echo "hello world"</span> <h2>✓ Macros</h2>
<p>Use <code class="bright">@def</code> to define reusable command macros.</p>
<p>If a command matches a macro name exactly, the body is substituted. Use <code>\</code> for multi-line bodies. Macros from setup files are inherited; user-file macros override them.</p>
</div>
<div class="example">
<pre><code><span class="bright">@def</span> <span class="cmd">greet echo "hello world"</span>
<span class="prompt">$</span> <span class="cmd">greet</span> <span class="prompt">$</span> <span class="cmd">greet</span>
<span class="output">hello world</span></code></pre> <span class="output">hello world</span></code></pre>
<p>If a command matches a macro name exactly, the body is substituted. Use <code>\</code> for multi-line bodies — the body can start on the same line or on the next continuation line. Macros from setup files are inherited; user-file macros override them.</p> </div>
</section> </div>
</div>
<section> <div class="section-box">
<h2>Run it</h2> <div class="two-col">
<pre><code><span class="prompt">$</span> <span class="cmd">shout test</span> <div class="explain">
<h2>✓ Run it</h2>
<p>Each file gets a fresh temp directory and its own <code>/bin/sh</code> session. State carries between commands within a file.</p>
</div>
<div class="example">
<pre><code><span class="prompt">$</span> <span class="cmd">shout test</span>
<span class="pass">............... <span class="pass">...............
15 passed</span> <span class="dim">in 23ms</span></code></pre> 15 passed</span> <span class="dim">in 23ms</span></code></pre>
<p>Each file gets a fresh temp directory and its own <code>/bin/sh</code> session. State carries between commands within a file.</p> </div>
</section> </div>
</div>
<section> <div class="section-box">
<h2>Update expectations</h2> <div class="two-col">
<pre><code><span class="prompt">$</span> <span class="cmd">shout test --update</span></code></pre> <div class="explain">
<p>Rewrites your <code>.shout</code> files with the actual output. No more copy-pasting from the terminal.</p> <h2>✓ Update expectations</h2>
</section> <p>Rewrites your <code>.shout</code> files with the actual output. No more copy-pasting from the terminal.</p>
</div>
<div class="example">
<pre><code><span class="prompt">$</span> <span class="cmd">shout test --update</span></code></pre>
</div>
</div>
</div>
<section> <section>
<h2>Usage</h2> <h2>Usage</h2>
@ -268,12 +399,12 @@ Options:
<section> <section>
<h2>Environment</h2> <h2>Environment</h2>
<p>Shout sets these variables before running your commands:</p> <p>Shout sets these variables before running your commands:</p>
<pre><code><span class="bright">HOME</span> <span class="dim"></span> <span class="output">temp directory for this test file</span> <pre><code><span class="bright">HOME</span> <span class="dim"></span> <span class="output">temp directory for this test file</span>
<span class="bright">SHOUT_DIR</span> <span class="dim"></span> <span class="output">same temp directory</span> <span class="bright">SHOUT_DIR</span> <span class="dim"></span> <span class="output">same temp directory</span>
<span class="bright">SHOUT_SOURCE_DIR</span> <span class="dim"></span> <span class="output">directory containing the .shout file</span> <span class="bright">SHOUT_SOURCE_DIR</span> <span class="dim"></span> <span class="output">directory containing the .shout file</span>
<span class="bright">SHOUT_PROJECT_DIR</span> <span class="dim"></span> <span class="output">directory where shout was invoked</span> <span class="bright">SHOUT_PROJECT_DIR</span> <span class="dim"></span> <span class="output">directory where shout was invoked</span>
<span class="bright">PORT</span> <span class="dim"></span> <span class="output">auto-assigned from 5400 (or --port-from), increments per file</span> <span class="bright">PORT</span> <span class="dim"></span> <span class="output">auto-assigned from 5400 (or --port-from), increments per file</span>
<span class="bright">PATH</span> <span class="dim"></span> <span class="output">prepended with --path dirs, if any</span></code></pre> <span class="bright">PATH</span> <span class="dim"></span> <span class="output">prepended with --path dirs, if any</span></code></pre>
<p>Each file runs in its own temp directory. <code>--clean-env</code> starts with an empty environment instead of inheriting yours.</p> <p>Each file runs in its own temp directory. <code>--clean-env</code> starts with an empty environment instead of inheriting yours.</p>
</section> </section>
@ -281,5 +412,13 @@ Options:
<a href="https://github.com/because/shout">GitHub</a> &middot; <a href="https://www.npmjs.com/package/@because/shout">npm</a> <a href="https://github.com/because/shout">GitHub</a> &middot; <a href="https://www.npmjs.com/package/@because/shout">npm</a>
</footer> </footer>
<script>
function copyInstall(btn) {
navigator.clipboard.writeText('curl -fsSL https://because.sh/shout | sh');
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
}
</script>
</body> </body>
</html> </html>