scripths

GHCi scripts for standalone execution and Markdown documentation.

https://www.datahaskell.org/

Stackage Nightly 2026-07-22:0.5.3.1
Latest on Hackage:0.5.3.1

See all snapshots scripths appears in

MIT licensed by DataHaskell
Maintained by [email protected]
This version can be pinned in stack with:scripths-0.5.3.1@sha256:c8b727c511ce073fd0de5dc278cd46a55e31c0ccca65c8ef229fdff3fdd45f52,2654
Used by 1 package in nightly-2026-07-22(full list with versions):

scripths

GHCi scripts for standalone execution and Markdown documentation.

scripths lets you write .ghci scripts with dependency management and run them as standalone programs — or embed executable Haskell code blocks in Markdown files and evaluate them notebook-style with captured output.

Features

  • Standalone .ghci execution — Run GHCi scripts directly from the command line, with automatic dependency.
  • Compiled or intepreted - you can either compile or interpret cells. Heavier functions can be compiled so they move fast and interactive parts can be kept interpreted.
  • Cabal metadata directives — Declare build-depends, default-extensions, ghc-options, and local packages inline using -- cabal: comments.
  • Markdown notebooks — Execute Haskell code blocks inside Markdown files and render the output back into the document as block quotes. Re-run in place with -i — output is replaced cleanly, with no accumulating blank lines.
  • Inline errors — A block that fails to compile renders its GHC error beneath the block instead of producing silent empty output.
  • Smart GHCi rendering — Multi-line definitions are automatically wrapped in :{/:} blocks, and IO binds / Template Haskell splices are handled correctly as individual statements.
  • Compile-time TH reads your tree — A splice that reads a file at compile time (e.g. $(declareTable "./data/x.db" …)) resolves ./relative paths against the directory you ran scripths from, not the internal build directory.
  • Version tag — Files record the scripths that wrote them on their first line; running a file authored by a newer scripths than your binary prints a warning rather than failing.

Installation

cabal install scripths

CLI Usage

scripths [-o FILE | --output=FILE] [-i | --in-place] [-p DIR | --package DIR]... [--no-local-project] [-h | --help] [-v | --version] <script>

When -o / --output is provided for Markdown files, the result is written to that path. Otherwise it is printed to stdout. With -i / --in-place the notebook is rewritten in place: any previously rendered output is stripped and replaced, and re-running is idempotent (it does not accumulate blank lines). -i is only valid for .md / .markdown notebooks and cannot be combined with -o.

The file extension determines the mode. .ghci / hs files are parsed and executed as a standalone GHCi script. .md / .markdown files are processed as a notebook with captured output. Run scripths --help for the full option and directive list, or scripths --version to print the version.

Version tag

Each file can carry a first-line tag recording the scripths version that authored it, so you can tell whether your binary is current enough to parse it. The spelling matches each file type’s comment syntax:

-- scripths: 0.4.1.0          # .ghci / .hs scripts
<!-- scripths: 0.4.1.0 -->    # .md / .markdown notebooks (invisible when rendered)

When scripths writes a notebook (-o / --in-place) it stamps or refreshes this tag at the top of the document; output streamed to stdout is left untouched. When you run a file whose tag declares a newer scripths than your binary, scripths warns on stderr and proceeds anyway (the syntax may not fully parse). The tag is recognised after a leading #! shebang (scripts) or YAML frontmatter block (notebooks).

Requires GHC and cabal-install on your PATH.

Quick Start

Running a GHCi script

Create a file example.ghci:

double :: Int -> Int
double = (*2)

-- cabal: build-depends: text
:set -XOverloadedStrings
import qualified Data.Text as T

T.take 10 "hello"

double 5

Run it:

scripths example.ghci

Running a Markdown notebook

Create a file notebook.md:

# My Analysis

Some introductory prose.

```haskell
print (5 + 5)
```

Define some values:

```haskell
x = 10
y = 20
```

We can then add the values in the next block and
the output is embeded below.

```haskell
x + y
```

Run it and write the results to a new file:

scripths -o output.md notebook.md

Each Haskell code block is evaluated in order, and its output is inserted into the Markdown as a block quote beneath the code fence (tagged with a <!-- scripths:mime … --> marker so a later run can find and replace it). If a block fails to compile, its GHC error is rendered inline beneath that block rather than vanishing.

Tip: each code block should end in a single bare expression so it is auto-printed. A block that ends in a <- bind (or several ;-joined statements) prints nothing — repeat the bound name on a final line to print it:

```haskell
x <- pure 42
x
```

Cabal Metadata Directives

You can declare dependencies, language extensions, and GHC options directly inside your scripts using -- cabal: comments:

-- cabal: build-depends: text, containers
-- cabal: default-extensions: OverloadedStrings, TypeApplications
-- cabal: ghc-options: -Wall
-- cabal: packages: ../sibling-pkg
-- cabal: source-repository-package: https://github.com/owner/repo v1.2.3

The recognised keys are build-depends, default-extensions, ghc-options, packages (extra local package directories, relative to the script), and source-repository-package. An unrecognised key is reported as a warning.

OverloadedStrings is enabled in every scripths repl by default, so string literals work directly as Text / ByteString; add any further extensions with default-extensions.

The -- compile directive

A notebook cell (or script) can be marked compiled:

-- compile

or, naming the generated module explicitly:

-- compile: Training.Core

The directive declares that the cell’s contents are module-level declarations destined for a generated Haskell module rather than the GHCi prompt — a host environment (such as Sabela) writes those declarations into a module, loads it with :load under -fobject-code -O2, and gets native compiled code instead of interpreted bytecode. Cells sharing a module name merge into one module; bare -- compile cells share a default module.

What scripths provides:

  • ScriptHs.Parser recognises the directive (scriptCompile on ScriptFile) and offers parseScriptNumbered, a line-number-preserving parse.
  • ScriptHs.Compiled validates that a cell is declarations-only (checkCompilable) and renders the generated module (renderCompiledModule), preceding every unit with a {-# LINE n "sabela-cell-<id>" #-} pragma so GHC diagnostics map back to the originating cell and line (linePragmaTag / parseLinePragmaTag).

Under the scripths CLI the directive is currently a graceful no-op: it parses as an ordinary comment and the cell runs interpreted, producing the same results (just slower). Standalone compiled execution in the CLI may come later.

Local packages

By default build-depends resolve from Hackage. To build a script or notebook against a local checkout instead — e.g. to document a library version that only exists on a branch — there are two mechanisms.

The enclosing project (zero-config)

If a script/notebook lives inside a cabal project and a build-depends names that project’s package (or one of its sub-libraries, pkg:sublib), scripths builds against the local working tree automatically — so documenting your own repo just works:

scripths -o docs/MANUAL.md docs/base/MANUAL.md

Pass --no-local-project to suppress this and resolve every build-depends from Hackage instead — e.g. to verify the doc’s examples against the released version of your package rather than the working tree. Explicit --package dirs are unaffected.

--package DIR

Point at another local checkout (a sibling package, or a sub-library project). DIR must be a package root (contain a .cabal); it is resolved to an absolute path at the invocation site, so nothing machine-specific is committed to the shared document. Repeatable.

scripths --package ../granite -o out.md in.md

The package’s name is read from its .cabal and added to the script’s build-depends automatically, so its modules are importable without also listing it in a -- cabal: build-depends: line.

packages: directive (committed into the document)

To depend on one or more local packages from inside the document itself — for example a second, non-enclosing package in the same repo — list their directories (relative to the script) with a packages directive:

-- cabal: packages: ../sibling-pkg, ../another-local

Like --package, each named package’s name is added to build-depends automatically. Unlike --package, the dependency travels with the document. Paths are resolved relative to the script file.

Pinned git packages (source-repository-package)

To pin a pushed commit/tag reproducibly (and commit the pin into the shared document), declare a source-repository-package directive as <location> <ref> [subdir]:

-- cabal: source-repository-package: https://github.com/owner/repo v1.2.3
-- cabal: source-repository-package: https://github.com/owner/mono abc123 sub/dir

scripths writes these as source-repository-package stanzas in the generated cabal.project. Use a local checkout (above) for an unpushed branch, and a git pin for a pushed, reproducible reference.

Changes

Revision history for scripths

0.5.3.1 – 2026-06-28

  • Merge consecutive blank lines.

0.5.2.0 – 2026-06-16

  • Styling with RenderOptions. You can now have the output quoted vs unquoted and the code shown or not (code not shown is if you want to export the results only). Thanks to @tchoutri
  • Blank-line fix: whitespace-only prose between two code fences no longer becomes a spurious empty prose segment.

0.4.1.0 – 2056-05-31

  • Custom-prelude support: auto print (our hook to rout evething but strings to shw) now doesn’t assume a prelude.
  • Cleaner error rendering (notebooks): don’t leak internals when printing errors.
  • UTF-8 output: captured notebook output is decoded as UTF-8.
  • Friendlier errors: a missing input file reports No such file or directory instead of a raw exception; a failing .ghci script’s stderr is scrubbed too; cell-end markers carry a per-run nonce so cell output can’t spoof a boundary.
  • Version tag: files may carry a first-line tag recording the scripths that wrote them — -- scripths: X in .ghci/.hs scripts, <!-- scripths: X --> in .md/.markdown notebooks. Running a file that declares a newer scripths than the binary prints a warning and continues. scripths stamps/refreshes the tag when it writes a notebook (-o/--in-place); stdout output is left unstamped. The tag is recognised after a leading #! shebang (scripts) or YAML frontmatter block (notebooks), tolerating a BOM, CRLF, and indentation. Behaviour change: the first --in-place run on an existing notebook inserts the <!-- scripths: X --> line (and normalises leading blank lines), a one-time visible diff in tracked files; it is invisible when rendered and idempotent thereafter.
  • scripths --version / -v prints the scripths version.

0.4.0.1 – 2026-05-30

  • Parse pandoc markdown code fences as haskel code fences.

0.4.0.0 – 2026-05-29

  • OverloadedStrings on by default in every scripths repl, so string literals work directly as Text / ByteString.
  • Compile-time Template Haskell now sees your working directory: notebooks using TH (e.g. $(declareTable "./data/x.db" …)) have their body bracketed with a compile-time setCurrentDirectory, so splices that read files at compile time resolve ./relative paths against your tree instead of the throwaway project dir. Only added when the notebook actually uses TH.
  • In-place notebooks: -i / --in-place rewrites a notebook over itself, stripping and replacing previously rendered output. reassemble is now idempotent — re-running no longer accumulates blank lines around code fences.
  • Inline block errors: a code block that fails to compile now renders its GHC diagnostic beneath the block (stdout and stderr are captured as one ordered stream) instead of producing silent empty output.
  • Second local package: new -- cabal: packages: <dir>, … directive for depending on non-enclosing local packages (relative to the script). Local package names — from packages:, --package, or the enclosing project — are now added to the synthetic build-depends automatically, so their modules import without a separate build-depends line.
  • Output marker renamed from sabela:mime to scripths:mime. The legacy marker is still recognised when re-parsing older notebooks.

0.3.3.0 – 2026-05-24

  • Build against local packages: auto-include the enclosing cabal project when a build-depends names it (zero-config; --no-local-project to disable), and a repeatable --package DIR / -p DIR flag for sibling/sub-library checkouts. Lets documentation build against an unreleased local working tree.
  • New -- cabal: source-repository-package: <location> <ref> [subdir] directive for pinning a pushed git commit/tag reproducibly; rendered into the generated cabal.project.
  • CabalMeta gains metaSourceRepos and metaUnknownKeys; unrecognised -- cabal: keys now warn to stderr (instead of being silently dropped).
  • The generated cabal.project is regenerated each run (guarded by a -- managed by scripths sentinel so hand-edited project files are preserved), replacing the old write-once behaviour. Rewrote the CLI argument parser.

0.3.1.0 – 2026-04-23

  • Better block parsing when there are function signatures and comments.

0.3.0.1 – 2026-03-14

  • improve block parsing

0.2.0.2 – 2026-03-14

  • Stop adding extra white space to code cells.

0.2.0.1 – 2026-03-01

  • Fix issue with SVG mime type.

0.2.0.0 – 2026-03-01

  • Add mime type to output.

  • Make blocks out of single lines of code as well.

0.1.0.2 – 2026-02-28

  • Make blocks out of single lines of code as well.

0.1.0.1 – 2026-02-24

  • Fix code new ine behaviour for code fencing.

0.1.0.0 – YYYY-mm-dd

  • First version. Released on an unsuspecting world.