Monk is a Haskell project that tries to translate Bash scripts into fish. It started as a fun excuse to learn more about shell parsing, typed ASTs, and all the weird corners where Bash and fish do not line up cleanly.
Monk is deliberately conservative: it
translates what it understands, emits warnings for the parts that need a human to look again, and can fail fast in --strict mode when it would rather stop than try its best.
Monk parses Bash with ShellCheck, lowers it into a typed fish IR, and renders fish source from there.
Today it handles a lot of ordinary shell code:
- control flow such as
if,while,for, andcase - functions, arrays, variable assignments, and common special variables
- pipelines, background jobs, and command substitution
- redirections, here-strings, and a chunk of process substitution
- recursive
sourcetranslation for literal source paths
It also has a long tail of best-effort behavior.
If you run Monk on a script, the happy path is:
- it produces fish output
- it tells you where translation got lossy or approximate
- you review the result like generated migration code, not handwritten code
The current source of truth for exact vs best-effort behavior is
docs/design/translator-audit.md.
Constructs that still deserve extra attention include:
- subshell-heavy scripts
readedge cases and delimiter-sensitive behaviorset -e/pipefailinteractions in compound shell logic- non-literal
source - option-heavy
trap,shopt, andcoproc - Linux-only
>(...)coverage
Build it from source:
git clone https://github.com/eessmann/monk.git
cd monk
cabal buildTranslate a script:
monk script.sh > script.fish
monk script.sh --output script.fish
monk script.sh --strict
monk script.sh --recursive --sources separateUseful flags:
--output FILEwrites to a file instead of stdout--strictturns best-effort warnings into failures where supported--quiet-warningssuppresses warning output--recursivefollows literalsource/.--sources inline|separatecontrols how recursive source translation is emitted
Warnings and notes go to stderr.
The public modules are intentionally small:
Monk.Translationfor parse + translate entry pointsMonk.Translation.Typesfor the stable translation/diagnostics contractMonk.ASTfor the public fish ASTMonk.Sourcefor recursive source-graph helpersMonk.Diagnosticsfor warning rendering and confidence summariesMonkas a thin convenience re-export
Example:
import Monk.Translation
main :: IO ()
main = do
result <- translateBashFile defaultConfig "script.sh"
case result of
Left err -> print err
Right translation -> do
putStrLn (toString (renderTranslation translation))
print (translationWarnings translation)The normal local loop is:
cabal build
cabal test
MONK_INTEGRATION=1 cabal test
hlint .There is also a bake-off runner for comparing Monk and Babelfish:
cabal run monk-bakeoff -- --compatible --no-benchmark --out-dir /tmp/monk-bakeoffBake-off prerequisites:
babelfishandfishare requiredhyperfineis optional and only needed for benchmark runs- the runner now validates tool paths up front and reports actionable preflight errors or benchmark-skip notes
docs/design/translator-audit.md: fidelity matrix and evidence backlogdocs/design/translator-todo.md: active translator backlogdocs/design/architecture.md: module layout and subsystem boundariesdocs/migration-guide.md: manual cleanup patterns after translationdocs/babelfish-comparison.md: current bake-off workflow and comparison notes