Skip to content

DataHaskell/scripths

Repository files navigation

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.

About

GHCi scripts for standalone execution and Markdown documentation.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors