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, hands translation output through a typed fish IR, applies typed source/output passes, and renders fish source.
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
- residual
readedge cases outside the exact helper-backed surface set -e/pipefailinteractions in compound shell logic- non-literal
source - option-heavy
trap, uncatchable trap signals,shopt, andcoproc - argument-position or broader
>(...)forms outside the covered Linux redirect-target fixtures
Monk works like a small compiler:
Bash source
-> ShellCheck parser and Bash AST
-> Monk translator
-> structural fish DSL
-> typed source graph and output bundle
-> pretty-printed fish source
Language.Bash.Parserasks ShellCheck to parse Bash and retain source positions and parse diagnostics.Language.Fish.Translatorrecursively translates ShellCheck tokens. Focused modules handle control flow, commands, variables, arithmetic, redirections, parameter expansion, process substitution, and other semantic areas.- The translator produces a typed
Language.Fish.DSL.Script. Its types keep blocks and pipelines non-empty, distinguish expression types, and restrict pipeline stages to status-returning commands. - Typed simplification, renaming, source rewriting, inlining, and output planning operate on that same structural representation.
- The private renderer boundary produces final fish source while the translation result retains structured diagnostics for the caller.
The translator also tracks context such as function scope, local variables,
command substitution, errexit, and pipefail. When fish has no direct
equivalent for required Bash behavior, Monk can emit a generated helper
preamble for supported cases such as background-job tracking, exact read
behavior, process substitution, and pipefail handling.
Diagnostics are structured values with a stable code, phase, severity, message,
optional source range, and ReviewRisk (Clean, Review, or Unsafe). The CLI
prints them to stderr together with deduplicated runtime requirements. Numeric
confidence scores are not part of the 0.4 API.
Default mode keeps translating when a best-effort result is available.
--strict instead turns unsupported constructs into translation failures. This
makes normal mode useful for migrations and strict mode useful when approximate
output is unacceptable.
With --recursive, Monk discovers literal source and . references and
builds a graph of the scripts it can resolve. --sources inline combines
translated files into one output, while --sources separate emits individual
.fish files and rewrites source paths to their translated targets. Separate
recursive bundles extract live generated helpers into at most one
_monk_runtime.fish, sourced through quoted relative paths.
Dynamic source expressions cannot be resolved statically and remain warning-driven manual-review cases.
Build it from source:
git clone https://github.com/eessmann/monk.git
cd monk
cabal buildGenerated scripts target Fish 4.6 or newer. Python 3 is declared as an explicit runtime requirement only when an exact hard-case fallback needs it.
Translate 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.AST/Language.Fish.DSLfor the public type-safe Fish construction DSLMonk.Sourcefor recursive source-graph helpersMonk.Outputfor typed stdout, combined, and separate bundle planningMonk.Diagnosticsfor diagnostics, review-risk, and requirement renderingMonkas a thin convenience re-export
Monk.AST now exposes smart constructors such as script, stmt,
command, arg, redirect, begin, pipeline, if_, while, for,
switch, and function. The DSL keeps block and pipeline bodies non-empty at
the type level. Raw constructors and lowering internals are no longer public;
callers that depended on them must migrate to the structural DSL in 0.4.
Successful translations retain the structural Script, ordered diagnostics,
and declared requirements in TranslationResult.
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 (translationDiagnostics translation)
print (translationRuntimeRequirements translation)The normal local loop is:
cabal build
cabal test
MONK_INTEGRATION=1 cabal test
hlint .app/: themonkCLI entry pointsrc/Monk/: public translation, diagnostics, and source-graph APIssrc/Language/Bash/: the ShellCheck parser boundarysrc/Language/Fish/DSL*: the structural fish IR and safe construction APIsrc/Language/Fish/Translator/: translation orchestration and semantic subsystemssrc/Language/Fish/Pretty/: the private structural Fish renderertest/: unit, property, golden, integration, and real-world testsscripts/Bakeoff/: the Monk-versus-Babelfish comparison harnessdocs/design/: architecture, fidelity evidence, and active translator design notes
The test suite checks both generated structure and runtime behavior:
- unit tests cover focused translator, DSL, renderer, diagnostics, source, and harness behavior
- property tests exercise rendering and translation invariants
- golden tests compare generated fish text with checked-in expected output
- integration and real-world tests run Bash and translated fish, then compare exit status, stdout, stderr, and environment changes
- the bake-off runner compares Monk with Babelfish, benchmarks both translators, and reports Bash-versus-generated-Fish runtime medians
Run cabal test for the normal suite. Set MONK_INTEGRATION=1 to enable tests
that require Bash and fish execution.
The bake-off runner compares 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
- runtime benchmark workers replay fixture arguments, stdin, and execution mode against the original Bash and Monk-generated Fish scripts
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/design/shellcheck-syntax-inventory.md: explicit parser-node support and scope decisionsdocs/migration-guide.md: manual cleanup patterns after translationdocs/babelfish-comparison.md: current bake-off workflow and comparison notes