feat: add bash tree-sitter support

- Add bash query patterns for parsing bash scripts
- Add bash language support to languageParser.ts for .sh and .bash extensions
- Create comprehensive test suite for bash parsing
- Add sample bash fixture with various language constructs

Fixes #9430
This commit is contained in:
Roo Code 2025-11-20 12:41:54 +00:00
parent 0851769450
commit 0eb381a6fa
6 changed files with 387 additions and 0 deletions

View file

@ -0,0 +1,143 @@
export const sampleBashContent = `
#!/bin/bash
# Function definition - demonstrates basic function structure
function multi_line_function() {
local param1=\$1
local param2=\$2
echo "Processing \$param1 and \$param2"
return 0
}
# Alternative function syntax
another_function() {
local result=""
for i in {1..5}; do
result+="\$i "
done
echo "\$result"
}
# Variable assignments and exports
GLOBAL_VAR="global_value"
export PATH_VAR="/usr/local/bin:\$PATH"
readonly CONSTANT_VAR="immutable"
declare -a array_var=(
"element1"
"element2"
"element3"
)
# Alias definitions
alias ll='ls -la'
alias grep='grep --color=auto'
alias ..='cd ..'
# Complex variable assignment with command substitution
CURRENT_DIR=\$(
pwd |
sed 's/\\/home\\///' |
tr '/' '-'
)
# Here document example
cat <<EOF > output.txt
This is a multi-line
here document that spans
several lines for testing
EOF
# Case statement with multiple patterns
case "\$1" in
start|START)
echo "Starting service..."
start_service
;;
stop|STOP)
echo "Stopping service..."
stop_service
;;
restart|RESTART)
echo "Restarting service..."
restart_service
;;
*)
echo "Unknown command"
exit 1
;;
esac
# If statement with multiple conditions
if [[ -f "\$CONFIG_FILE" && -r "\$CONFIG_FILE" ]]; then
source "\$CONFIG_FILE"
echo "Configuration loaded"
elif [[ -f "\$DEFAULT_CONFIG" ]]; then
source "\$DEFAULT_CONFIG"
echo "Default configuration loaded"
else
echo "No configuration found"
exit 1
fi
# While loop with read
while IFS= read -r line; do
process_line "\$line"
counter=\$((counter + 1))
done < input.txt
# For loop with array iteration
for element in "\${array_var[@]}"; do
echo "Processing: \$element"
transform_element "\$element"
done
# Pipeline example
cat data.txt |
grep "pattern" |
sort -u |
head -20 > results.txt
# Function with arithmetic operations
calculate_sum() {
local sum=0
for num in "\$@"; do
sum=\$((sum + num))
done
echo \$sum
}
# Source another script
source ./config.sh
. ./utils.sh
# Array manipulation
declare -A associative_array
associative_array["key1"]="value1"
associative_array["key2"]="value2"
# Test command examples
if [ -z "\$VAR" ]; then
echo "Variable is empty"
fi
if [[ "\$VAR" =~ ^[0-9]+\$ ]]; then
echo "Variable is numeric"
fi
# Command substitution in arithmetic context
result=\$((10 + \$(get_value)))
# Redirection examples
exec 3< input.txt
exec 4> output.txt
exec 5>&1
# Trap signal handling
trap cleanup EXIT
trap 'echo "Interrupted"' INT TERM
`
export default {
path: "test.sh",
content: sampleBashContent,
}

View file

@ -0,0 +1,51 @@
import { inspectTreeStructure, debugLog } from "./helpers"
import { sampleBashContent } from "./fixtures/sample-bash"
describe("Inspect Bash", () => {
it("should capture Bash-specific constructs", async () => {
const result = await inspectTreeStructure(sampleBashContent, "bash")
debugLog("Bash Inspect Result:", result)
// Check for function definitions
expect(result).toContain("function_definition")
// Check for variable assignments
expect(result).toContain("variable_assignment")
// Check for command structures
expect(result).toContain("command")
// Check for control flow structures
expect(result).toContain("case_statement")
expect(result).toContain("if_statement")
expect(result).toContain("while_statement")
expect(result).toContain("for_statement")
// Check for test commands
expect(result).toContain("test_command")
// Check for arithmetic operations
expect(result).toContain("arithmetic_expansion")
// Check for here documents
expect(result).toContain("heredoc_redirect")
// Check for pipelines
expect(result).toContain("pipeline")
// Check for redirections
expect(result).toContain("redirect")
// Check for arrays
expect(result).toContain("array")
// Check for command substitution
expect(result).toContain("command_substitution")
// Check for program structure
expect(result).toContain("program")
// The shebang appears as a comment in the tree
expect(result).toContain("comment")
})
})

View file

@ -0,0 +1,102 @@
/*
TODO: The following structures can be parsed by tree-sitter but lack query support:
1. Process Substitution:
(process_substitution)
Example: diff <(command1) <(command2)
2. Coprocess:
(coproc_statement)
Example: coproc NAME { command; }
3. Compound Commands:
(compound_statement)
Example: { command1; command2; }
4. Select Loops:
(select_statement)
Example: select item in list; do ...; done
5. Extended Glob Patterns:
(extglob_pattern)
Example: !(pattern), ?(pattern), *(pattern), +(pattern), @(pattern)
*/
import { testParseSourceCodeDefinitions, debugLog } from "./helpers"
import { sampleBashContent } from "./fixtures/sample-bash"
import { bashQuery } from "../queries"
// Bash test options
const bashOptions = {
language: "bash",
wasmFile: "tree-sitter-bash.wasm",
queryString: bashQuery,
extKey: "sh",
}
describe("parseSourceCodeDefinitionsForFile with Bash", () => {
let parseResult: string | undefined
beforeAll(async () => {
// Cache parse result for all tests
parseResult = await testParseSourceCodeDefinitions("test.sh", sampleBashContent, bashOptions)
debugLog("Bash Parse Result:", parseResult)
})
it("should parse function definitions", () => {
expect(parseResult).toMatch(/\d+--\d+ \| function multi_line_function\(\)/)
expect(parseResult).toMatch(/\d+--\d+ \| another_function\(\)/)
expect(parseResult).toMatch(/\d+--\d+ \| calculate_sum\(\)/)
debugLog("Function definitions found:", parseResult)
})
it("should parse variable declarations and exports", () => {
expect(parseResult).toMatch(/\d+--\d+ \| GLOBAL_VAR="global_value"/)
expect(parseResult).toMatch(/\d+--\d+ \| export PATH_VAR/)
expect(parseResult).toMatch(/\d+--\d+ \| readonly CONSTANT_VAR/)
expect(parseResult).toMatch(/\d+--\d+ \| declare -a array_var/)
debugLog("Variable declarations found:", parseResult)
})
it("should parse alias definitions", () => {
expect(parseResult).toMatch(/\d+--\d+ \| alias ll='ls -la'/)
expect(parseResult).toMatch(/\d+--\d+ \| alias grep='grep --color=auto'/)
debugLog("Alias definitions found:", parseResult)
})
it("should parse control structures", () => {
expect(parseResult).toMatch(/\d+--\d+ \| case "\$1" in/)
expect(parseResult).toMatch(/\d+--\d+ \| if \[\[ -f "\$CONFIG_FILE"/)
expect(parseResult).toMatch(/\d+--\d+ \| while IFS= read/)
expect(parseResult).toMatch(/\d+--\d+ \| for element in/)
debugLog("Control structures found:", parseResult)
})
it("should parse here documents and redirections", () => {
expect(parseResult).toMatch(/\d+--\d+ \| cat <<EOF/)
expect(parseResult).toMatch(/\d+--\d+ \| cat data.txt/)
debugLog("Here documents and redirections found:", parseResult)
})
it("should parse source commands", () => {
expect(parseResult).toMatch(/\d+--\d+ \| source \.\/config.sh/)
expect(parseResult).toMatch(/\d+--\d+ \| \. \.\/utils.sh/)
debugLog("Source commands found:", parseResult)
})
it("should parse test commands", () => {
expect(parseResult).toMatch(/\d+--\d+ \| if \[ -z "\$VAR" \]/)
expect(parseResult).toMatch(/\d+--\d+ \| if \[\[ "\$VAR" =~ \^/)
debugLog("Test commands found:", parseResult)
})
it("should parse arithmetic operations", () => {
expect(parseResult).toMatch(/\d+--\d+ \|.*\$\(\(/)
debugLog("Arithmetic operations found:", parseResult)
})
it("should parse shebang", () => {
expect(parseResult).toMatch(/\d+--\d+ \| #!\/bin\/bash/)
debugLog("Shebang found:", parseResult)
})
})

View file

@ -28,6 +28,7 @@ import {
embeddedTemplateQuery,
elispQuery,
elixirQuery,
bashQuery,
} from "./queries"
export interface LanguageParser {
@ -218,6 +219,11 @@ export async function loadRequiredLanguageParsers(filesToParse: string[], source
language = await loadLanguage("elixir", sourceDirectory)
query = new Query(language, elixirQuery)
break
case "sh":
case "bash":
language = await loadLanguage("bash", sourceDirectory)
query = new Query(language, bashQuery)
break
default:
throw new Error(`Unsupported language: ${ext}`)
}

View file

@ -0,0 +1,84 @@
/*
Bash Tree-sitter Query Patterns
*/
export default `
; Function definitions
(function_definition
name: (word) @name.definition.function) @definition.function
; Variable declarations and assignments
(variable_assignment
name: (variable_name) @name.definition.variable) @definition.variable
; Export statements
(declaration_command
name: (simple_expansion
(variable_name) @name.definition.export)) @definition.export
; Alias definitions
(declaration_command
name: "alias"
value: (concatenation
(word) @name.definition.alias)) @definition.alias
(declaration_command
name: "alias"
value: (word) @name.definition.alias) @definition.alias
; Source/dot commands (file includes)
(command
name: (command_name (word) @source_cmd (#match? @source_cmd "^(source|\\.)$"))
argument: (_) @name.definition.source) @definition.source
; Here documents
(redirected_statement
body: (command)
redirect: (heredoc_redirect
(heredoc_start) @name.definition.heredoc)) @definition.heredoc
; Case statements
(case_statement
value: (_) @name.definition.case) @definition.case
; If statements
(if_statement) @definition.if_statement
; While loops
(while_statement) @definition.while_loop
; For loops
(for_statement
variable: (variable_name) @name.definition.for_variable) @definition.for_loop
; Array declarations
(variable_assignment
name: (variable_name) @name.definition.array
value: (array)) @definition.array
; Command substitutions
(command_substitution) @definition.command_substitution
; Pipeline commands
(pipeline) @definition.pipeline
; Redirections
(command
redirect: (_)) @definition.redirection
; Test commands ([ ] and [[ ]])
(test_command) @definition.test_command
; Arithmetic expressions
(arithmetic_expansion) @definition.arithmetic
; Parameter expansions
(expansion
(variable_name) @name.reference.variable) @reference.variable
; Comments (for documentation purposes)
(comment) @comment
; Shebang
(program
. (comment) @shebang (#match? @shebang "^#!/"))
`

View file

@ -26,3 +26,4 @@ export { zigQuery } from "./zig"
export { default as embeddedTemplateQuery } from "./embedded_template"
export { elispQuery } from "./elisp"
export { scalaQuery } from "./scala"
export { default as bashQuery } from "./bash"