agentleFS
buildosaurusbuildosaurus/scripteratops/skills/rust-script/SKILL.md

rust-script

Skill0 starsChanged 3 months agoMITView on GitHub

Write self-contained, portable, testable Rust command-line tools — a Cargo project with pure-logic/side-effect separation (boundaries as traits) that `cargo test` can drive with fakes, a clap CLI, and a build/test/clippy/fmt gate. Use as soon as the user wants a Rust script, CLI utility, automation tool, or "glue" code (orchestrating git, curl/http, file manipulation, parsing) in Rust, even if they never say "cargo". Also triggers on adding tests to an existing Rust tool, making one portable/testable, or porting a shell/Python script to Rust.

  • Installs packages

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every module ships #[cfg(test)] mod tests in the same file (idiomatic Rust).

    line 204

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust script

Standard for all Rust command-line tools: a small Cargo project whose logic lives in a testable library, driven by a thin clap binary, verified by the compiler + cargo test + clippy + rustfmt. The goal is a self-contained tool (one static binary, no runtime to install) whose logic is trivially testable — the Rust counterpart of the python-script, swift-script, kotlin-script, typescript-script, and go-script skills.

Three decisions are already settled. Don't second-guess them, apply them:

  1. Distribution → a Cargo project compiled to a single static binary (cargo build --release) — like Go, no runtime, easy cross-compile.
  2. Interface → clap (derive API).
  3. Tests → cargo test (built-in #[test]), made easy by an architecture that isolates side effects behind traits.

This file carries the decisions; the code implementing them is a complete, gate-passing project in assets/template/. Copy and adapt that project — don't retype it from here. Then delete what your tool doesn't earn.

1. Project layout

tool/
├── Cargo.toml              # deps (pinned in Cargo.lock — commit it, it's a binary)
├── Cargo.lock
├── src/
│   ├── lib.rs              # module tree → LIBRARY crate (testable, `use tool::…`)
│   ├── main.rs            # clap wiring → thin binary
│   ├── errors.rs           # ToolError enum (thiserror) + exit_code()
│   ├── logic.rs            # pure transform (+ #[cfg(test)] tests)
│   ├── boundaries.rs       # CommandRunner / HttpFetcher / EnvironmentReading traits
│   ├── config.rs           # Config + precedence + JSON file read (+ tests)
│   ├── color.rs            # ColorMode + should_color (+ tests)
│   ├── log.rs              # net-verbosity → level; stderr Logger
│   ├── runner.rs           # std::process subprocess boundary
│   ├── http.rs             # ureq boundary
│   └── work.rs             # orchestration (do_work) (+ tests)
└── README.md

The lib/bin split is the whole point: the binary can't be unit-tested as easily as a library, so all logic lives in the library crate (src/lib.rs + modules), and src/main.rs only parses args and wires things together (use tool::…).

  • Pin every dependency — cargo add writes exact versions to Cargo.toml and locks them in Cargo.lock; commit Cargo.lock (this is a binary).
  • Rust's stdlib has no HTTP and no JSON, so those earn a crate. The kept set: clap (CLI), serde/serde_json (config), ureq (HTTP, blocking — no async runtime), thiserror (typed error enum). Subprocess (std::process) and env (std::env) are stdlib.

2. Structure

The structure is designed to make tests trivial: pure logic never touches the outside world directly. Everything that leaves the process (subprocess, network, filesystem, environment) sits behind a trait boundary, injected into the orchestration. Tests pass a fake impl; production passes the real one.

  • Boundaries are traits — CommandRunner, HttpFetcher, EnvironmentReading. These are the single mock hooks. A fake is a tiny struct with one method.
  • Pure logic (transform) — input → output, no I/O, tested by direct call.
  • Config is a serde struct with Option fields (so "absent" is distinct from "empty"); precedence is one pure function, resolve_config, using Option::or.
  • Orchestration (do_work) assembles boundary calls + pure logic: data positional, behaviour flags last, returns Result<usize, ToolError>. It takes the boundaries as &dyn Trait, so a test drives it with no process, no socket.
  • main.rs is thin: parse args, build the real boundaries, call do_work, map the error to an exit code. Nothing worth testing lives here.

2.1 Errors with thiserror

ToolError is an enum with one variant per failure category, deriving thiserror::Error for the Display/Error impls, plus an exit_code() method that keeps the process-exit contract with the category. Every fallible function returns Result<_, ToolError>; ? propagates.

2.2 Subprocess boundary

ProcessRunner routes all subprocess calls through std::process::Command:

  • Never a shell. Pass the program + an args slice — no sh -c, so no injection from a filename or argument.
  • Always a timeout. Rust's std has no built-in process timeout, so the template drains both pipes on background threads and polls try_wait against a deadline; on deadline it kill()s the child and returns Timeout (exit 124). (For a crate-based alternative, wait-timeout replaces the poll loop.)
  • On non-zero exit, return Command { command, code, stderr }.

2.3 Config and precedence

Resolve settings with an explicit, documented precedence — higher wins:

CLI flags  >  environment variables  >  config file  >  built-in defaults

resolve_config is pure (Option::or-chains the layers so an absent value never clobbers a lower layer). Each source is read by one boundary — read_config_file (JSON via serde), config_from_environment (through the EnvironmentReading hook). Secrets get no CLI flag (argv is world-readable via ps); a token comes from the environment or config file. Never log a raw Config — route it through redacted(…).

2.4 HTTP requests

UreqClient wraps ureq in one boundary:

  • Set an explicit timeout on the agent (timeout_global).
  • Non-2xx is an error — ureq returns Error::StatusCode on 4xx/5xx by default; map it to a typed Http { url, status }.
  • Inject a fake HttpFetcher to test offline. ureq is blocking (no tokio), which keeps the boundary a plain synchronous method.

2.5 Colorized logging

Logging uses the template's own Logger, constructed once in main.rs and writing to stderr (never stdout — stdout is for program data). Color:

  • should_color(mode, is_tty, env) is a pure decision — precedence --color=always/never > NO_COLOR/FORCE_COLOR > TTY (and not TERM=dumb).
  • TTY detection in main.rs uses std::io::IsTerminal (stdlib, no dependency).
  • The Logger holds its sink in a RefCell<Box<dyn Write>> so &self methods can write and tests can inject io::sink().

2.6 Robustness & signals

  • Bound every subprocess and HTTP call with a timeout (§2.2, §2.4).
  • Signals: Rust installs no handlers by default, so SIGINT/SIGTERM terminate with the OS default exit codes (130 / 143) — no handler needed for the baseline. SIGPIPE caveat: Rust ignores SIGPIPE at startup, so writing to a closed downstream pipe (tool | head) surfaces as an io::Error and a println! can panic with "Broken pipe". If your tool streams into pipes, handle the write error (or reset SIGPIPE to default via a small unsafe/libc call).
  • Preflight external tools for glue scripts (a which-style check up front), returning Config(...) listing every missing tool.

2.7 Subcommands (if needed)

clap derive handles this natively: an enum with #[derive(Subcommand)] as a #[command(subcommand)] field on the root Cli. No manual dispatch.

2.8 Man page (optional)

clap can emit man pages via the clap_mangen crate, but the simplest path (no extra dep) is help2man from the built binary's --help:

help2man --no-info --name "tool" "target/release/tool" > tool.1
sudo install -m 644 tool.1 /usr/local/share/man/man1/tool.1

2.9 Build & install (optional, for tools you distribute)

The README carries the environment set-up, not just the gates: a Prerequisites section with per-platform install of the Rust toolchain (macOS/Linux/Windows) and the dependency list, pointing to Cargo.toml + Cargo.lock as the pinned source. For a distributed binary, an optional --dependencies flag printing the pinned versions is a nice touch — skip it for source-run tools.

  • Single binary: cargo build --release → target/release/tool (static, self-contained). cargo install --path . drops it in ~/.cargo/bin.
  • Cross-compile: cargo build --release --target <triple> (add the target with rustup target add).
  • Run from source: cargo run -- <args>.

3. CLI contract

clap (derive) gives most of the baseline for free:

  • -h/--help and -V/--version are automatic (#[command(version)]).
  • Verbosity: #[arg(short, long, action = ArgAction::Count)] verbose: u8 counts occurrences; same for quiet. Map the net count with log_level_for_verbosity(verbose as i32 - quiet as i32).
  • --color is a CLI-local #[derive(ValueEnum)] mapped into the core ColorMode via From — this keeps clap out of the library.
  • --dry-run short-circuits before output, announced at warning level.
  • Stream discipline: program data → stdout (println!); logs, progress, diagnostics → stderr (the Logger).
  • clap groups options cleanly in --help by default; use help_heading on args if you want explicit sections.

Exit codes

main returns ExitCode; on error it logs and returns ExitCode::from(code).

CodeMeaning
0Success (also --help/--version)
1Runtime error (a ToolError); clap also uses 2 for usage errors
2Usage error (clap emits this itself)
124A subprocess exceeded its timeout (Timeout)
130Interrupted — SIGINT / Ctrl-C (OS default)
143Terminated — SIGTERM / kill (OS default)

ToolError::exit_code() keeps the code with the failure category. (clap is the one CLI here that natively uses the conventional 2 for usage errors.)

4. Tests with cargo test

Every module ships #[cfg(test)] mod tests in the same file (idiomatic Rust).

  • Never test subprocess or network for real — inject a fake impl of the boundary trait. That's what the trait is for.
  • Fakes are tiny structs (struct FakeRunner(String); with one method), not a mocking framework.
  • Pure logic and precedence are tested by direct call — no fakes at all.
  • Assert with assert_eq! / assert!; the Logger takes an injectable sink (io::sink()) so orchestration tests stay silent.

Coverage priority: pure logic and orchestration first, then config precedence, then arg parsing.

5. Quality gates

Four gates, all from the Rust toolchain — no extra installs:

cargo fmt --check                          # formatting: rustfmt
cargo clippy --all-targets -- -D warnings  # lint: warnings are errors
cargo build                                # compiles — the compiler IS the type gate
cargo test                                 # tests
  • Indentation is 4-space (rustfmt default). Like Go's tabs, this is the ecosystem standard — don't fight it; cargo fmt fixes layout.
  • Treat clippy warnings as errors (-D warnings) — it's the real lint gate, and it will suggest idioms (collapsed if let … && …, etc.).

Summary of what to produce

Faced with a Rust tool request, produce:

  1. A Cargo project copied from assets/template/: logic + boundaries in the library, a thin clap main.rs, deps pinned via cargo add (+ Cargo.lock), the §2 structure, the §3 CLI contract and exit codes, and the robustness rules the tool's job earns (shelling out earns the timeout + preflight; the network earns the HTTP boundary + timeout; holding config/secrets earns precedence + redaction). Delete what it doesn't earn.
  2. #[cfg(test)] mod tests covering pure logic, config precedence, orchestration (with fakes), and any boundary the tool ships.
  3. In one line: how to run it (cargo run -- <args>), and that the four gates — fmt + clippy + build + test — are green.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.