ghul.cli
0.19.2
Prefix Reserved
See the version list below for details.
dotnet tool install --global ghul.cli --version 0.19.2
dotnet new tool-manifest
dotnet tool install --local ghul.cli --version 0.19.2
#tool dotnet:?package=ghul.cli&version=0.19.2
nuke :add-package ghul.cli --version 0.19.2
ghūl CLI
Run a ghūl script directly, without a project file — on
Linux, including via a #! shebang line.
#!/usr/bin/env ghul
entry(args: string[]) is
IO.Std.write_line("hello, {if args.count > 0 then args[0] else "world" fi}");
si
chmod +x greet.ghul
./greet.ghul world
# hello, world
Or run it directly without the shebang:
ghul greet.ghul world
Commands
ghul [--no-cache] [--] <script> [args...] # run, if it looks runnable (see below)
ghul run [--no-cache] <script> [args...] # run unconditionally
ghul compile [--no-cache] <script.ghul> # compile and print the path to the result
ghul install-compiler [version] # install (or update) ghul.compiler
ghul cache clear # empty the compiled-script cache
ghul repl [--no-server] [--no-default-use] # an interactive session
ghul version # print ghul's and ghul.compiler's versions
With no verb, ghul only runs a file that looks like a script: one whose
name ends in .ghul, or one that is executable and starts with #! — the
same file a shell would already agree to run directly. Anything else is
refused, naming ghul run as the way to force it. ghul run runs the given
file regardless, which is also what a #!/usr/bin/env ghul shebang line
invokes. A file that happens to be named run, compile, cache,
install-compiler or version is reached with ghul -- <name>, the same
-- convention every other CLI uses to end option/verb parsing.
ghul compile compiles the script (installing the compiler first if
needed) and prints the path to the compiled binary on stdout, with nothing
else — install and compiler diagnostics stay on stderr, so the path is safe
to capture with $(...). It never runs the result.
ghul install-compiler installs ghul.compiler into ghul's own private
tool directory ahead of time, optionally pinned to a given version, so the
first real script run doesn't pay for it. Given no version it installs (or
updates to) the latest, so upgrading needs no version looked up; given a
version, it installs exactly that one, older or newer, and is a no-op when
that version is already there.
A - in place of <script> reads the source from standard input instead
of a file, for both running and compiling:
echo 'entry() is IO.Std.write_line("hi"); si' | ghul -
curl -fsSL https://example.com/greet.ghul | ghul -
--no-cache, given before the script (or before the verb, for run and
compile), forces a fresh compile even if a matching cache entry already
exists — useful if a cached result ever looks wrong and a rebuild is wanted
without reaching for ghul cache clear first. ghul cache clear empties
the whole compiled-script cache outright.
The REPL
ghul repl starts an interactive session. What you type is compiled and
run once it is finished, and what it declares stays available to
everything you type afterwards:
> let names mut = LIST[string]()
| names.add("first")
> for name in ["second", "third"] do
| names.add(name)
| od
| names
[first, second, third]
A line that finishes an expression or a call submits what you have typed and shows the value, so a short program can be written as one submission ending on the value it is for:
> let x = 123
| let y = 2
| x * y
246
A line that finishes a let, an assignment or a definition does not end
the submission, since those set something up for what follows: the |
prompt stays, and the next line joins the same submission. Nor does an
if, case, loop or try written over several lines when it closes. A
line that leaves something open - a block with no closing keyword, an
open bracket, an operator with nothing after it - waits for more too. A
blank line submits whatever is there, and a line holding only . does
the same. Text that can never be finished is submitted as soon as every
block it opens is closed, so its errors are reported then.
A submission that ends on a value shows it, so names above needs no
write_line. A submission that ends on a statement shows nothing, and
neither does one ended by a blank line after a construct over several
lines, since such a construct is usually there for what it does. End
it with . instead to see its value:
> if names.count > 2 then
| "several"
| else
| "few"
| fi
| .
several
Read from a pipe or a file rather than typed at a terminal, a construct
over several lines ends the submission when it closes, and a . line
ends one too.
At a terminal each line starts indented four spaces further in after a
line that opens a block - one ending in is, then, else, do, try
or =>, a case or catch line, or an open bracket - and a line
starting with si, fi, od, esac, yrt, else, elif, when,
catch or finally steps back out as the word is typed. Backspace in a
line's indent removes a whole step, and leaves the line's indent to you
from then on.
What you type is coloured as you type it, in the colours ghul.dev and the
playground use: VS Code's Dark+ on a dark background and Light+ on a light
one. The session asks the terminal for its background colour, falls back
to COLORFGBG, and uses the dark colours when neither says. ghul repl --theme dark, --theme light or --theme none chooses for it. There
is no colour when NO_COLOR is set, and none when the input or output is
not a terminal. COLORTERM and TERM decide between 24-bit colour, the
256-colour palette and the standard sixteen.
The compiler's default imports (use default) are in force in every
submission, so write_line, the pipes and the collections need no use.
ghul repl --no-default-use leaves them out. A use you type stays in
force for every later submission, the same as a definition does:
> use IO.Path.combine
> combine("a", "b")
a/b
Messages name a submission by its label, cell-3 for the third. Its name
in code is cell3, which is what cell3.x reaches an earlier submission's
x through.
Redefining something replaces it going forward, rather than editing what has
already run: a later cell sees the new one, and code compiled earlier keeps
the behaviour it was compiled against. Redefining at a new type is allowed,
so let x = 41 followed later by let x = "now a string" is fine.
A redefinition can read the value it replaces: after let x = 10, y = 20,
let x = x + y makes x 30.
At a terminal the whole submission is edited in place, however many lines
it has. Up and Down move between its lines, and past the first or last line
bring back earlier submissions, each as a whole, including those of earlier
sessions: the last thousand are kept in $XDG_STATE_HOME/ghul-cli/history
(~/.local/state/ghul-cli/history when XDG_STATE_HOME is not set). Enter on the last line
submits or waits for more as described above; Enter on an earlier line, or
Alt-Enter anywhere, starts a new line there. Backspace at the start of a line
joins it to the one above. Home and End (or Ctrl-A and Ctrl-E) go to either
end of a line, Tab completes the name being typed from everything the session
has defined, and Ctrl-D in an empty one leaves. Ctrl-C at the prompt does
nothing; while a submission is running, Ctrl-C interrupts it and brings the
prompt back with the session intact. .NET cannot stop a running thread from
outside, so an interrupted submission is abandoned rather than ended: it
keeps its definitions, and may go on running in the background, using a
core or writing output, until it finishes or the session ends. A block pasted in is taken as it is, with its own indentation and any
blank lines in it. A mistake inside an if, a loop or a definition is
reported once the block is closed rather than as soon as it is typed, so the
closing lines stay part of the same submission.
Input that is not a terminal, such as a script piped in, is read a line at a time as before.
:help lists the commands, :reset starts a fresh session, and :quit
leaves. :complete TEXT lists what could follow TEXT, and :hover TEXT
says what the end of TEXT names, both taking in everything the session has
defined so far:
> let answer = 41
> :complete ans
answer
> :hover answer
answer: int
They start a compiler in analysis mode the first time either is used, which
takes a second or two; after that an answer takes tens of milliseconds, and
a little longer straight after a submission, which the analyser has to take
in first. They need ghul.compiler 59.9.0 or newer, and say they are not
available with an older one.
The session keeps one compiler running for its whole length, started
before the first prompt so that it warms up while you type. The first
submission takes most of a second; after that a submission takes a few tens
of milliseconds from Enter to its answer, including working out whether the
line finishes a submission. ghul repl --no-server starts the compiler
afresh for each submission instead, about a second each, and the session
does the same by itself, saying so once, if the running compiler fails or
stops answering.
Each submission is compiled as its own small library and loaded into the session, so two things are true of this version:
- A name beginning with
_is private to the submission that declares it: each submission is its own assembly, and such a name does not leave one. - A trait declared in one submission can be implemented in a later one, but not where the trait declares a property.
Each of those is this version of the REPL rather than something about the
language, and each is lifted by a later one. The session needs
ghul.compiler 59.8.0 or newer, and says so if an older one is installed.
The session itself - the accepted cells, the import prelude each new
submission needs, and what a compiled cell exports - is published
separately as ghul.repl, so a browser page or a notebook kernel can host
the same session over whatever compiles for it. It compiles nothing and
runs nothing itself; see repl/README.md. What this tool puts around it -
compiling cells with the installed compiler, the compile server and its
fallback, loading and running what they produce - is published as
ghul.repl.host, for another host running cells in its own process; see
host/README.md.
Installing
dotnet tool install -g ghul.cli
On first use, ghul installs its own private copy of ghul.compiler (into
~/.local/share/ghul-cli/tools) if one isn't already there — nothing else
to set up first.
How it works
Each script is compiled once per script-content-and-compiler-version pair
and the result cached under ~/.cache/ghul-cli/scripts; a later run of an
unchanged script skips straight to running the cached build. Everything
after the script path is passed through unchanged as the running program's
own command-line arguments, and its exit code becomes ghul's own.
Installing the compiler and compiling a given script are each protected by a file lock, so running several scripts (or the same new one) at once doesn't race two installs or two compiles of the same content against each other — one does the work and the rest wait for it, rather than one of them losing a half-written result to the other.
There are no reference-assembly options: ghul relies entirely on what
ghul.compiler resolves on its own, and doesn't try to add anything it
wouldn't already find.
Building from source
dotnet tool restore
dotnet build
dotnet test unit-tests
./tests/smoke.sh
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.37.0 | 0 | 9/19/2026 |
| 0.36.1 | 0 | 9/19/2026 |
| 0.36.0 | 0 | 9/19/2026 |
| 0.35.0 | 0 | 9/19/2026 |
| 0.34.0 | 0 | 9/19/2026 |
| 0.33.1 | 0 | 9/19/2026 |
| 0.33.0 | 0 | 9/19/2026 |
| 0.32.0 | 0 | 9/19/2026 |
| 0.31.2 | 0 | 9/19/2026 |
| 0.31.1 | 0 | 9/19/2026 |
| 0.31.0 | 0 | 9/19/2026 |
| 0.30.0 | 0 | 9/19/2026 |
| 0.29.0 | 3 | 9/19/2026 |
| 0.28.0 | 25 | 9/19/2026 |
| 0.27.2 | 27 | 9/19/2026 |
| 0.27.1 | 31 | 9/19/2026 |
| 0.27.0 | 31 | 9/19/2026 |
| 0.26.1 | 27 | 9/19/2026 |
| 0.26.0 | 28 | 9/19/2026 |
| 0.19.2 | 36 | 9/19/2026 |