RepoPilotOpen in app →

mvdan/sh

A shell parser, formatter, and interpreter with bash and zsh support; includes shfmt

Healthy

Healthy across all four use cases

weakest axis
Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 4d ago
  • 7 active contributors
  • BSD-3-Clause licensed
Show all 6 evidence items →
  • CI configured
  • Tests present
  • Single-maintainer risk — top contributor 84% of recent commits

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/mvdan/sh)](https://repopilot.app/r/mvdan/sh)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/mvdan/sh on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: mvdan/sh

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/mvdan/sh shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across all four use cases

  • Last commit 4d ago
  • 7 active contributors
  • BSD-3-Clause licensed
  • CI configured
  • Tests present
  • ⚠ Single-maintainer risk — top contributor 84% of recent commits

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live mvdan/sh repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/mvdan/sh.

What it runs against: a local clone of mvdan/sh — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in mvdan/sh | Confirms the artifact applies here, not a fork | | 2 | License is still BSD-3-Clause | Catches relicense before you depend on it | | 3 | Default branch master exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 34 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>mvdan/sh</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of mvdan/sh. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/mvdan/sh.git
#   cd sh
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of mvdan/sh and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "mvdan/sh(\\.git)?\\b" \\
  && ok "origin remote is mvdan/sh" \\
  || miss "origin remote is not mvdan/sh (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(BSD-3-Clause)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"BSD-3-Clause\"" package.json 2>/dev/null) \\
  && ok "license is BSD-3-Clause" \\
  || miss "license drift — was BSD-3-Clause at generation time"

# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
  && ok "default branch master exists" \\
  || miss "default branch master no longer exists"

# 4. Critical files exist
test -f "syntax/parser.go" \\
  && ok "syntax/parser.go" \\
  || miss "missing critical file: syntax/parser.go"
test -f "syntax/nodes.go" \\
  && ok "syntax/nodes.go" \\
  || miss "missing critical file: syntax/nodes.go"
test -f "syntax/printer.go" \\
  && ok "syntax/printer.go" \\
  || miss "missing critical file: syntax/printer.go"
test -f "interp/runner.go" \\
  && ok "interp/runner.go" \\
  || miss "missing critical file: interp/runner.go"
test -f "cmd/shfmt/main.go" \\
  && ok "cmd/shfmt/main.go" \\
  || miss "missing critical file: cmd/shfmt/main.go"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 34 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~4d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/mvdan/sh"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

mvdan/sh is a pure-Go shell parser, formatter, and interpreter that understands POSIX Shell, Bash, Zsh, and mksh syntax. It provides three core capabilities: the syntax package for parsing and inspecting shell ASTs, the shell package for high-level expansions (parameter, brace, arithmetic), and the interp package for executing shell scripts. The flagship tool shfmt formats shell code with opinionated style rules (see syntax/canonical.sh), and gosh is a proof-of-concept interactive shell. Flat package structure under mvdan.cc/sh/v3: syntax/ handles parsing and AST manipulation, expand/ covers parameter/brace/arithmetic expansions, interp/ interprets and executes ASTs, fileutil/ and internal/ provide utilities, and cmd/shfmt/ and cmd/gosh/ are CLI tools wrapping the core packages. No internal monolith—each package is independently testable with separate test files (*_test.go).

👥Who it's for

DevOps engineers, shell script developers, and infrastructure teams who need to programmatically parse, format, or execute shell scripts in Go applications without shelling out to external tools. Also used by developers maintaining CI/CD pipelines, container tools, and build systems that need to understand or transform shell syntax.

🌱Maturity & risk

Highly mature and production-ready. The codebase is 874KB of Go with native fuzzing tests (go test -fuzz=ParsePrint in syntax/), a comprehensive test suite across all packages, active CI via GitHub Actions (test.yml), and requires Go 1.25+. The project is actively maintained with shfmt packaged in 13+ distributions (Alpine, Arch, Debian, Homebrew, Docker, etc.), indicating wide adoption.

Low risk overall. The dependency list is small and high-quality (creack/pty for PTY handling, google/renameio for atomic file ops, rogpeppe/go-internal for testing). Single-maintainer (Marcel van Lohuizen/@mvdan) is a concern for long-term bus factor, but the codebase is stable and well-tested. Breaking changes are documented in CHANGELOG.md. The pure-Go implementation means it cannot fully match POSIX/Bash semantics (no real forking, documented in README caveats), but this is by design.

Active areas of work

Based on go.mod (Go 1.25.0 requirement), the project is actively tracking recent Go versions. The test.yml workflow in .github/workflows/ runs continuous integration. No specific recent feature list is visible in the file structure provided, but the presence of testdata/script/ with .txtar files (txtar is a text archive format for tests) suggests active test case development for edge cases in shfmt behavior (atomic, diff, editorconfig, simplify, tojson, walk modes).

🚀Get running

Clone and install shfmt or gosh:

git clone https://github.com/mvdan/sh
cd sh
go install ./cmd/shfmt@latest
go install ./cmd/gosh@latest

To use the packages in your own Go project:

go get mvdan.cc/sh/v3

Then import "mvdan.cc/sh/v3/syntax" (or expand, shell, interp, etc.).

Daily commands: As a CLI tool: shfmt -l -w script.sh (list + write mode, see cmd/shfmt/main.go for all flags). As a Go package: Create a *syntax.File via syntax.Parse(), inspect/modify the AST, print via syntax.Print(). Interactive shell: gosh runs the interpreter interactively (cmd/gosh/main.go). Fuzzing: cd syntax && go test -run=- -fuzz=ParsePrint for continuous parser testing.

🗺️Map of the codebase

  • syntax/parser.go — Core parser that transforms shell script text into an AST; all shell parsing flows through this file
  • syntax/nodes.go — Defines the AST node types used throughout the codebase; essential for understanding the data structure
  • syntax/printer.go — Converts AST back to formatted shell text; critical path for shfmt output generation
  • interp/runner.go — Main interpreter execution engine that evaluates AST nodes; core of shell script execution
  • cmd/shfmt/main.go — Entry point for shfmt tool; shows how parser/printer are wired together for the primary use case
  • expand/expand.go — Handles shell variable expansion, brace expansion, and parameter substitution; essential utility layer
  • syntax/lexer.go — Tokenizes shell input into lexical elements; foundation layer consumed by parser

🧩Components & responsibilities

  • Lexer (syntax/lexer.go) — Tokenizes raw shell text into tokens; manages stateful

🛠️How to make changes

Add support for a new shell syntax construct

  1. Define new AST node type in syntax/nodes.go (e.g., type MyNewNode struct {...}) (syntax/nodes.go)
  2. Add parsing logic to handle the construct in syntax/parser.go (syntax/parser.go)
  3. Implement printing logic in syntax/printer.go to convert node back to text (syntax/printer.go)
  4. Add test cases in syntax/parser_test.go or syntax/printer_test.go (syntax/parser_test.go)
  5. If the construct needs runtime behavior, implement handling in interp/runner.go (interp/runner.go)

Add a new shell built-in command to the interpreter

  1. Add command handler function in interp/builtin.go (func(ctx context.Context, args []string) error) (interp/builtin.go)
  2. Register the command in the builtinCmd map or handler logic in interp/builtin.go (interp/builtin.go)
  3. Add test cases in interp/interp_test.go to verify command execution (interp/interp_test.go)
  4. If command needs variable expansion, leverage expand/expand.go utilities (expand/expand.go)

Add a new expansion type (variables, globs, etc.)

  1. Implement expansion logic in appropriate expand/*.go file (expand.go, param.go, braces.go, arith.go) (expand/expand.go)
  2. Add test cases in expand/expand_test.go or type-specific test files (expand/expand_test.go)
  3. Integrate with main Expand function call in expand/expand.go if needed (expand/expand.go)
  4. Update interp/runner.go to use new expansion in appropriate contexts (interp/runner.go)

Add a new formatting option to shfmt

  1. Define new flag/option in cmd/shfmt/main.go argument parsing (cmd/shfmt/main.go)
  2. Store option in PrintConfig struct (likely in syntax package) that controls printer behavior (syntax/printer.go)
  3. Modify printer.go logic to respect the new option when generating output (syntax/printer.go)
  4. Add integration test in cmd/shfmt/main_test.go demonstrating the option (cmd/shfmt/main_test.go)

🔧Why these technologies

  • Go — Fast compilation, single binary distribution, excellent standard library for I/O and text processing; essential for a command-line tool like shfmt
  • Recursive descent parsing — Well-suited for shell's context-sensitive grammar; allows straightforward handling of nested structures and bash/zsh extensions
  • AST representation — Enables separation of concerns: lexing→parsing→formatting/interpreting; allows multiple backends (printer, interpreter) on single AST

⚖️Trade-offs already made

  • Single AST representation for bash, zsh, posix, and mksh

    • Why: Reduces code duplication and allows shared parsing/printing logic
    • Consequence: Some shell-specific features require dialect flags in AST nodes; dialect differences embedded in parser state
  • Lexer is stateful (tracks context like inside heredocs, arithmetic mode)

    • Why: Shell's grammar is context-sensitive (e.g., operators mean different things in [[ ]] vs arithmetic)
    • Consequence: Lexer complexity increases; state must be carefully managed to avoid parser desynchronization
  • Interpreter walks AST directly rather than compiling to bytecode

    • Why: Simpler implementation and faster time-to-market; sufficient for most shell scripts
    • Consequence: Execution slower than compiled shells; no optimization passes; repeated script execution re-interprets AST
  • Pattern matching (globs) implemented in expand package, not in parser

    • Why: Globs are expanded at runtime based on filesystem state, not compile-time
    • Consequence: Expansion logic is decoupled from parsing; glob patterns are plain strings in AST until expansion

🚫Non-goals (don't propose these)

  • Does not aim for 100% compatibility with all shell dialects; prioritizes common bash/posix subset
  • Not a full POSIX shell replacement; gosh is explicitly marked as 'proof of concept'
  • Does not support running arbitrary external programs in gosh without explicit handler registration
  • No support for shell history/completion/readline features (these are shfmt-specific, not interpreter-wide)
  • Does not optimize for interactive shell performance; designed for script parsing/formatting

🪤Traps & gotchas

1. No real forking: interp/ uses goroutines for subshells, not fork(). This breaks assumptions about $$ (PID), file descriptors, and signal handling—documented but easy to forget. 2. Ambiguity parsing rules: $(( vs $( + ( requires proper spacing (documented in README); tight lookahead in parser. 3. Associative array indexing: Bash associative arrays require quoted indices (${arr[key]} not ${arr[spaced key]}), else parser assumes arithmetic; see caveat in README. 4. Pure Go limits: Cannot invoke external commands with full shell semantics; interp/runner.go has fallbacks but they're incomplete. 5. EditorConfig integration: shfmt respects .editorconfig files (mvdan.cc/editorconfig dep); style flags from files override CLI flags in some cases—check expand/ behavior.

🏗️Architecture

💡Concepts to learn

  • Recursive Descent Parsing — mvdan/sh's syntax package uses recursive descent to parse shell grammar without backtracking, enabling streaming support via io.Reader—understanding this parser style is crucial to modifying syntax rules
  • Abstract Syntax Tree (AST) — The entire project operates on syntax.Node trees (syntax.File, syntax.Command, syntax.Word); you must understand AST traversal to implement formatters, linters, or transformations
  • Parameter Expansion / Variable Substitution — expand/param.go implements shell parameter expansion ($var, ${var}, ${var:-default}, etc.); this is core to expand package and interp's variable resolution
  • Brace Expansion — expand/braces.go implements {a,b,c} and {1..10} expansion; subtle interaction with other expansions and important for script preprocessing before execution
  • Arithmetic Evaluation — expand/arith.go evaluates shell arithmetic expressions like $((2 + 3 * x)); used in loops, conditionals, and parameter expansion—requires precedence parsing
  • POSIX vs. Bash vs. Zsh Dialects — mvdan/sh supports multiple shell languages via syntax.Lang flag (LangPOSIX, LangBash, LangZsh); dialects have conflicting grammar rules (e.g., Bash arrays vs POSIX), and the parser must branch on this
  • Goroutines for Subshell Simulation — interp/runner.go executes subshells as goroutines instead of fork(); this breaks real PID/FD semantics but enables the pure-Go design—critical limitation to understand
  • golang/go — mvdan/sh uses Go's native AST patterns and fuzzing infrastructure; understanding Go's own parser (src/go/parser) is instructive for the syntax package design
  • shellcheck-net/shellcheck — Complementary static analyzer for shell scripts; while mvdan/sh parses and formats, shellcheck lints for common errors—often used together in CI pipelines
  • koalaman/shellcheck-docker — Docker image packaging shellcheck; users of mvdan/sh's shfmt often also run shellcheck, making this a companion tool in containerized workflows
  • bats-core/bats-core — Shell testing framework; developers using mvdan/sh to parse/format shell tests often use bats to run them, creating a complete testing ecosystem
  • nix-community/nixpkgs — Package collection that includes mvdan/sh; relevant because NixOS packaging shows real-world usage patterns and build dependencies

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive tests for shell/package with real-world examples

The shell package is listed in the README as a high-level API for shell expansions but has no visible test file (shell/ directory shows only doc.go). This is a critical gap since it's documented as a primary entry point. Adding tests would increase coverage, prevent regressions, and serve as usage examples for users attempting parameter expansion, brace expansion, and arithmetic expansion through the public API.

  • [ ] Create shell/shell_test.go with tests for the main shell expansion functions
  • [ ] Add test cases covering parameter expansion (${var}, ${var:-default}, etc)
  • [ ] Add test cases for brace expansion ({a,b,c})
  • [ ] Add test cases for arithmetic expansion $((expr))
  • [ ] Include edge cases and cross-shell compatibility scenarios (bash vs zsh vs POSIX)

Add integration tests for shfmt with editorconfig edge cases

The test suite includes cmd/shfmt/testdata/script/editorconfig.txtar but no corresponding tests verify shfmt's behavior with complex or conflicting .editorconfig hierarchies. Given the repo depends on mvdan.cc/editorconfig, adding robust tests would prevent subtle configuration bugs that affect many users relying on IDE integration.

  • [ ] Extend cmd/shfmt/main_test.go with tests for nested .editorconfig files
  • [ ] Add test cases for conflicting editorconfig settings (indent_style vs indent_size)
  • [ ] Test editorconfig precedence when multiple configs exist in parent directories
  • [ ] Add tests for unsupported editorconfig keys gracefully falling back to defaults
  • [ ] Verify shfmt respects charset, end_of_line, and trim_trailing_whitespace from editorconfig

Add cross-platform tests for interp package on Windows with Unix-specific builtins

The interp package has both interp/os_unix.go and interp/os_notunix.go but windows_test.go appears minimal. The interpreter implements Unix builtins (cd, test, etc) that behave differently on Windows. Adding comprehensive Windows-specific tests would catch platform-specific bugs in builtin behavior, path handling, and file operations that currently may not be well-covered.

  • [ ] Expand interp/windows_test.go with tests for path separator handling in cd and test builtins
  • [ ] Add tests verifying Unix-style test operators work correctly on Windows (e.g., -f for file existence)
  • [ ] Test environment variable expansion with Windows path conventions
  • [ ] Add tests for signal handling differences (SIGTERM availability on Windows)
  • [ ] Verify file I/O builtins handle Windows-specific cases (drive letters, UNC paths)

🌿Good first issues

  • Add test cases for syntax/canonical.sh in syntax/*_test.go to ensure the formatter's default style is fully covered; currently many edge cases in nested parameter expansions may lack specific regression tests: small
  • Extend interp/builtin.go to support more POSIX builtins (e.g., umask, hash, command -v); currently only core builtins are implemented, and test coverage in interp/*_test.go is incomplete for edge cases: medium
  • Document the expand package's public API with examples in expand/doc.go (similar to interp/api.go); currently users must infer behavior from expand_test.go, making it hard to understand when to use expand.Env vs syntax.Config: small

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 9e7dd28 — syntax: don't treat '#' as a comment inside [[ ]] tests (mvdan)
  • fcd06d4 — syntax: replace lossy ParseToken with per-operator UnmarshalText (mvdan)
  • 4175a00 — syntax/typedjson: encode operators as their syntax string form (mvdan)
  • 5b01370 — syntax/typedjson: disable HTML escaping in the encoder (mvdan)
  • 6251269 — syntax: add ParseToken as the inverse of Token.String (mvdan)
  • 81a4969 — syntax: parse zsh subscript flag argument as a pattern (mvdan)
  • 0bb52b2 — syntax: allow [ glob in array elements (mvdan)
  • 5ca0d0d — syntax: support zsh ;| case clause terminator (mvdan)
  • bb64dc5 — expand,syntax: parse brace sequence numbers as int64 (mvdan)
  • cfd1ecf — expand: add BracesSeq with a config parameter and error returns (mvdan)

🔒Security observations

The mvdan/sh codebase demonstrates good security posture overall. It is a well-established shell parsing and interpretation tool with no obvious hardcoded secrets, SQL injection risks, or XSS vulnerabilities. Dependencies are minimal and well-maintained. The primary security considerations relate to the nature of the project itself (shell script interpretation inherently carries execution risks) and ensuring users understand safe usage patterns. The high Go version requirement (1.25.0) may impact adoption but ensures use of recent security patches. No critical vulnerabilities were identified in the static analysis.

  • Medium · Go version compatibility constraint — go.mod. The project requires Go 1.25.0 or later, which is a very recent version. This may limit adoption and security updates for users on stable Go releases. However, this is a deliberate choice for the project. Fix: Consider evaluating if Go 1.25.0 is truly necessary, or if support for earlier stable versions (1.23+) could be maintained for broader compatibility.
  • Low · Potential code execution through shell interpretation — interp/runner.go, interp/builtin.go. The interp package provides shell script interpretation capabilities. While this is the intended functionality, interpreting untrusted shell scripts could lead to arbitrary code execution. Users need to be aware of the risks when interpreting user-supplied scripts. Fix: Ensure documentation clearly warns about security implications of interpreting untrusted scripts. Provide examples of safe usage patterns and sandboxing recommendations.
  • Low · Platform-specific code paths — interp/os_unix.go, interp/os_notunix.go, expand/expand_windows.go. The codebase contains platform-specific implementations (os_unix.go, os_notunix.go, expand_windows.go, etc.) which could have differing security characteristics across platforms. Windows implementation may have different security considerations. Fix: Conduct platform-specific security reviews, particularly for Windows implementations. Ensure consistent security controls across all supported platforms.

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · mvdan/sh — RepoPilot