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.
- Standalone
.ghciexecution — Run GHCi scripts directly from the command line, with automatic dependency. - Cabal metadata directives — Declare
build-depends,default-extensions,ghc-options, and localpackagesinline 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./relativepaths against the directory you ranscripthsfrom, not the internal build directory.
cabal install scripthsscripths [-o FILE | --output=FILE] [-i | --in-place] [-p DIR | --package DIR]... [--no-local-project] [-h | --help] <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.
Requires GHC and cabal-install on your PATH.
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 5Run it:
scripths example.ghciCreate 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.mdEach 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 ```
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.
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.
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.mdPass --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.
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.mdThe 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.
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.
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.