scripths
GHCi scripts for standalone execution and Markdown documentation.
| Stackage Nightly 2026-07-22: | 0.5.3.1 |
| Latest on Hackage: | 0.5.3.1 |
scripths-0.5.3.1@sha256:c8b727c511ce073fd0de5dc278cd46a55e31c0ccca65c8ef229fdff3fdd45f52,2654Module documentation for 0.5.3.1
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
.ghciexecution — 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 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. - 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.Parserrecognises the directive (scriptCompileonScriptFile) and offersparseScriptNumbered, a line-number-preserving parse.ScriptHs.Compiledvalidates 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 directoryinstead of a raw exception; a failing.ghciscript’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: Xin.ghci/.hsscripts,<!-- scripths: X -->in.md/.markdownnotebooks. 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-placerun 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/-vprints 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
OverloadedStringson by default in every scripths repl, so string literals work directly asText/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-timesetCurrentDirectory, so splices that read files at compile time resolve./relativepaths against your tree instead of the throwaway project dir. Only added when the notebook actually uses TH. - In-place notebooks:
-i/--in-placerewrites a notebook over itself, stripping and replacing previously rendered output.reassembleis 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 — frompackages:,--package, or the enclosing project — are now added to the syntheticbuild-dependsautomatically, so their modules import without a separatebuild-dependsline. - Output marker renamed from
sabela:mimetoscripths: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-dependsnames it (zero-config;--no-local-projectto disable), and a repeatable--package DIR/-p DIRflag 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 generatedcabal.project. CabalMetagainsmetaSourceReposandmetaUnknownKeys; unrecognised-- cabal:keys now warn to stderr (instead of being silently dropped).- The generated
cabal.projectis regenerated each run (guarded by a-- managed by scripthssentinel 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.