A Shell is Born [a-shell-is-born]
- June 17, 2026
- Patrick Ferris
Merry passed a milestone this week after installing the following packages vim fakeroot fakechroot debootstrap and successfully performing a fakechroot debootstrap.
To me, this signals that Merry is closer than ever to being an actual POSIX-ish shell. This is exciting because we now have a completely customisable, OCaml implementation of a shell that can run a significant number of existing shell scripts in the world. Let the hacking commence!
Installation
- June 17, 2026
- Patrick Ferris
You can install the binary, msh, today:
opam pin git+https://git.sirref.org/merry
eval $(opam env)
msh
What's next?
- June 17, 2026
- Patrick Ferris
There are a few avenues to try next. In the short-term, more testing of the shell is probably worthwhile. In particular, trying to boot an Alpine Linux image with msh as the default shell would be good.
Integrating the various components of Shelter is also on the list. A time-travelling POSIX shell is within reach; the actual semantics still need to be ironed out.
I know for certain that some shell options and built-ins will be the first issues to arise going forward.
As a fun experiment, Anil's vibecoded OCaml busybox translation could be plumbed into msh. This would allow a very versatile, but zero-forking shell! Throw in an in-memory Eio filesystem into the mix and the whole thing becomes in-memory... mirageSH.
Weeknotes
- June 17, 2026
- Patrick Ferris
Below are a collection of weeknotes for Merry.
Shell Benchmarks, Back to Writing [weekly-2026-w26]
- July 4, 2026
- Patrick Ferris
A little late due to travel, but I did a little benchmarking of Merry.
Signal Handling in Merry
- July 4, 2026
- Patrick Ferris
In order to even run shellbench, I first had to fix some bugs in the shell implementation. Shellbench aims to warm up the shell before starting any benchmarks; it includes this prelude before getting started:
__ready=
trap __ready=1 HUP
kill -HUP "$MAIN_PID"
until ["$__ready" ]; do __dummy=; done
Whilst writing Merry, I hadn't really considered that the return context from a trap needed to be taken into consideration! I had assumed they would be silently ignored, how else would we know where to thread that context back into the shell's execution.
Even after reading through the specification for trap I am still none the wiser. I've opted to place some sensible signal polling points during the execution of the commands to run any signals that have come in. The single handlers in Merry simply push thunks onto a run queue. Looking at the Dash source code, it seems to be something that happens just before evaluation.
Benchmarking Merry [merry-benchmarks]
- July 4, 2026
- Patrick Ferris
Medium-to-small potatoes
- July 4, 2026
- Patrick Ferris
I used shellbench to understand where some of the performance pitfalls were coming from.
$ ./shellbench -s bash,zsh,msh sample/func.sh
-----------------------------------------------------------------------------------
name bash zsh msh
-----------------------------------------------------------------------------------
func.sh: no func 775,539 1,124,485 5,229
func.sh: func 398,861 197,169 93,424
-----------------------------------------------------------------------------------
* count: number of executions per second
This particular benchmark first runs nothing (i.e. :) and then a function lookup and execution. Surprisingly msh is slower at doing nothing! I perf-ed a run of msh test.sh with just a single : in the file:
Test built-in
- July 4, 2026
- Patrick Ferris
After making various improvements to Merry that almost halved the memory usage (e.g., removing extraneous string manipulations, adding a fast-path to the PATH variable for resolving programs). I was still pretty far off the performance of these other shells. I decided to have a look at dash which has a simple implementation.
This flame graph is for the following shell script:
f () { :; }
while __count=$(($__count+1)) && ["$__count" -le 10000 ]; do
f
done
echo "count is $__count"
And here are some rudimentary benchmarks:
$ time -p dash test.sh
count is 10001
real 0.01
user 0.01
sys 0.00
$ time -p msh test.sh
count is 10001
real 15.53
user 4.28
sys 12.28
It turns out the massive slowdown is very simple to explain. In the dash flamegraph there is a oexpr stack; which comes from the bltin/test.c file. dash, along with the other shells, does not spawn a process for the [-test command! Forcing dash to do use the binary [ gives:
$ time -p dash test.sh
count is 10001
real 7.55
user 3.95
sys 3.44
Which is much more respectable in terms of performance compared to msh. Plenty of work to do though!
Parsing arithmetic expressions
- July 4, 2026
- Patrick Ferris
There was some obvious low-hanging fruit to help improve some of the performance differences. The first was pre-parsing arithmetic expressions. Morbig, the static parser, does not implement arithmetic expression parsing; so Merry does it for us. Before, it was on-demand, as we encountered an expression of the form $((<e>)). Now, it happens up front. This should improve tight-loops.
Writing
- July 4, 2026
- Patrick Ferris
More on this soon, but I have started writing two separate papers bringing all of this work together.
Three weeks in the Shell [weekly-2026-w25]
- June 16, 2026
- Patrick Ferris
The last three weeks have been focused work on building Merry. In practice, I wanted to run the following two commands through Merry's shell: msh:
apt-get install debootstrap -y debootstrap stable /stable-chroot http://deb.debian.org/debian/
Seemingly innocuous. But both commands run thousands of lines of shell script. Whether that's Debian package maintainer scripts or state-machines for processing package release files.
What follows is a handful of the bugs and the shell scripts that caused them.
Field Splitting
- June 16, 2026
- Patrick Ferris
Edge-cases abound in the POSIX shell specification; and field splitting is one of the most serious perpetrators. Field splitting happens after word expansion. It is a known footgun amongst shell script authors.
$ count () { echo "Count: $#"; }; FOO="hello world"; count $FOO
Count: 2
Quoting variables that need expanding will prevent field splitting from taking place, but it is an easy thing to forget.
Field splitting uses the IFS (internal field separator) environment variable to determine how to do the splitting, and it comes with some extra rules beyond "split on these characters".
$ IFS=" "; FOO=" hello world"; echo $FOO
hello world
$ IFS=" "; FOO=" hello world"; echo "$FOO"
hello world
$ IFS=":"; FOO="a:b:c:"; echo $FOO
a b c
$ IFS=":"; FOO="a:b::c:d"; echo $FOO
a b c d
$ IFS=":"; FOO=":a:b:c"; echo $FOO
a b c
$ IFS=; FOO="unset IFS"; echo $FOO
unset IFS
Variable assignment
- June 16, 2026
- Patrick Ferris
How a shell stores variables also impacts field splitting. Naively, Merry stored variables as strings, but really they should be stored as their expanded form.
Consider this example of saving a function output in a variable called base.
iter () {
echo "a"
echo "b"
echo "c"
}
base=$(iter)
for n in $base; do
echo "Got $n"
done
for n in "$base"; do
echo "Got $n"
done
Later, when we come to iterate across the fields of $base we wouldn't be able to reconstruct the intial fields that $(iter) produced if we simply saved the result as a string. Too much information would have been lost.
Redirections
- June 16, 2026
- Patrick Ferris
Redirections control what file descriptors point to. Each redirection has a lifetime associated with it. For example: cat < README.md really says "open README.md and execute the cat program mapping the process's FD 0 to the FD of README.md". When cat returns that redirection is no longer valid.
Other redirections are not so simple. Consider:
$ echo "!dlrow olleh" > input.txt
$ (read line; echo $line) < input.txt | rev
hello world!
There are lots of redirections here too. Firstly the standard input for everything inside the braced command (read line2; echo $line) should be the open file descriptor for input.txt. The standard output of the braced command should be the writing end of a pipe whose reading end is passed to rev as that process's standard input. The final standard output for rev should be unchanged from what it was originally (the shell's standard output).
Getting redirections right is hard, consider this snippet from the debootstrap scripts (all in the context of stdout and stderr pointing to a debootstrap.log file and file descriptor 4 pointing at the true stdout).
for m in $MIRRORS; do
pkgdest="$TARGET/$($DLDEST pkg "$s" "$c" "$a" "$m" "$path")"
if [! -e "$pkgdest" ]; then continue; fi
pkgs_to_get="$(download_debs "$m" "$pkgdest" $pkgs_to_get 5>&1 1>&6)"
if [ -z"$pkgs_to_get" ]; then break; fi
done 6>&1
More niche built-ins
- June 16, 2026
- Patrick Ferris
As the dust begins to settle on the nuts and bolts of implementing a POSIX-ish shell, new challenges are allowed to emerge from deep within scripts.
One such challenge is the seemingly endless (I know, they're finite) shell built-ins! Whilst trying to run debootstrap using its fakechroot variant, I bumped into getopts... a whole command-line parsing shell built-in.
An In-memory Filesystem for Eio [eio_mem]
- June 16, 2026
- Patrick Ferris
Towards the end of last week and the beginning of this week, I threw together a prototype for an in-memory filesystem for Eio: Eio_mem. Concretely, this provides a means for constructing an Eio.Fs.dir_ty Eio.Path.t value that is completely in-memory.
Thanks to Eio's capability-first approach, any existing code implemented against the _ Eio.Path.t API should just work with the in-memory filesystem.
There's a convenient Eio_mem.Fs.load function that can take a path from one filesystem (most likely a normal one from Eio_linux or Eio_posix) and replicate it into the in-memory filesystem. For example, you might have a static web-server pointing to a directory like "/var/www". The following might be the only change you need to make it completely in-memory (example from ocaml-cohttp):
let () = let port = ref 8080 in
Arg.parse
[ ("-p", Arg.Set_int port, " Listening port number(8080 by default)") ] ignore "An HTTP/1.1 server";
Eio_main.run @@ fun env ->
+ Eio_mem.run @@ fun menv ->
Eio.Switch.run @@ fun sw ->
- (* Restrict to current directory: *)
- let htdocs = Eio.Stdenv.cwd env in
+ (* Restrict to in-memory current directory: *)
+ Eio_mem.Fs.load ~filter (Eio.Stdenv.cwd env);
+ let htdocs = Eio.Stdenv.cwd menv in
let socket =
Eio.Net.listen env#net ~sw ~backlog:128 ~reuse_addr:true
(`Tcp (Eio.Net.Ipaddr.V4.loopback, !port))The keen-eyed reader might have noticed the filter parameter. This can be used to ignore certain paths (here I ignored .git and _build which were adding substantial time to loading the filesystem).
Using meio to debug Merry [weekly-2026-w23]
- June 4, 2026
- Patrick Ferris
During a large rewrite of Merry, I introduced a regression causing certain shell programs to hang indefinitely. I thought it might be a good opportunity to try out Meio.
First, let's look at a trace of Meio from some real-world programs. For example: meio -- msh -c "sleep 100".

One problem that comes up immediately from this example is that the tree hierarchy of tasks and cancellation contexts makes it tricky to reason about the logging. Here, we can see the pipeline's cancellation context clearly labelled pipeline-id1. This is an Eio.Switch.t for the pipeline sleep 100. The actual execution log for sleep 100 bubbles back up to the main fiber (that has no name, so it is called <task>). The daemon fiber, for ensuring the spawned process is reaped, is attached to the switch and is displayed below it.
Meio has a long way to go. Most programs are too fast to be monitored in this way. Using something like tokio-console's model might be a better idea here focusing less on the tree strucuture of the program, and more on the performance and monitoring of individual tasks. Without this, it turns out Meio was not all that useful for debugging this specific issue (which ended up being some bad pipe management on my part).
Shell forks revisited [weekly-2026-w17]
- April 28, 2026
- Patrick Ferris
Whilst I did get up to some other work last week, I decided to dedicate this weekly to fork-ing in the shell.
Must we fork?
- April 28, 2026
- Patrick Ferris
I have spent some time trying to figure out: can you implement a shell that never has to fork and run its own code (as opposed to fork+exec-ing, which is necessary)? There are, at least, two bits of POSIX shell semantics that make this hard: concurrency and redirects, particularly in the presence of shell built-ins.
Individual commands in a pipeline, according to the POSIX specification, will have their standard input and output connected up before other redirections.
The standard input, standard output, or both of a command shall be considered to be assigned by the pipeline before any redirection specified by redirection operators that are part of the command.
Additionally:
...each command of a multi-command pipeline is in a subshell environment; as an extension, however, any or all commands in a pipeline may be executed in the current environment.
Where a subshell is defined as:
Subshell: A shell execution environment, distinguished from the main or current shell execution environment.
We will note that subshell \neq child process. In practice, however, the two are used fairly interchangeably given the semantics of spawning a child process. Perhaps the two most crucial aspects of a given command in a subshell are that:
It is run in parallel with the other commands.
It should be reentrant.
In what follows we will assume that we are discussing a multi-command pipeline p.
\texttt {p} = \texttt {cmd}_1 | ... | \texttt {cmd}_n \text { where } n \geq 2
Pipeline parallelism
- April 28, 2026
- Patrick Ferris
The POSIX specification does not mandate parallelism explicitly for commands in a pipeline. However, in practice the semantics of a pipeline necessitate concurrency at the very least.
Suppose we have \texttt {cmd}_1 | \texttt {cmd}_2 where \texttt {cmd}_1 produces an infinite amount of output (e.g., cat /dev/urandom) and \texttt {cmd}_2 consumes a finite amount of input (e.g., hxd-xxd -l100). What is the result of running this pipeline with the default POSIX shell options?
$ dash -c "cat /dev/urandom | hxd.xxd -l100"
00000000: 5033 7981 ae44 d475 074c 5361 0a9e 3828 P3y..D.u.LSa..8(
00000010: f88c 1f9e 3b0e bcde 528a f78c b1e4 7195 ....;...R.....q.
00000020: 16f2 8325 c90b 9310 539c af04 8289 c3a0 ...%....S.......
00000030: 57c5 680a 2c81 a999 71b6 708e 95df e720 W.h.,...q.p....
00000040: 004c 407a 8365 9448 65c6 b435 bed2 4084 .L@z.e.He..5..@.
00000050: 62ea 96ab 2b23 0385 f9d2 fc30 d090 1719 b...+#.....0....
00000060: 68d1 c514 h...
cat: write error: Broken pipe
There are three factors that influence the pipeline's execution behaviour (i.e. what is done about the commands that are run and the exit status we return).
pipefail: Withpipefailset tofalse(the default), the exit status of the pipeline is entirely decided upon by the exit status of the rightmost command (above \texttt {cmd}_2). Withpipefail, the pipe will exit with 0 if all commands exit with 0; otherwise the non-zero exit status of the rightmost command is taken.!: If the pipeline begins with!then this inverts the exit code in the way you might expect.All commands must terminate.
Whilst in theory \texttt {cmd}_1 does not terminate, the termination of \texttt {cmd}_2 closes the reading end of the pipe resulting in the Broken pipe system error.
Let's replace \texttt {cmd}_1 with the following compound command that also produces, left to its own devices, infinite output on stdout.
while echo "some bytes"; do
:
done | hxd.xxd -l100
In this example, we no longer have the luxury of being able to fork and exec. The compound command (the while loop) forces our hand -- we must rely on the internal execution logic of the shell to keep it going. How then do we prevent \texttt {cmd}_1 from blocking our shell from spawning \texttt {cmd}_2?
For most shells, this is where they might fork themselves and allow the while loop to execute in the child. The parent continuing on. Alternatively, the work could continue on in a separate thread (in OCaml parlance, domain). Fibers (green threads, coroutines etc.) are only possible if there are a sufficient number of co-operative yields (which can be hard to track: any control flow or shell built-in would need to ensure they periodically yield to the other commands in the pipeline).
With that being said, it would seem we have workarounds to ensure the parallel aspects of pipelines are possible to build without forking the shell state itself.
Command Reentrancy
- April 28, 2026
- Patrick Ferris
Command reentrancy deals with the fact that commands in a pipeline run in subshells. In practice, this means any \texttt {cmd}_i should not impact the execution environment of any other \texttt {cmd}_j. Commands may certainly interfere with one another: a rogue rm might remove a file that some other command was about to read or was in the middle of reading!
\S 2.13 of the POSIX shell specification describes the execution environment. In Merry we lift this into the ctx type making it completely immutable; the only way to update the context is to destructively declare new fields, e.g., { ctx with cwd = "/home/bactrian" }. All shell built-ins work directly from the context as opposed to any state that might also be kept by the process itself (e.g., we do not call getcwd(3) to expand PWD, but instead read it from the context).
Most state is handled nicely by this functional implementation of the shell; except open (or not) file descriptors. The exec built-in is particularly thorny in this regard:
If exec is specified with no operands, any redirections associated with the exec command shall be made in the current shell execution environment.
So, exec 3>&1 says "in the current execution environment map file descriptor 3 to stdout" and exec > /dev/null says "in the current execution environment map stdout to /dev/null. This is often used to silence anything writing to stdout.
info () {
echo "INFO: $1" >&3
}
exec 3>&1 > /dev/null
info "Starting up server..."
echo "Some noisy program spamming STDOUT"
info "All is quiet..."
If we are not running shell built-ins inside new child processes, then we must lift the file descriptor table into the context as well and disallow any two built-ins to run in parallel. Consider this example:
exec < ./hello.txt
while read line; do
echo "Got $line" >&2
done | (exec < ./world.txt; cat)
If we allow exec to change the set of open file descriptors of the shell process directly, then individual commands in the pipeline are no longer re-entrant. The pipeline here is not being used to pipe bytes from one command to another, but instead to expose concurrency and the subshell behaviour.
The second exec < ./world.txt has no impact on the currently opened file on stdin, the while loop continues to read ./hello.txt. This is straightforward to implement with fork as you can dup2 any files you need. The bookkeeping and isolation is all provided by the POSIX process semantics. Without fork, the file descriptors would have to be saved and restored on every suspension and resumption to provide an illusion of isolation.
We could relax our rule to "disallow any two built-ins to run in parallel" by taking a lock on the file descriptor table, but considering the lock is held from the point of resumption until the next suspension it might not achieve very much in practice.
Some of the intricacies of scheduling are also covered in Michael Greenberg's An executable and formal semantics of the POSIX shell.
To motivate the question, consider the following two pipelines:
(1)
while true; do echo 5; done | true(2)
while true; do echo 5; done | { read x; echo $((x+42)); }Both pipelines spawn two processes, both of which use shell builtins exclusively: neither of these pipelines needs to make an
execvesystem call (though some systems may implement true or echo as executables, Smoosh and most shells build them in).
Scheduling is an opaque part of the POSIX specification. Indeed, as we have argued here, there is no semantics for scheduling. Here is a similar pipeline run through a script that straces for clone and fork calls on various shells:
% ./test/check_fork.sh "while echo 5; do : ; done | true" bash 3 dash 3 yash 1 zsh 1 ksh 1 ash 3
It might make an interesting use case for effects in OCaml -- whilst we can certainly piggyback on Eio's existing suspension mechanisms, this is slightly different in that we need to ensure some preconditions are true before resuming certain computations. Fiber-local state is insufficient as programs implicitly depend on global state of the process (in particular the FD table). In this sense, we need some form of suspension-resumption hooks.
Forking in Shells & Library Maintenance [weekly-2026-w14]
- April 3, 2026
- Patrick Ferris
This week, Merry got close to executing apt-get install ca-certificates and running Debian's debootstrap scripts correctly. Nathan and I released Ppxlib 0.38.0, I added support to ocaml-proj for compiling to the browser and I fixed a long-standing bug in hilite.
Forking in Merry
- April 3, 2026
- Patrick Ferris
So far, Merry has managed to support many features of the POSIX shell specification without needing to do a fork(2) without an exec. Or, to put it another way, all the forking that Merry does, will only be followed by running C code. This is actually a feature of Eio's Process API. It is required to make process execution safe in the context of multiple domains in OCaml (mostly due to ensuring consistency within the garbage collector). However, it is perfectly okay to fork and run OCaml code provided there is only a single domain.
In a shell, many features of the shell language require that commands (or built-ins, function applications etc.) execute within a subshell. The specification does not go into detail about how shells should implement this, but many choose to fork in order to preserve some state in the parent (for example, the file descriptor table). Some shells try to minimise the number of forks as an optimisation. The korn shell (ksh) is one such shell.
Using the notation
$(command)will causecommandto execute in a subshell of the current ksh. In many instances, ksh will not actually fork/exec a subshell when command is a built-in or a shell function.
By trying to adhere to Multicore OCaml's "thou shalt not fork" commandment, Merry is much more similar to ksh in this regard. However, whenever there is an interaction with a shell built-in and semantics that need a child process, things get tricky very quickly. Consider the following:
while echo "hello"; do
:
done | head -n 3
We have a mix of shell built-ins, normal commands, a compound command (the while loop) and a pipeline. Each individual command in the pipeline requires you to run them in a subshell and set them up before executing them. Without a fork for that first command, you may end up looping forever which is not the intended behaviour here. I had a good conversation with Michael about the implications of this for reproducibility.
For now, I have started implementing a fork for some of the shell features whilst still trying to maintain the functional core of the implementation.
An OCaml 5.5 compatible Ppxlib
- April 3, 2026
- Patrick Ferris
Nathan and I released an OCaml 5.5 compatible version of ppxlib.
Ppxlib Releases › 0.38.0 [ppxlib-release-0-38-0]
- March 30, 2026
- Patrick Ferris
Nathan and I released ppxlib.0.38.0 last week. Its main feature is full migration support for the upcoming OCaml 5.5 compiler (currently in its alpha3 release). This means supporting the handful of new features landing in OCaml 5.5: modular explicits, external type declarations and arbitrary "local" structure items.
As Nathan and I continue to find a plausible maintenance story for ppxlib, Nathan has opened an issue on the OCaml compiler to discuss the idea of adding additional extension points to the language.
This comes from the desire to be able to store encoded versions of new OCaml features inside older abstract syntax trees.
There are also some nice bug fixes in there too:
A potential OOM loop has now been removed.
Browser support for ocaml-proj [ocaml-proj-virt]
- April 3, 2026
- Patrick Ferris
A while ago I built some "modern" bindings to PROJ4 in OCaml. After reading Jon Ludlam's weeknotes (and speaking with him and Anil, I thought it might be nice to add a Javascript backend to those bindings). This was relatively straight-forward using Dune's virtual libraries and is available on Github.
With virtual libraries, your own data analysis could (if you wished) depend solely on the proj library and later choose to either link it with proj.c or proj.js depending on where the analysis is being deployed.
I briefly looked at WASM support, but quickly realised there was not much appetite for it and trying to compile around the C FFI was going to be hard.
Hilite updates
- April 3, 2026
- Patrick Ferris
Since I helped relaunch the ocaml.org website a few years ago, I have maintained hilite, a tool for doing build-time syntax highligting.
It is being used on the ocaml.org website, in yocaml_markdown and also in Michael's webplats (when it works...).
And it was Michael who led me to finally fixing and adding syntax highlighting support for Python and Go. Once I was on this roll, I finally took a look at fixing the long-standing bug of trying to highlight ocaml-mdx code, like the following:
# 50.123;;
# 1 "file.ml";;
In fact, this code now highlights fine, but was the source of the bug. The syntax highlighting grammer confuses the ocaml-mdx # as a toplevel directive and trouble ensues.
Ppxlib release and Merry updates [weekly-2026-w13]
- March 30, 2026
- Patrick Ferris
Ppxlib Releases › 0.38.0 [ppxlib-release-0-38-0]
- March 30, 2026
- Patrick Ferris
Nathan and I released ppxlib.0.38.0 last week. Its main feature is full migration support for the upcoming OCaml 5.5 compiler (currently in its alpha3 release). This means supporting the handful of new features landing in OCaml 5.5: modular explicits, external type declarations and arbitrary "local" structure items.
As Nathan and I continue to find a plausible maintenance story for ppxlib, Nathan has opened an issue on the OCaml compiler to discuss the idea of adding additional extension points to the language.
This comes from the desire to be able to store encoded versions of new OCaml features inside older abstract syntax trees.
There are also some nice bug fixes in there too:
A potential OOM loop has now been removed.
Merry updates
- March 30, 2026
- Patrick Ferris
It turns out writing a POSIX(ish) shell is hard; at least, there is a vast number of slightly obscure features that one needs to support. I think this is surprising because most people writing shell scripts write simple shell scripts; scripts that use a much smaller subset of features. In the same breath, those same developers are likely to use something like apt-get install bash which runs a plethora of more advanced (and non-POSIX) shell scripts!
Exec redirects
- March 30, 2026
- Patrick Ferris
In a shell script, you can use the built-in command exec to replace the current process with a new one (e.g. exec vim). However, there is a different mode of operation for exec:
If exec is specified with no operands, any redirections associated with the exec command shall be made in the current shell execution environment.
One of my litmus tests for Merry is the Debian debootstrap scripts (h/t Anil). One thing that it does is the following:
err () {
printf "err $1" >&4
}
exec 4>&1
A lot of detail has been elided for clarity. exec 4>&1 sets up a redirection for the shell's execution environment in which file descriptor 4 is now an alias for standard output. So, writing to 4 (by redirecting a command's standard output to 4 i.e. >&4) will output to where standard output is going (most likely the terminal).
For this to work, shell's must dup2 the relevant file descriptors which have the following condition:
If the file descriptor newfd was previously open, it is closed before being reused; the close is performed silently (i.e., any errors during the close are not reported by dup2()).
This is all very well, unless your program has an important file already open that happens to have file descriptor 4. The Eio Linux and POSIX backends suffer from this problem. Both make use of a file-based synchronisation mechanism for waking up the eventloop should another domain push a completion to the scheduler's run queue. On Linux this is via an eventfd and in POSIX, a pipe.
This dup2 will close the eventfd and will likely grind Eio to a halt (or an assert false). For now, I have resorted to vendoring Eio and moving the eventfd file descriptors to higher values, though I have opened a PR to make this more configurable. This bug is quite easy to write up... it was not so easy to find!
As a random example, consider the debconf/confmodule script which offers very little room for a buggy implementation!
Outreachy
- March 30, 2026
- Patrick Ferris
The presentations from the demonstrations for this round of Outreachy are now online!
Write support in OCaml TIFF library [outreachy-ocaml-tiff]
- February 9, 2026
- Patrick Ferris
I am mentoring Tambe Salome during the December 2025 Outreachy round to add support for writing TIFF files in the ocaml-tiff library.
You can now see the video of the demonstration day presentation:
A POSIX Shell in OCaml [weekly-2026-w12]
- March 19, 2026
- Patrick Ferris
Long time, no weekly. Since the start of this year I have been building a POSIX shell in OCaml called msh (with the underlying library called Merry). Merry is available online now.
A POSIX(ish) shell in OCaml
- March 19, 2026
- Patrick Ferris
Merry is a POSIX(ish) in OCaml. It uses Eio alongside Morbig (a static parser for POSIX shell).
Why another (POSIX) shell?
- March 19, 2026
- Patrick Ferris
Shells have been around for a long time. In my research on the notion of metashell I wrote about Louis Pouzin originally coining the term:
Metashells › Louis Pouzin's "SHELL" [pouzin-shell]
- May 19, 2025
- Patrick Ferris
I spent some time reading part of the multics design documentation this week. Louis Pouzin coined the term "SHELL" in this document, and I was reminded yet again just how important it is to be a good writer even as a "computer science researcher". For example, this excerpt from the requirements section of the document
The previous definitions imply that a command MUST be designed while keeping in mind the user, sitting at his console, wondering about what might be going on, mistyping or forgetting arguments, even if fully aware of the conventions, and possibly interfering with the command by hasty quits, carriage returns, and other temperamental reactions.
And then later, when defining the "SHELL".
We may envision a common procedure called automatically by the supervisor whenever a user types in some message at his console, at a time when he has no other process in active execution under console control (presently called command level). This procedure acts as an interface between console messages and subroutine. The purpose of such a procedure is to create a medium of exchange into which one could activate any procedure, if it were called inside of another program. Hereafter, for simplification, we shall refer to that procedure as the "SHELL".
It still surprises how little the undergraduate degree in computer science at Cambridge focuses on writing skills.
I built Shelter exploring the idea of a shell-like interface that allowed users to undo their shell actions (amongst other cool tricks). Unfortunately shell-like is not enough. Shelter cut many corners to masquerade as a shell (e.g. appending env to understand how a command may have altered the execution environment). I felt it was necessary to make Shelter a "SHELL"! To do that, I needed a solid foundation to build on.
msh, the POSIX shell that comes with Merry, is by no means POSIX-complete in terms of features. But it is my daily driver at this point. It includes a pure OCaml rewrite of linenoise (a small, self-contained alternative to the venerable readline) called bruit.
If you have docker installed on your machine, you can take it for a spin today:
docker run -it --rm patrickferris/msh
The patrickferris/msh docker image is just for trying it out. It is based on the OCaml 5.3 alpine image.
Alternatively, you can build msh from source and have it available in your opam switch.
opam pin git+https://tangled.org/patrick.sirref.org/merry
There are many small paper cuts left to patch over, but most of it is porcelain (e.g. ctrl+left-arrow for moving in bruit). Unfortunately, these are the kinds of things that you will immediately stumble upon.
What makes Merry different?
- March 19, 2026
- Patrick Ferris
Nothing.
Merry is supposed to be a solid, POSIX-ish base to build on. Unfortunaltely, as it turns out, the subset of features from the POSIX specification that people actually use... is pretty much all of it. Every possible redirection, variable expansion, shell built-in and compound command make some appearance. Not to mention the non-POSIX bits of shell we all take for granted (e.g. &>-redirection).
Merry and msh are useable. You will most likely find bugs if you use them. If, when using msh, you find something obscure happening you can enable debug mode either my setting the variable MSH_DEBUG or by invoking msh with -v -v.
What's next?
- March 19, 2026
- Patrick Ferris
It is soon time to combine Shelter and Merry into the time-travelling, POSIX-ish shell that I have been trying to build since I first started working on Shelter.
For this to be successful, I need Merry to be able to pass plenty of tests and right now that involves trying to install plenty of packages using tools like apk and apt.
Shell MRDT
- March 19, 2026
- Patrick Ferris
What I am particularly interested in reasoning about, is the execution context in Merry. This value, alongside the file-system, constitutes a fairly deep understanding of the state that changes in each step of a shell's evaluation loop.
This was, in terms of Shelter, the missing piece for truly building some kind of MRDT across shell sessions.
TIFF in OCaml
- March 19, 2026
- Patrick Ferris
I picked up from the excellent Outreachy work of Tambe Salome in getting write-support in ocaml-tiff. We are getting closer to the kind of API I envisaged in this latest round of refinement and review.
let ( / ) = Eio.Path.( / )
let checkerboard ~size =
let v = Nx.zeros Nx.uint8 [| size; size |] in
for row = 0 to size - 1 do
for col = 0 to size - 1 do
if (row + col) mod 2 = 0 then Nx.set_item [ row; col ] 254 v
done
done;
Nx.to_bigarray v
let () =
Eio_posix.run @@ fun env ->
Tiff_eio.with_open_out (env#cwd / "example.tiff") @@ fun w ->
let tif = Tiff.make (checkerboard ~size:256) in
Tiff.to_file tif w

I have also been extremely pleased to see further external collaborators appearing:
Merry Shell(e)y [weekly-2025-w50]
- December 15, 2025
- Patrick Ferris
Aside from a serious amount of admin, last week I decided to spend a majority of my time thinking about shells.
What is a shell?
- December 15, 2025
- Patrick Ferris
There is no single definition of what a shell is or isn't.
AI Blues [ai-blues]
- December 15, 2025
- Patrick Ferris
Undoubtedly last week, I suffered a severe case of "AI blues" (for lack of a better description).
I have been doing some thinking, trying to distil my own thoughts on what aspects of vibecoding seem to irk me so much. Ultimately, it seems it is more about attitudes than anything else.
Exploitative
- December 15, 2025
- Patrick Ferris
Increasingly, I am finding that other people's vibecoded work is my problem. I have been an active participant in OCaml's outreachy work for going on five years now, and a problem mentors have to face is deciding the appropriate amount of help to give to your intern when they come to you with questions. More than likely, you could answer with specifics and solve the problem at hand. However, this doesn't scale very well and doesn't help your intern learn where else they may look for answers (e.g. Dune's documentation, a project's README page, OCaml's package explorer, heck even just using some simple CLI tools!) or how they might go about debugging a certain problem.
LLMs, in my experience, make this problem worse. When they come up short, they can leave their programmer higher and drier than they would be had they hit a problem on their own. There is an inertia to understanding problems now that there's lots of code lying around. Turning to your colleagues has a few problems that it didn't before:
Your colleagues don't know whose understanding they are correcting: yours or the LLMs?
If it is the latter, there seems to me to be an exploitative quality to even asking for help until you yourself know what's going wrong.
Technological Accelerationism
- December 15, 2025
- Patrick Ferris
There is no doubt that LLMs are pretty good at writing large amounts of okay code. The generation of this code, and often subsequent publishing of it, completely outpaces the ability to maintain it.
Programmers may be able to release ten times more libraries than they could before, but at the moment, they certainly cannot maintain ten times more libraries than they could before.
Scale --