| 389 | } |
| 390 | |
| 391 | fn process_command_node(node: tree_sitter::Node, source: &[u8], result: &mut ParsedCommand) { |
| 392 | // Get full command text, including redirects if parent is redirected_statement |
| 393 | let command_text = if node.parent().map(|p| p.kind()) == Some("redirected_statement") { |
| 394 | node.parent() |
| 395 | .unwrap() |
| 396 | .utf8_text(source) |
| 397 | .unwrap_or_default() |
| 398 | .to_string() |
| 399 | } else { |
| 400 | node.utf8_text(source).unwrap_or_default().to_string() |
| 401 | }; |
| 402 | |
| 403 | // Extract tokens: command_name + word/string/raw_string/concatenation children |
| 404 | let mut tokens: Vec<String> = Vec::new(); |
| 405 | let mut cursor = node.walk(); |
| 406 | for child in node.children(&mut cursor) { |
| 407 | match child.kind() { |
| 408 | "command_name" | "word" | "string" | "raw_string" | "concatenation" => { |
| 409 | let text = child.utf8_text(source).unwrap_or_default().to_string(); |
| 410 | tokens.push(text); |
| 411 | } |
| 412 | _ => {} |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | if tokens.is_empty() { |
| 417 | return; |
| 418 | } |
| 419 | |
| 420 | // Check for path-manipulating commands and extract external paths |
| 421 | if PATH_COMMANDS.contains(&tokens[0].as_str()) { |
| 422 | for arg in &tokens[1..] { |
| 423 | if arg.starts_with('-') || (tokens[0] == "chmod" && arg.starts_with('+')) { |
| 424 | continue; |
| 425 | } |
| 426 | // Resolve path |
| 427 | let path = if std::path::Path::new(arg).is_absolute() { |
| 428 | arg.clone() |
| 429 | } else if arg.starts_with('~') { |
| 430 | if let Ok(home) = std::env::var("HOME") { |
| 431 | arg.replacen('~', &home, 1) |
| 432 | } else { |
| 433 | arg.clone() |
| 434 | } |
| 435 | } else { |
| 436 | // Relative path — can't resolve without cwd context here, |
| 437 | // but the caller checks is_external_path which handles this |
| 438 | arg.clone() |
| 439 | }; |
| 440 | result.directories.push(path); |
| 441 | } |
| 442 | } |
| 443 | |
| 444 | // Skip "cd" from patterns (covered by directory check above) |
| 445 | if tokens[0] != "cd" { |
| 446 | result.patterns.insert(command_text); |
| 447 | let prefix = BashArity::prefix(&tokens); |
| 448 | result.always.insert(format!("{} *", prefix.join(" "))); |