// Bash AST 节点类型定义. // // 本文件定义了 Bash 命令解析后的抽象语法树(AST)节点类型. // 用于安全分析,命令提取等下游任务. // // 设计原则: // - 节点类型覆盖 Bash 核心语法(不追求完整性,够用即可) // - 每个节点记录在源文本中的位置(Start/End) // - 子节点按出现顺序排列 // - 宽容解析:无法识别的部分标记为 NodeWord package bash import "fmt" // NodeType 是 AST 节点类型枚举. type NodeType int const ( NodeProgram NodeType = iota // 顶层程序(根节点) NodePipeline // 管道 cmd1 | cmd2 NodeList // 列表 cmd1 && cmd2 || cmd3 NodeSimpleCommand // 简单命令 ls -la NodeCompoundCommand // 复合命令 { cmd; } NodeSubshell // 子 shell (cmd) NodeIf // if/elif/else/fi NodeFor // for/do/done NodeWhile // while/do/done NodeCase // case/esac NodeFunction // function 定义 NodeRedirection // 重定向 > >> < 2>&1 等 NodeHeredoc // heredoc <(cmd) NodeArithmeticExpansion // 算术展开 $((expr)) NodeVariableExpansion // 变量展开 $VAR 或 ${VAR} NodeQuotedString // 引号字符串 "..." 或 '...' 或 $'...' NodeComment // 注释 # ... ) // nodeTypeNames 用于调试输出 var nodeTypeNames = map[NodeType]string{ NodeProgram: "Program", NodePipeline: "Pipeline", NodeList: "List", NodeSimpleCommand: "SimpleCommand", NodeCompoundCommand: "CompoundCommand", NodeSubshell: "Subshell", NodeIf: "If", NodeFor: "For", NodeWhile: "While", NodeCase: "Case", NodeFunction: "Function", NodeRedirection: "Redirection", NodeHeredoc: "Heredoc", NodeAssignment: "Assignment", NodeWord: "Word", NodeCommandSubstitution: "CommandSubstitution", NodeProcessSubstitution: "ProcessSubstitution", NodeArithmeticExpansion: "ArithmeticExpansion", NodeVariableExpansion: "VariableExpansion", NodeQuotedString: "QuotedString", NodeComment: "Comment", } // String 返回节点类型的可读名称. func (t NodeType) String() string { if name, ok := nodeTypeNames[t]; ok { return name } return fmt.Sprintf("Unknown(%d)", int(t)) } // Node 是 AST 中的一个节点. // // 所有 Bash 语法结构共用这一个节点类型,通过 Type 字段区分. // 不同类型的节点使用不同的额外字段(未使用的字段保持零值). type Node struct { Type NodeType // 节点类型 Value string // 节点的原始文本(或解析后的值) Children []*Node // 子节点列表 Start int // 在源文本中的字节起始位置 End int // 字节结束位置(不含) // ---- 以下字段仅特定类型使用 ---- // Operator: List 节点的操作符(&&, ||, ;, &) Operator string // RedirectOp: Redirection/Heredoc 节点的操作符(>, >>, <, <<, 2>&1 等) RedirectOp string // RedirectTarget: 重定向目标文件名/fd RedirectTarget string // Quoted: QuotedString 节点 - true 表示单引号(阻止变量展开) Quoted bool // HeredocTag: Heredoc 节点的分隔符标签 HeredocTag string // HeredocStripTabs: Heredoc <<- 变体,去除行首 tab HeredocStripTabs bool // HeredocQuoted: Heredoc 分隔符被引号包裹(阻止变量展开) HeredocQuoted bool // HeredocBody: Heredoc 的内容体 HeredocBody string // HeredocBodyStart / HeredocBodyEnd: Heredoc body 在原文中的字节 // 半开区间 [start, end). 用于审计 / 安全消费者把危险 heredoc 定位 // 回用户输入的精确位置. 不变量: source[Start:End] 等于 HeredocBody // (闭合 heredoc 末尾有合成 '\n', 未闭合没有). // // HeredocBodyStart / HeredocBodyEnd: byte offsets of the heredoc // body within the original source, as a half-open interval // [Start, End). Lets audit / security consumers pinpoint a dangerous // heredoc back to the user's raw input. Invariant: // source[Start:End] == HeredocBody (closed heredocs append a // synthetic trailing '\n', unterminated ones do not). HeredocBodyStart int HeredocBodyEnd int // Assignments: SimpleCommand 节点的环境变量赋值前缀(VAR=value cmd args) Assignments []*Assignment // Background: List 节点 - true 表示 & 后台执行 Background bool } // Assignment 表示一个变量赋值(VAR=value). type Assignment struct { Name string // 变量名 Value string // 变量值(原始文本,含引号) } // AddChild 添加子节点. func (n *Node) AddChild(child *Node) { if child != nil { n.Children = append(n.Children, child) } } // CommandName 返回 SimpleCommand 节点的命令名(第一个非赋值的 Word 子节点). // 对于非 SimpleCommand 节点,返回空字符串. func (n *Node) CommandName() string { if n.Type != NodeSimpleCommand { return "" } for _, child := range n.Children { if child.Type == NodeWord || child.Type == NodeQuotedString || child.Type == NodeVariableExpansion || child.Type == NodeCommandSubstitution { return child.Value } } return "" } // CommandArgs 返回 SimpleCommand 节点的参数列表(不含命令名). func (n *Node) CommandArgs() []string { if n.Type != NodeSimpleCommand { return nil } var args []string foundCmd := false for _, child := range n.Children { if child.Type == NodeRedirection || child.Type == NodeHeredoc { continue } if child.Type == NodeAssignment { continue } if !foundCmd { foundCmd = true continue // 跳过命令名本身 } args = append(args, child.Value) } return args }