mrkeyoor.com_
Sat 08 Aug 21:00 UTC
PyPIUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability3/5The grammar follows tree-sitter conventions, publishes node-types.json, exposes typed language and highlight-query exports, and keeps major node concepts such as command, pipeline, function_definition, and variable_assignment recognizable. Query consumers still bind directly to node and field names, so grammar changes can break them. More seriously, version 0.25.1's optional core metadata selects tree-sitter 0.24 even though its ABI 15 parser is incompatible with that runtime, showing that the package boundary is not currently version-safe without explicit pins and tests.
Docs2/5The README accurately says this is a Bash grammar, links the Bash and POSIX references, and explains how contributors build and test it. It does not provide Python installation, Parser construction, query, traversal, error, byte-offset, incremental-edit, or version-compatibility examples. Python behavior must be reconstructed from a tiny binding test, __init__.py, py-tree-sitter documentation, grammar.js, node-types.json, corpus fixtures, and queries/highlights.scm. Those sources are useful, but the package page is not a practical user guide.
Maintenance3/5Version 0.25.1 and its broad wheel set were published on December 2, 2025, and the repository was pushed the same day. It is neither archived nor disabled, sits in the official tree-sitter organization, and includes CI, fuzzing, corpus tests, generated parser sources, and bindings for C, Go, Node, Python, Rust, and Swift. The stale incompatible Python optional dependency lowers confidence, and no repository push in about eight months is notable for a grammar tracking both tree-sitter ABI changes and Bash syntax edge cases.
Ecosystem4/5The recorded usage is about 6.1 million downloads per week, and the grammar is distributed for npm, PyPI, crates.io, Go, Swift, and direct C consumers under the tree-sitter organization. It works with the wider query, editor, syntax-highlighting, and language-server ecosystem rather than a Bash-only framework. The direct repository has 324 GitHub stars, and Python users still need a separately compatible runtime plus their own application layer, so the high distribution count should not be mistaken for a large Python-specific support community.

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
Skip it if

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

PackageRegistryPick it when
bashlexPyPIYou want a Python-focused Bash parser and do not need tree-sitter's editor-oriented incremental trees
tree-sitter-language-packPyPIYou need many prebuilt tree-sitter grammars behind one Python installation and can accept a larger dependency
shellcheck-pyPyPIYour actual goal is actionable shell linting and common bug detection rather than building syntax-tree tooling
shfmt-pyPyPIYou need a distributable shell formatter and syntax check instead of a programmable parse tree