tree-sitter-bash
tree-sitter-bash is the Bash grammar and compiled Python language binding for the tree-sitter incremental parsing system. It turns shell source bytes into a concrete syntax tree that preserves incomplete code, byte ranges, comments, redirects, expansions, pipelines, functions, and error nodes. The package is not a parser application by itself: you supply a compatible tree-sitter Python runtime, create a Parser, and write traversals or tree-sitter queries for highlighting, navigation, refactoring, indexing, or editor features.
Use tree-sitter-bash when you need editor-grade Bash syntax trees or cross-language tree-sitter queries. Do not use it as a shell interpreter, linter, or semantic analyzer, and pin a compatible tree-sitter runtime because the current core extra is incompatible with the published grammar ABI.
Use it if
- You are building an editor, code browser, indexer, or refactoring tool that must parse Bash while the file may be incomplete
- You need byte-accurate syntax nodes, incremental reparsing, and changed ranges after text edits
- You want one tree-sitter query model across Bash and other programming-language grammars
- You need the grammar's packaged highlight query as a starting point for syntax coloring
- You need to execute shell code or determine its runtime behavior: a syntax tree cannot resolve expansions, sourced files, eval, command lookup, environment state, or side effects
- You need lint findings and security advice rather than syntax nodes: ShellCheck already models many shell-specific mistakes and is the better finished tool
- You want a pure-Python install on every target: the package contains a compiled C binding and falls back to a source build when no compatible wheel exists
- You expect a self-contained parser API: tree-sitter-bash only exports language() and HIGHLIGHTS_QUERY, so parsing, queries, edits, traversal, and error handling come from a separate tree-sitter package
- You plan to install the advertised core extra without pin testing: 0.25.1 declares tree-sitter ~=0.24, but this grammar uses ABI version 15 and tree-sitter 0.24 accepts only ABI versions 13 through 14
Setup reality
Install Python 3.10 or newer and install both tree-sitter-bash and a compatible tree-sitter runtime. There is a current packaging trap: the optional dependency exposed as tree-sitter-bash[core] pins tree-sitter to the 0.24 line, while the compiled 0.25.1 grammar reports language ABI version 15. tree-sitter 0.24 rejects that grammar because it only accepts ABI 13 through 14. Use an explicitly tested current runtime such as tree-sitter 0.26.0 instead of trusting that extra until the metadata changes. The package publishes ABI3 wheels for common macOS, glibc Linux, musl Linux, Windows x64, Windows Arm64, and Linux Arm64 targets. Other platforms need a C compiler, Python headers, and the source build path. Parser input should be bytes. Node start_byte and end_byte are byte offsets, and points count rows plus byte columns, so slicing a decoded Unicode string with those offsets corrupts locations after non-ASCII text. Keep the original bytes beside the tree and decode individual slices. Tree-sitter is error-tolerant: parse returns a tree even for invalid Bash, so check root_node.has_error and inspect ERROR or missing nodes before treating an extraction as authoritative. The grammar recognizes syntax, not meaning. It does not expand variables, follow source, know aliases, execute command substitutions, or prove a command is safe. Incremental parsing only helps if every edit supplies exact old and new byte and point ranges and the old tree is passed back to parse. Query node names are grammar API: command, command_name, function_definition, variable_assignment, and field names can change with grammar releases, so compile queries in tests against the pinned version. The README is mainly for grammar contributors and does not teach the Python runtime; expect to consult py-tree-sitter documentation, node-types.json, grammar.js, corpus tests, and the packaged highlight query together.
Patterns
Load the Bash grammar into a parsercreate-parser
import tree_sitter_bash
from tree_sitter import Language, Parser
BASH = Language(tree_sitter_bash.language())
parser = Parser(BASH)With tree-sitter-bash 0.25.1, use a runtime that supports language ABI 15, such as tree-sitter 0.26.0.
Parse Bash source bytesparse-source
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)Pass bytes and retain them. Node offsets are byte offsets, not Python Unicode character indexes.
Reject a tree containing parse errorsdetect-syntax-errors
tree = parser.parse(source)
if tree.root_node.has_error:
raise ValueError('Bash source contains an ERROR or missing node')Tree-sitter returns a tree for malformed or incomplete input; parsing successfully is not the same as valid Bash.
Traverse named syntax nodeswalk-named-nodes
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_point, node.end_point)named_children omits punctuation tokens. Use children when operators and delimiters are significant to the tool.
Slice source text by node byte rangeextract-node-text
def node_text(node, source_bytes):
return source_bytes[node.start_byte:node.end_byte].decode('utf-8')
print(node_text(tree.root_node, source))Do not apply start_byte and end_byte to a decoded str; non-ASCII characters make character and byte positions diverge.
Query function definitions by fieldfind-functions
from tree_sitter import Query, QueryCursor
query = Query(BASH, '(function_definition name: (word) @function.name)')
captures = QueryCursor(query).captures(tree.root_node)
names = [node_text(node, source) for node in captures.get('function.name', [])]Capture results are grouped by capture name in current py-tree-sitter. Compile queries during startup so invalid node names fail early.
Extract syntactic command namesfind-command-names
query = Query(BASH, '(command name: (command_name) @command.name)')
commands = QueryCursor(query).captures(tree.root_node)
for node in commands.get('command.name', []):
print(node_text(node, source))This finds syntax, not the executable ultimately invoked after aliases, functions, expansions, command, eval, or PATH lookup.
Capture variable names and assignment valuesfind-assignments
query = Query(BASH, '''
(variable_assignment
name: (variable_name) @assignment.name
value: (_) @assignment.value)
''')
for _, match in QueryCursor(query).matches(tree.root_node):
name = node_text(match['assignment.name'][0], source)
value = node_text(match['assignment.value'][0], source)
print(name, value)A syntactic value may still contain expansions and substitutions; the captured text is not an evaluated shell value.
Reparse after a precisely described editapply-incremental-edit
source = b'echo hi\n'
old_tree = parser.parse(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_source = b'printf hi\n'
new_tree = parser.parse(new_source, old_tree)All byte and point coordinates must describe the same edit exactly. Incorrect points can corrupt reuse and changed-range reporting.
Find syntax ranges changed by an editinspect-changed-ranges
for changed in old_tree.changed_ranges(new_tree):
print(changed.start_byte, changed.end_byte, changed.start_point, changed.end_point)Call this after editing the old tree and producing the new tree. Changed syntax ranges can be larger than the literal text replacement.
Use the packaged highlight queryrun-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)The query provides semantic capture names, not colors. Your renderer must map captures to a theme and resolve overlaps.
Find the smallest named node covering a byte rangefind-node-at-position
start = source.index(b'echo')
node = tree.root_node.named_descendant_for_byte_range(start, start + len(b'echo'))
print(node.type, node_text(node, source))Use byte ranges derived from the same encoded source. Editor UTF-16 positions need conversion before calling this API.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| bashlex | PyPI | You want a Python-focused Bash parser and do not need tree-sitter's editor-oriented incremental trees |
| tree-sitter-language-pack | PyPI | You need many prebuilt tree-sitter grammars behind one Python installation and can accept a larger dependency |
| shellcheck-py | PyPI | Your actual goal is actionable shell linting and common bug detection rather than building syntax-tree tooling |
| shfmt-py | PyPI | You need a distributable shell formatter and syntax check instead of a programmable parse tree |