Download packages/tree-sitter-bash/README.md from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/tree-sitter-bash/README.md
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/tree-sitter-bash/README.md
-
curl -L -o README.md https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/tree-sitter-bash/README.md
@moonshot-ai/tree-sitter-bash
A pure-TypeScript bash parser that produces a syntax tree whose named node types match tree-sitter-bash 0.25.0 one-to-one, built for agent-side command permission analysis.
- No native addons; offsets are UTF-16 code units (
node.textis always a directsource.slice(startIndex, endIndex)). - Parsing runs under a hard budget and never throws: budget exhaustion
returns
{ ok: false, reason: 'aborted' }, malformed input returns a degraded tree withhasError: true. - Correctness is enforced by a differential test suite that parses hundreds
of samples β plus the complete official tree-sitter-bash 0.25.0 corpus β
with both this parser and the real tree-sitter-bash (wasm) and compares
the trees byte-for-byte (see
test/fixtures/). The only remaining deviations are the documented ones listed under Known differences below.
API
import { parse } from '@moonshot-ai/tree-sitter-bash';
const result = parse('git status && rm -rf /');
if (result.ok) {
// result.rootNode: program β list β command β¦
}
parse(source: string, options?: ParseOptions): ParseResultParseResultis one of:{ ok: true; rootNode: SyntaxNode; hasError: boolean }{ ok: false; reason: 'aborted' }
ParseOptions:{ timeoutMs?: number; maxNodes?: number }(defaults 50 ms / 50 000 nodes;timeoutMs: Infinitydisables the time check).SyntaxNode:{ type, text, startIndex, endIndex, isNamed, parent, children, namedChildren }. Named node types come from tree-sitter-bash'snode-types.json;childrenincludes the anonymous (punctuation / keyword token) nodes as well,namedChildrenonly the named ones, anddescendantsOfType(root, ...types)walks named descendants.- Depth caps (pathological nesting degrades locally β ERROR nodes,
hasError: trueβ instead of overflowing the stack):MAX_SUBSTITUTION_DEPTH = 150for the $( β¦ ) /β¦/ <( β¦ ) sub-parser chain (the deepest per level, ~13 frames β measured stack overflow at ~380β500 levels on a default Node stack, so the cap keeps a β₯2.5Γ margin);MAX_PARSE_DEPTH = 500for subshell/compound/${β¦}/expression nesting (verified safe at those caps);MAX_SCAN_DEPTH = 1024for the lexer's own scan recursion.
Budget semantics
The budget caps total work, not input size: a multi-hundred-KB string or
heredoc body parses fine (it produces only a handful of nodes), but a
source whose tree would exceed 50 000 nodes β a megabyte-long case
statement, tens of thousands of arithmetic expressions β is aborted under
the defaults. Every created node ticks the budget; long character-level
scan loops re-check the deadline periodically. When either limit is hit,
parse returns { ok: false, reason: 'aborted' } and the abort is prompt
(see Performance). Pass timeoutMs / maxNodes to raise the limits.
Error recovery
Malformed input never throws either. Unterminated constructs (quotes,
expansions, substitutions, heredocs, compound commands) are kept as
partial nodes and flagged with hasError: true; tokens that cannot start
or continue a statement (stray ), a leading &&, β¦) are wrapped in
ERROR nodes and parsing continues. Newlines never appear in the tree:
statement-terminating \ns produce no nodes at all (matching
tree-sitter-bash, whose scanner only emits \n tokens in heredoc
position), while ; / & terminators are anonymous children. As a
last-resort guard against parser bugs, any unexpected internal exception
degrades to a program root with a single ERROR child spanning the
source and hasError: true β callers still get a usable tree.
Known differences from tree-sitter-bash
Named node types always come from tree-sitter-bash's node-types.json, but
for the following constructs the tree shape deliberately deviates from what
tree-sitter-bash 0.25.0 produces (each verified against the real parser,
and pinned by a stored expectation in test/fixtures/ β see
test/helpers/known-differences.ts):
<>(read-write redirect) is parsed as a normalfile_redirectoperator; tree-sitter-bash 0.25.0 fails to parse it.- Several heredocs on one line (
cat <<A <<B) cannot be represented as an overlap-free tree (their bodies interleave with the redirect nodes). tree-sitter-bash errors; this parser degrades the second<<to anERRORnode, skips its body registration, and parses the "body" lines as ordinary commands, mirroring the reference's recovery shape. The same applies to a<<inside a heredoc's line tail. - Statements after a heredoc on the same line (
cat <<EOF; echo x, also&) are absorbed into theheredoc_redirectnode so the tree stays overlap-free once the body (which textually follows the whole line) is attached. tree-sitter-bash errors on the whole line. - Inside an unquoted
heredoc_body, every plain text chunk β including the leading one β becomes aheredoc_contentnode, and backtick command substitutions are parsed ascommand_substitutionchildren. tree-sitter-bash's scanner hides the leading chunk and swallows backticks into the content. - A
$'sequence inside an unquoted heredoc body is plain text here (bash does not expand ANSI-C strings in heredocs; the reference does the same mid-line). Quirk: when a body line STARTS with$'β¦', the reference's scanner errors and drops the wholeheredoc_redirect. - A heredoc redirect at statement start (
<<EOF cat) parses normally; tree-sitter-bash errors. - Unterminated or invalid constructs keep their partial nodes with
hasError: true(see above); tree-sitter-bash degrades them toERRORnodes, and the exact recovery shape differs. This covers invalid compound commands β a fallthrough terminator on the last case item (a) x ;;& esac), a missing separator before a compound keyword (if a; then b fi,{ ls }β both flaggedhasError: truehere), a heredoc inside an if/while condition,foo () ls, a regex with an unquoted space after=~β and small recovery details:- An unterminated test command always keeps its partial
test_commandwith a zero-width closer here. The reference only inserts that zero-width closer for an unterminated[or for a[[ending in a complete unary test ([[ -f file); other unterminated[[forms ([[ $a,[[ a == b) become anERRORnode with no closer there. (An empty[[ ]]sets the error flag in BOTH parsers β the reference recovers by inserting the missing closer as a zero-width token β and this parser matches its tree exactly: a concatenation of two]word pieces plus a zero-width]].) - An unterminated compound command gets no synthetic keyword here
(
if a; then b;ends after the last statement withhasError: true); the reference inserts zero-width keyword nodes (a zero-widthfi). ${x@}(transformation operator with no letter) keeps anexpansionwithhasError: truehere; the reference splits it into anERRORnode and a}command.
- An unterminated test command always keeps its partial
- A trailing connector (
ls &&,ls |) yields a single-childlist/pipelinewithhasError: true; tree-sitter-bash inserts a zero-widthcommandrecovery node. - An empty backtick substitution (
), or a pair containing only whitespace, is a `command_substitution` with no statements; tree-sitter-bash treats the two backticks as a singletoken that is only valid inside a concatenation and errors in argument position. - An empty command substitution (
$( )) is a cleancommand_substitutionwith no statements here; the reference inserts a zero-widthcommandrecovery node (and flagshasError). - In arithmetic, a hex literal is a
number($((0x1F))β number "0x1F"); the reference's arithmetic number token does not cover hex and produces avariable_name"0x1F" instead (a reference quirk β bash does evaluate hex in arithmetic). [[ ((a) == x) && y ]](parenthesized test group followed by&&) parses cleanly here as nestedparenthesized_expressions; the reference mis-reads it as anarithmetic_expansionwith an embeddedERRORnode (a reference quirk β the valid[[ ((a)) == x ]]form matches exactly).- An extglob group the reference rejects β one that directly follows a
pure literal or a dot (
x@(y|z)w,*.@(a|b),_@(y),*@(y),.@(y)) β is an error there (anERRORnode inside the comparison); this parser ends the pattern at the group and recovers with a different error shape. Groups the reference ACCEPTS β at the start of the right side (+(a|b)) or after glob characters (*/@(default).vim,*_@(LIB|SYMLINK),@(LLD|GNU\ ld)*,*([0-9])([0-9]),\"+(?)\") β match exactly, as do right sides split around one substitution or quote (*${var}*,*/${a}*,*"7f 45"*). - An extglob group the reference rejects in a case pattern (
x@(y)), and the same mechanism for.((x.(y)),*.(a|b))β.is not a sigil, but the reference rejects it identically): the reference reparses the group as the start of a new case item; this parser degrades the whole pattern to anERRORnode. (Accepted groups β*(a|b)),*([0-9])([0-9]))β match exactly.) - A single-dash short-option case pattern (
-o)) is awordβ the form this parser always produces β but the reference's classification flips with leftover scanner state (a first case item right after theinline's newline parses it as anextglob_pattern). - A case pattern directly after a line continuation (
| \<newline>) is awordin the reference even when its content would make it a glob (cmake_modulesβ a scanner-state quirk); this parser classifies patterns by content, so such a pattern is anextglob_patternhere. A continuation INSIDE a pattern (cont\<newline>ued)) is an error in the reference (word "cont" +ERROR"ued",hasError: true), and a continuation fused with a digit (a\<newline>1)) is anextglob_patternthere; this parser keeps the continuation and parses the whole pattern cleanly as onewordβ which is also how bash itself reads it. - A comparison right side with TWO substitutions or quotes
(
*${x}*${y},*"s"*"t") does not fit the reference's one-construct pattern rule either; it recovers with a nestedbinary_expressionthere, while this parser keeps a singleconcatenationright side. - A few
[[ β¦ ]]comparison right-hand sides deviate:- A negative decimal operand (
[[ $n == -0.5 ]]) is aunary_expression(-over word "0.5") in the reference; this parser produces a singleextglob_pattern"-0.5" (the unary-minus reading only covers integers here). - A test operator fused with an expansion (
[[ -x$f && β¦ ]]) is aunary_expression(-over aconcatenationofxand$f) in the reference; this parser produces a flatconcatenationof-xand$fwith no unary wrapper. - An escaped
|inside a pattern ([[ $a == foo\|bar ]]) splits the expression in the reference βbinary_expression(==, extglob "foo\")then|then word "bar" (a reference quirk); this parser keeps oneextglob_pattern"foo|bar".
- A negative decimal operand (
${!# }and${!## }(and${!##/}) are pathological expansion forms: the reference recovers with zero-width operator tokens, this parser with a flagged partialexpansion; the shapes differ. (The sane forms${#},${!#},${!##},${#!}all match exactly.)- A base prefix fused with an expansion (
10#${x}) is onenumbernode spanning the expansion in the reference (a quirk); this parser produces aconcatenationof word "10#" and the expansion. ${v:-(default)}(a parenthesized default value) is a plainwordhere; the reference parses it as anarraynode (a quirk).${=1}(zsh-style word splitting flag) produces avariable_name"1" in the reference; this parser produces aword.- An escaped space or tab between arguments is dropped by the reference
as invisible extras (its tree has gaps:
echo 1 \ 2produces number "1" and number "2", andecho a\<TAB>bsplits into word "a" and word "b"); this parser keeps the escape inside the word. (An escaped space INSIDE a word,a\ b, is kept by both parsers and matches.) - A
$directly fused with a backtick substitution ($`echo x`) is onecommand_substitutionwith a$+backtick token in the reference; this parser produces a($)token followed by the substitution. - A non-ASCII βidentifierβ in assignment position (
ει=εΌ,Γ©=1) is avariable_assignmentflaggedhasErrorin the reference; this parser keeps it a plain command word β which is also how bash itself treats it (variable names are ASCII-only, so the text runs as a command). - A negative literal after a compound assignment in a c-style for header
(
for (( β¦ j *= -1, β¦ ))) is folded into anumber"-1" by the reference; this parser produces aunary_expression(-over number "1") β matching what the reference itself produces everywhere else. - A double backslash in a replacement value (
${x// /\\|}) loses its first backslash in the reference's tree (a quirk β thewordstarts one character later); this parser keeps both. string_contentis not split at newlines (tree-sitter-bash's scanner splits it).
For completeness, a few constructs that LOOK like deviations but are not β the reference behaves the same way (verified):
coprochas no grammar support in tree-sitter-bash 0.25.0 and parses as an ordinary command; so does this parser.selectis parsed as afor_statementwith aselectkeyword token β that is exactly the reference's tree (there is noselect_statementnode type).$"β¦"(translated string): in command-argument position it is an anonymous$argument followed by thestringargument; everywhere else (assignment values, redirect destinations, for-loop values, β¦) it is atranslated_stringnode. Both match the reference exactly.$'β¦'is a single-child-freeansi_c_stringnode, also matching.- Brace expansion: only
{N..M}(unsigned digits) is abrace_expression;{a..z},{1..10..2}and{-5..5}are plain word concatenations in the reference and here. - Statement-position
((word++β¦))(inner text starting with a bare identifier plus a postfix++/--) is atest_commandin the reference β its statement-position arithmetic grammar does not accept that form β while((x = 1)),((b + a++)),((a[i]++))and argument-position((a++))arearithmetic_expansion. This parser matches all of these, including after!. - Command-prefix redirects take exactly one destination (
> a cmd xmakescmdthecommand_name), while redirects after the command name consume every following word (cmd > out argputs both words in the redirect) β this matches tree-sitter-bash's actual disambiguation.
Performance
Measured on an Apple-silicon MacBook (Node 24, default 50 ms / 50 000-node budget):
- A typical one-line command (
git status && rm -rf /): ~4 Β΅s per parse. - A 100 KB realistic deployment script (functions, loops, redirects,
expansions, heredocs): tens of milliseconds per parse (~20β60 ms across
runs and machines) β the same order of magnitude as the default 50 ms
budget, so such inputs may return
ok: trueor a prompt abort depending on the machine; raisetimeoutMswhen parsing whole scripts. - A 500 KB heredoc body: parses within the default budget (the tree has only a handful of nodes).
- The abort path is prompt: a 400 KB node-budget bomb aborts in ~20 ms (hard guarantee asserted by the test suite: < 100 ms).
These numbers are smoke-tested as orders of magnitude in
test/performance.test.ts to guard against accidental quadratic
complexity; absolute timings vary by machine.