tree-sitter-bash review
tree-sitter-bash 0.25.1 packages the Bash grammar and compiled Python language binding used by tree-sitter. With a compatible runtime, it produces concrete syntax trees for complete or broken shell source, with byte ranges, redirects, expansions, pipelines, functions, missing nodes, and errors. Version 0.25.1 fixes false arithmetic-expansion parsing around doubled parentheses and moves valid arithmetic compound commands into the correct grammar branch. The package exports a language capsule and highlight query; it does not execute shell, lint code, or resolve variable meaning.
tree-sitter-bash 0.25.1 installed in 0.2 seconds and used 2 MB in our sandbox, but direct `import _binding` failed with `ModuleNotFoundError`; the supported import is `tree_sitter_bash`. Install it for typed, incremental Bash syntax trees after pin-testing the separate runtime, not for shell execution, lint findings, or semantic security claims.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 2 MB |
| Import | ✗ | import _binding · compiled extensions · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does tree-sitter-bash install cleanly?
Yes. In a fresh container with an empty cache, pip install tree-sitter-bash finished in 0.2s, leaving 1 package and 2 MB on disk. pip-audit reported no known vulnerabilities.
What does tree-sitter-bash need to run?
Python >=3.10, and a platform wheel with compiled extensions. In our run import _binding failed, so it needs extra system packages, and the package ships py.typed for type checkers.
tree-sitter-bash or tree-sitter: which should you use?
tree-sitter: Use the core runtime when you already have or build a Bash language capsule another way. tree-sitter-bash 0.25.1 installed in 0.2 seconds and used 2 MB in our sandbox, but direct import _binding failed with ModuleNotFoundError; the supported import is tree_sitter_bash.
When should you not use tree-sitter-bash?
You need bug or security findings. ShellCheck understands many shell mistakes, while this grammar only describes syntax.
Use it if
- An editor, indexer, code browser, or refactoring tool needs Bash nodes while a user is still typing invalid code.
- Incremental reparsing and changed ranges matter after precisely tracked text edits.
- The same query and traversal model should cover Bash plus other tree-sitter grammars.
- Byte-accurate captures and the packaged highlight query are useful building blocks for syntax tooling.
- You need bug or security findings. ShellCheck understands many shell mistakes, while this grammar only describes syntax.
- You need to know what a script does. The tree cannot resolve sourced files, aliases, environment values, command substitutions, `eval`, PATH lookup, or side effects.
- Every target must remain pure Python. Version 0.25.1 ships compiled extensions and needs a C build path on platforms without a matching wheel.
- You expect parsing from this package alone. It exports a language capsule and query text; Parser, Query, tree editing, traversal, and nodes come from a separately installed tree-sitter runtime.
- You plan to trust the `[core]` extra without a compatibility test. Its metadata asks for `tree-sitter~=0.24`, while this release's grammar uses ABI 15 and current runtime compatibility must be pinned deliberately.
Setup reality
We installed tree-sitter-bash 0.25.1 in 0.2 seconds in a fresh Python 3.12 Bookworm sandbox. It left 1 package and 2 MB on disk. The package declares 1 dependency entry, requires Python 3.10 or newer, includes compiled .so extensions, ships py.typed, and uses the MIT license. pip-audit found 0 known vulnerabilities. Our direct import _binding probe failed with ModuleNotFoundError: No module named '_binding'.
That failure names an internal extension incorrectly, not the documented public module. The package's tree_sitter_bash/__init__.py imports ._binding relatively and exports language; user code should import tree_sitter_bash. Parsing also requires the separate tree-sitter package. The [core] extra in 0.25.1 specifies the 0.24 line, while the published grammar reports ABI 15. Pin a runtime proven to accept that ABI, such as the currently available tree-sitter 0.26.0, and run a construction test in CI.
Wheels cover common CPython 3.10+ macOS, Windows, glibc Linux, and musl Linux targets, including several Arm builds. A platform without one must compile C sources and needs a working compiler plus Python build headers. Parser input should remain bytes. start_byte and end_byte are byte offsets, and point columns also count bytes; slicing a decoded Python string breaks after non-ASCII text. Store the original byte buffer beside the tree.
A returned tree can contain ERROR and missing nodes, so check root_node.has_error before trusting an extraction. Incremental reuse only works when Tree.edit() receives exact old and new byte and point coordinates, then the edited old tree is supplied to parse. Query node and field names are grammar API and may change between releases. Compile every query against the pinned grammar in tests, and use ShellCheck or execution-aware tooling for conclusions about behavior or safety.
Patterns
Load the grammar into py-tree-sitter create-bash-parser
import tree_sitter_bash
from tree_sitter import Language, Parser
BASH = Language(tree_sitter_bash.language())
parser = Parser(BASH)Version 0.25.1 exposes `tree_sitter_bash.language()`. Pin a tree-sitter runtime that accepts its ABI 15 capsule and test construction in CI.
Parse a Bash byte buffer parse-bash-bytes
source = b'#!/usr/bin/env bash\necho "$HOME"\n'
tree = parser.parse(source)
root = tree.root_node
print(root.type, root.start_point, root.end_point)Keep `source` as bytes. Tree-sitter byte offsets and point columns do not index a decoded Unicode string correctly after non-ASCII characters.
Check for syntax recovery nodes reject-error-tree
tree = parser.parse(source)
if tree.root_node.has_error:
raise ValueError('Bash tree contains an ERROR or missing node')Parsing malformed text still returns a Tree. `has_error` is the first check before treating captures as authoritative.
Traverse structural nodes recursively walk-named-children
def walk(node):
yield node
for child in node.named_children:
yield from walk(child)
for node in walk(tree.root_node):
print(node.type, node.start_byte, node.end_byte)`named_children` leaves out punctuation and operators. Use `children` when delimiters such as pipes or semicolons are part of the analysis.
Decode one node by byte range slice-node-source
def text_of(node, source_bytes):
return source_bytes[node.start_byte:node.end_byte].decode('utf-8')
print(text_of(tree.root_node, source))Slice the original bytes first and decode the slice. Applying byte offsets to Python `str` can return the wrong text.
Capture shell function names query-functions
from tree_sitter import Query, QueryCursor
query = Query(BASH, '(function_definition name: (word) @function.name)')
captures = QueryCursor(query).captures(tree.root_node)
names = [text_of(node, source) for node in captures.get('function.name', [])]Query construction fails if a node or field name does not exist. Compile queries during startup tests against the pinned 0.25.1 grammar.
Capture syntactic command names query-command-names
query = Query(BASH, '(command name: (command_name) @command.name)')
captures = QueryCursor(query).captures(tree.root_node)
for node in captures.get('command.name', []):
print(text_of(node, source))A capture is source syntax, not the executable ultimately run after functions, aliases, expansions, `eval`, or PATH resolution.
Read assignment names and source values query-variable-assignments
query = Query(BASH, '''
(variable_assignment
name: (variable_name) @name
value: (_) @value)
''')
for _, match in QueryCursor(query).matches(tree.root_node):
print(text_of(match['name'][0], source), text_of(match['value'][0], source))Captured values may contain variables and command substitutions. The text is not an evaluated shell value.
Describe an edit before reparsing apply-incremental-edit
old_source = b'echo hi\n'
old_tree = parser.parse(old_source)
old_tree.edit(
start_byte=0, old_end_byte=4, new_end_byte=6,
start_point=(0, 0), old_end_point=(0, 4), new_end_point=(0, 6),
)
new_tree = parser.parse(b'printf hi\n', old_tree)All 6 coordinates must describe the same replacement exactly. Incorrect byte or point values can invalidate incremental reuse.
Inspect syntax affected by an edit list-changed-ranges
for changed in old_tree.changed_ranges(new_tree):
print(
changed.start_byte, changed.end_byte,
changed.start_point, changed.end_point,
)Changed syntax ranges may extend beyond the literal replacement because an edit can alter the surrounding parse structure.
Run the packaged highlight captures load-highlight-query
query = Query(BASH, tree_sitter_bash.HIGHLIGHTS_QUERY)
captures = QueryCursor(query).captures(tree.root_node)
for capture_name, nodes in captures.items():
for node in nodes:
print(capture_name, node.start_point, node.end_point)Highlight captures are semantic labels rather than colors. A renderer must map them to a theme and decide how overlapping captures win.
Locate the smallest named node at a byte range find-node-at-byte
start = source.index(b'echo')
node = tree.root_node.named_descendant_for_byte_range(start, start + 4)
print(node.type, text_of(node, source))Editor protocols often use UTF-16 positions. Convert them to offsets in the same UTF-8 byte buffer before querying the tree.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| tree-sitter | PyPI | Use the core runtime when you already have or build a Bash language capsule another way. |
| tree-sitter-language-pack | PyPI | Use it when one Python dependency should provide many prebuilt grammars, including Bash. |
| bashlex | PyPI | Use it for a Python-focused Bash parser when incremental editor parsing is unnecessary. |
| shfmt-py | PyPI | Use it when formatting and a distributable shell syntax checker are the actual deliverables. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

