Shell scripting
Verum's shell-scripting framework (core/shell/) replaces ad-hoc bash
or zx-style scripts with a fully typed, verifiable runtime that scales
from one-shot one-liners to long-running daemons. Everything that follows
assumes a single import:
mount core.shell.*;
This brings the executor (run, sh, Executor), structured concurrency
helpers (background, parallel, nursery), streaming (stream_lines),
typed command DSLs (git, docker, kubectl), built-ins (cp, which,
mkdir_p), interactive prompts (input, confirm, password), progress
indicators (Progress, Spinner), and the refinement-typed config helpers
into scope.
For the full API reference see stdlib → shell.
Tagged-literal dispatch with auto-escape
The fundamental primitive is the sh#"..." tagged literal. Every ${expr}
inside the literal is automatically passed through the
ShellEscape protocol, so user data cannot break
out of its quoted form:
let user_input: Text = read_line()?;
sh#"echo ${user_input}"?; // safe — even if user_input is "'; rm -rf /"
When you genuinely want to splice unescaped shell text (rare, dangerous),
use $unsafe{...} inside an unsafe block:
let raw_pipeline: Text = "grep error | head -10".into();
unsafe {
sh#"journalctl -u myservice | $unsafe{raw_pipeline}"?;
}
Quick recipes
Run a command and capture output
let result = sh#"git rev-parse --short HEAD"?;
let hash: Text = result.text(); // stdout, trimmed
println(&f"current commit: {hash}");
Pipe through several commands
let count: Int = sh#"git log --oneline | grep feat: | wc -l"?
.text()
.parse::<Int>()?;
Parse JSON output into a typed value
type Pod is { name: Text, ready: Bool }
let pods: List<Pod> = sh#"kubectl get pods -o json"?
.json::<KubeResponse>()?
.items;
Run commands in parallel
async fn ci() using [ShellContext] {
nursery(on_error: FailFast) {
spawn sh#"cargo test"?;
spawn sh#"cargo clippy -- -D warnings"?;
spawn sh#"verum check core/"?;
};
}
Stream long-running output
async fn tail_logs() using [ShellContext] {
async for line in stream_lines("journalctl -u myservice -f") {
let line = line?;
if line.contains("FATAL") { alert(&line).await; }
}
}
For very fast producers, use the bounded variant with explicit overflow policy:
let mut stream = stream_lines_bounded(
&"vmstat 1".into(),
StreamConfig.lossy(buffer: 16),
).await?;
stream.for_each(|line| {
update_metrics(line);
true
}).await;
Cancellation with grace period
let token = CancellationToken.with_timeout(d#"30s");
match sh_with_cancel(&"./long-task.sh".into(), &token, d#"5s").await {
Ok(r) => println(&r.text()),
Err(ShellError.Cancelled { reason, .. }) => println(&f"stopped: {reason}"),
Err(e) => die(&f"{e}", 1),
}
The executor sends SIGTERM first, waits up to 5s for graceful exit,
then escalates to SIGKILL — matching standard Unix shutdown conventions.
Retry with exponential backoff
let exec = Executor.new()
.with_retry(RetryPolicy.simple(5))
.with_timeout(d#"30s");
let result = exec.run_idempotent(&"./flaky-deploy.sh".into(), []).await?;
Typed command DSLs
For frequently invoked tools, prefer the algebraic command types over
free-form sh#. Each DSL provides refinement-typed argument atoms and a
render() method.
Git
let url = GitUrl.parse("https://github.com/user/repo.git".into())?;
git(GitCmd.Clone {
url, dest: Some(PathBuf.from("/tmp/work")),
depth: Some(1), branch: Some(GitBranch.parse("main".into())?),
recurse_submodules: false,
}).await?;
Invalid URLs and refspecs are rejected at construction:
GitUrl.parse("'; rm -rf /".into())?; // Err — refinement violated
Docker
let image = DockerImage.parse("myorg/api:1.2.3".into())?;
docker(DockerCmd.Run {
image, cmd: ["serve".into()],
env: [("PORT".into(), "8080".into())],
volumes: [VolumeMount.rw(PathBuf.from("/data"), PathBuf.from("/app/data"))],
ports: [PortMapping.tcp(8080, 8080)],
rm: true, detach: true, name: Some("api".into()),
}).await?;
Kubectl
The kubectl DSL is parameterised over the resource kind, so semantically incoherent calls don't typecheck:
let cmd: KubectlCmd<Pod> = KubectlCmd.Logs {
pod: KubeName.parse("api-7d9f-xyz".into())?,
namespace: Some(KubeNamespace.parse("default".into())?),
container: None, follow: true, tail: Some(100), since: None,
};
kubectl(cmd).await?;
// KubectlCmd<ConfigMap> does NOT support .Logs — would be a type error.
Built-ins (no spawning)
Pure-Verum implementations of common file operations. Faster than spawning
cp/rm/which per call, and identically portable across platforms:
mkdir_p(&PathBuf.from("/tmp/out").as_path())?;
write_str(&PathBuf.from("/tmp/out/data.json").as_path(), &payload)?;
let exists = command_exists(&"git".into());
let path = which(&"git".into());
Refinement-typed configurations
core/shell/verify.vr provides reusable refinement atoms. Constructors
return Err on invalid input, so a successfully built DeployConfig is a
proof that every field is valid:
let config = DeployConfig.parse(
"myservice".into(),
"1.2.3".into(), // SemVer-validated
"production".into(), // DNS-1123 namespace
"manifest.yaml".into(), // .yaml/.yml ending enforced
300, // PortNumber 1..65535
)?;
Interactive prompts
let name = input_required(&"Project name: ".into());
let template = select(&"Template:".into(), &[
("CLI App".into(), "cli"),
("Web Service".into(), "web"),
]);
if !confirm(&f"Create {name} ({template})?") { exit(0); }
let token = password(&"GitHub token: ".into());
Progress indicators
let mut progress = Progress.new("Building".into(), 3);
sh#"cargo build --release"?; progress.advance();
sh#"docker build -t app:latest ."?; progress.advance();
sh#"docker push app:latest"?; progress.advance();
progress.done("✓ released".into());
For unbounded operations:
let mut spinner = Spinner.new("Connecting".into());
spinner.start().await;
let conn = connect(&endpoint).await?;
spinner.stop("✓ connected".into()).await;
Permissions (frontmatter)
Add an explicit allow-list at the top of any .vr script. The runtime
permission gate denies anything not declared:
#!/usr/bin/env verum
// !@permission(run: ["git", "kubectl"])
// !@permission(fs_read: ["/etc/kube/*"])
// !@permission(net: ["api.github.com:443"])
mount core.shell.*;
async fn main() using [ShellContext] {
let ctx = bootstrap_from_file(&PathBuf.from("script.vr").as_path())?;
provide ShellContext = ctx;
sh#"git status"?; // OK — `git` is allow-listed
sh#"curl ..."?; // PermissionDenied at runtime
}
Testability — mock context
Unit-test scripts without spawning real processes:
#[test]
async fn deploy_runs_kubectl_in_order() {
provide ShellContext = ShellContext.mock([
MockResponse.success("kubectl apply".into(), "created".into()),
MockResponse.success("kubectl rollout".into(), "rolled out".into()),
]);
deploy_v2(&config).await?;
}
See also
- stdlib → shell — full API reference
core/shell/— implementationvcs/specs/L2-standard/shell/— type-check coverage