# Shellcheck for Cram tests

**URL:** https://discuss.ocaml.org/t/shellcheck-for-cram-tests/18551
**Category:** Ecosystem
**Tags:** testing, dune, cram
**Created:** [September 22, 2026, 8:15am UTC](https://discuss.ocaml.org/t/shellcheck-for-cram-tests/18551 "2026-09-22T08:15:32Z")
**Posts on this page:** 2
**Page:** 1

<div class="post-metadata">

### Author: ![amaro](https://avatars.discourse-cdn.com/v4/letter/a/9fc348/32.png) [@amaro](https://discuss.ocaml.org/u/amaro)
#### Post date: [September 22, 2026, 8:15am UTC](https://discuss.ocaml.org/t/shellcheck-for-cram-tests/18551/1 "2026-09-22T08:15:32Z")

</div>

When writing Cram tests, we end up making some mistakes, and using Shellcheck would allow us to avoid some of them.

However, the format of `run.t` files does not currently allow running it “as is”. I tried doing some simple grepping, e.g.:

```
cat run.t | grep ' [$]' | sed 's/ [$]//' | shellcheck - 

```

But it does not work when using continuation lines:

```auto
  $ if [-f foo.txt]; then
  > echo "file exists"; fi

```

I could craft a simple script to parse these lines, but I wonder if there’s a simpler integrated solution: either a way for Dune to output the list of commands to be run (so I could pass it to `shellcheck`), or some sort of “Cram linter”?

---

<div class="post-metadata">

### Author: ![shonfeder](https://sea2.discourse-cdn.com/flex020/user_avatar/discuss.ocaml.org/shonfeder/32/424_2.png) [@shonfeder](https://discuss.ocaml.org/u/shonfeder)
#### Post date: [September 22, 2026, 5:58pm UTC](https://discuss.ocaml.org/t/shellcheck-for-cram-tests/18551/2 "2026-09-22T17:58:44Z")

</div>

Hi 🙂

A linting script would work, and should indeed be easy to formulate with a bit of awk.

But you could also use a hack like the following: Assuming you have shellcheck as a binary dependency for your cram tests,

```lisp
(cram
 (deps
  %{bin:shellcheck}))

```

then use a helper function to run invocations thru shellcheck before executing them

```auto
  $ checked_sh() (
  > script=$(cat) || exit
  > printf '%s\n' "$script" | shellcheck --shell=sh - &&
  > printf '%s\n' "$script" | sh
  > )

  $ cat <<'SH' | checked_sh
  > name='world'
  > bar='unused'
  > printf 'Hello, %s!\n' "$name"
  > SH

```

which would give errors like

```auto
------ example/hello_world.t/run.t
++++++ example/hello_world.t/run.t.corrected
File "example/hello_world.t/run.t", line 12, characters 0-1:
 | $ checked_sh() (
 | > script=$(cat) || exit
 | > printf '%s\n' "$script" | shellcheck --shell=sh - &&
 | > printf '%s\n' "$script" | sh
 | > )
 |
 | $ cat <<'SH' | checked_sh
 | > name='world'
 | > bar='unused'
 | > printf 'Hello, %s!\n' "$name"
 | > SH
+|
+| In - line 2:
+| bar='unused'
+| ^-^ SC2034 (warning): bar appears unused. Verify use (or export if used externally).
+|
+| For more information:
+| https://www.shellcheck.net/wiki/SC2034 -- bar appears unused. Verify use (o...
+| [1]

```

Neither option is super clean, tho, is it?

You might consider opening an issues on [Issues · ocaml/dune · GitHub](https://github.com/ocaml/dune/issues) to propose some sort of support for configuring a linting check (or a more general preprocessor?) be added to the stanza for [Cram - Dune documentation](https://dune.readthedocs.io/en/stable/reference/dune/cram.html) . Core devs may then be able to opine other ways of addressing this and on the likelihood of such a feature request.

In dune itself, stuff that needs to go thru shell check live in its own .sh scripts, which we will then source and invoke via the provided functions. Here is an example: [dune/test/blackbox-tests/dune at main · ocaml/dune · GitHub](https://github.com/ocaml/dune/blob/main/test/blackbox-tests/dune#L13-L18) check [dune/test/blackbox-tests/setup-script.sh at main · ocaml/dune · GitHub](https://github.com/ocaml/dune/blob/main/test/blackbox-tests/setup-script.sh) , which is then included as a `setup_script` in [dune/test/blackbox-tests/test-cases/dune at main · ocaml/dune · GitHub](https://github.com/ocaml/dune/blob/main/test/blackbox-tests/test-cases/dune#L42) . This is clean, but you lose the transparency that should (IMO) be the core purpose of cram tests.
