// Bash 递归下降解析器. // // 将 Bash 命令字符串解析为 AST(抽象语法树). // 这是整个 Bash 安全分析的核心基础设施. // // 语法规则(简化版): // // program → list* // list → pipeline (('&&' | '||' | ';' | '&') pipeline)* // pipeline → command ('|' command)* // command → simple_command | compound_command | function_def // simple_command → assignment* word+ redirection* // compound_command → '{' list '}' | '(' list ')' | if | for | while | case // redirection → op word | number? '>&' number // word → (unquoted | quoted | cmd_sub | var_expand | arith)+ // // 设计原则: // - 宽容解析:遇到无法识别的语法,标记为 NodeWord 而不是报错 // - 不追求完美:只需足够准确以支持安全分析 // - 性能:1000 字符的命令在 1ms 内解析完 // - 只使用 Go 标准库 package bash import ( "strings" ) // 精妙之处(CLEVER): 节点数量预算机制--防止恶意构造的超长/深度嵌套命令导致 OOM. // 50000 个节点足以覆盖正常 Bash 脚本,但能拦截 $($($(... 式的指数膨胀攻击. // 早期方案没有此限制,曾出现过用户输入导致解析器占用数 GB 内存的问题. const maxNodes = 50000 // Parse 解析 Bash 命令字符串,返回 AST 根节点. // // 总是返回一个非 nil 的 NodeProgram 节点(宽容解析). // 即使输入为空,也返回一个空的 Program 节点. func Parse(source string) *Node { // 预处理 heredoc:提取 heredoc body,让主解析器不需要处理跨行问题 processed, heredocs := PreprocessHeredocs(source) p := &parser{ src: processed, pos: 0, heredocs: heredocs, heredocIdx: 0, nodeCount: 0, } return p.parseProgram() } // parser 是解析器状态. type parser struct { src string // 预处理后的源文本 pos int // 当前解析位置(字节偏移) heredocs []HeredocInfo // 预提取的 heredoc 列表 heredocIdx int // 下一个要消费的 heredoc 索引 nodeCount int // 已创建的节点数 aborted bool // 是否因超限而中止 // 精妙之处(CLEVER): compoundDepth 追踪复合命令嵌套层级,解决了一个隐蔽的歧义问题-- // "done" "fi" 等关键字只在对应的 if/for/while 内部才是终止符, // 在顶层它们只是普通命令名(如 `done=$(date)`). // 没有这个追踪,解析 `done; echo hi` 会提前终止解析. compoundDepth int } // ---- 辅助方法 ---- // peek 查看当前位置的字符,不移动位置. func (p *parser) peek() byte { if p.pos >= len(p.src) { return 0 } return p.src[p.pos] } // peekAt 查看相对当前位置偏移 offset 处的字符. func (p *parser) peekAt(offset int) byte { idx := p.pos + offset if idx >= len(p.src) || idx < 0 { return 0 } return p.src[idx] } // advance 前进一个字节,返回消费的字符. func (p *parser) advance() byte { if p.pos >= len(p.src) { return 0 } ch := p.src[p.pos] p.pos++ return ch } // eof 检查是否到达末尾. func (p *parser) eof() bool { return p.pos >= len(p.src) } // remaining 返回剩余未解析的文本. func (p *parser) remaining() string { if p.pos >= len(p.src) { return "" } return p.src[p.pos:] } // skipBlanks 跳过空格和 tab(不跳过换行). func (p *parser) skipBlanks() { for p.pos < len(p.src) { ch := p.src[p.pos] if ch == ' ' || ch == '\t' || ch == '\r' { p.pos++ } else if ch == '\\' && p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' { // 精妙之处(CLEVER): 行续接符处理--Bash 中 `\` 表示行继续, // 必须在 skipBlanks 中处理而非 skipWhitespace,否则 `cmd \ arg` // 会被错误地断开为两条命令.这是早期方案也容易漏掉的边界情况. p.pos += 2 } else { break } } } // skipWhitespace 跳过所有空白(含换行). func (p *parser) skipWhitespace() { for p.pos < len(p.src) { ch := p.src[p.pos] if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' { p.pos++ } else if ch == '\\' && p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' { p.pos += 2 } else { break } } } // skipNewlines 跳过连续的换行符和注释行. func (p *parser) skipNewlines() { for p.pos < len(p.src) { p.skipBlanks() if p.pos < len(p.src) && p.src[p.pos] == '\n' { p.pos++ continue } if p.pos < len(p.src) && p.src[p.pos] == '#' { // 跳过注释行 for p.pos < len(p.src) && p.src[p.pos] != '\n' { p.pos++ } continue } break } } // startsWith 检查从当前位置开始是否匹配指定前缀. func (p *parser) startsWith(prefix string) bool { return strings.HasPrefix(p.src[p.pos:], prefix) } // mk 创建一个新节点(带预算检查). func (p *parser) mk(t NodeType, start, end int, value string) *Node { p.nodeCount++ if p.nodeCount > maxNodes { p.aborted = true } return &Node{ Type: t, Value: value, Start: start, End: end, } } // ---- 语法规则 ---- // parseProgram 解析顶层程序. // // program → (list | comment | newline)* func (p *parser) parseProgram() *Node { start := p.pos prog := p.mk(NodeProgram, start, start, "") for !p.eof() && !p.aborted { p.skipBlanks() if p.eof() { break } ch := p.peek() // 跳过换行 if ch == '\n' { p.pos++ continue } // 注释 if ch == '#' { comment := p.parseComment() prog.AddChild(comment) continue } // 解析语句列表 stmt := p.parseList() if stmt == nil { // 无法解析 - 跳过一个字符,宽容处理 p.pos++ continue } prog.AddChild(stmt) // 消费尾部分隔符 p.skipBlanks() if !p.eof() { ch = p.peek() if ch == ';' || ch == '&' { // 检查不是 && 或 ;; if ch == '&' && p.peekAt(1) == '&' { continue } if ch == ';' && p.peekAt(1) == ';' { continue } p.pos++ } else if ch == '\n' { p.pos++ } } } prog.End = p.pos prog.Value = p.src[start:p.pos] return prog } // parseComment 解析注释(# 到行尾). func (p *parser) parseComment() *Node { start := p.pos // 跳过 # 到行尾 for p.pos < len(p.src) && p.src[p.pos] != '\n' { p.pos++ } return p.mk(NodeComment, start, p.pos, p.src[start:p.pos]) } // parseList 解析语句列表. // // list → pipeline (('&&' | '||' | ';' | '&') pipeline)* // // 为什么用左结合: // // a && b || c 解析为 ((a && b) || c) // 这是 Bash 的实际语义:从左到右求值 func (p *parser) parseList() *Node { left := p.parsePipeline() if left == nil { return nil } for !p.eof() && !p.aborted { p.skipBlanks() if p.eof() { break } // 检查列表操作符 op := p.tryListOperator() if op == "" { break } // 跳过操作符后的空白和换行(允许 && 后换行继续) p.skipNewlines() p.skipBlanks() right := p.parsePipeline() if right == nil { // Operator with no following command. `&` still has // semantic meaning -- bash treats `cmd &` at end of // input as a backgrounded singleton -- so wrap left // into a unary NodeList to carry Background=true. // Other trailing operators (`;`, `&&`, `||`) have no // analogous single-sided semantics; fall through to // break. // // 操作符后无命令. `&` 仍有语义 -- bash 把 `cmd &` // 尾部孤立视为单命令后台, 所以把 left 包一层 NodeList // 以承载 Background=true. 其他尾部操作符 (`;`, `&&`, // `||`) 无对称的单侧语义, 直接 break. if op == "&" { list := p.mk(NodeList, left.Start, left.End, p.src[left.Start:left.End]) list.Operator = op list.Background = true list.AddChild(left) left = list } break } // 构造列表节点 list := p.mk(NodeList, left.Start, right.End, "") list.Operator = op // Set Background when connector is `&` -- the left subtree // is a backgrounded job (bash semantics: `cmd1 & cmd2` means // cmd1 runs in the background, cmd2 in the foreground). // Safety consumers use this to flag commands that escape // foreground control -- a backgrounded `rm -rf /` keeps // running even if the user cancels the shell session. // // 当连接符是 `&` 时置位 Background -- 左子树即后台任务 // (bash 语义: `cmd1 & cmd2` cmd1 后台, cmd2 前台). // 安全消费者据此标记逃脱前台控制的命令 -- 后台的 `rm -rf /` // 即便用户取消 shell 会话也会继续执行. if op == "&" { list.Background = true } list.AddChild(left) list.AddChild(right) list.Value = p.src[left.Start:right.End] left = list } return left } // tryListOperator 尝试匹配列表操作符,成功则消费并返回操作符字符串. func (p *parser) tryListOperator() string { if p.eof() { return "" } ch := p.peek() // && - 逻辑与 if ch == '&' && p.peekAt(1) == '&' { p.pos += 2 return "&&" } // || - 逻辑或 if ch == '|' && p.peekAt(1) == '|' { p.pos += 2 return "||" } // ; - 顺序执行(但不匹配 ;; 用于 case) if ch == ';' && p.peekAt(1) != ';' { p.pos++ return ";" } // & - 后台执行(但不匹配 && 或 &> 或 &>>) if ch == '&' && p.peekAt(1) != '&' && p.peekAt(1) != '>' { p.pos++ return "&" } // 换行也可以作为语句分隔符 if ch == '\n' { p.pos++ return ";" } return "" } // parsePipeline 解析管道. // // pipeline → command ('|' command)* // // 为什么 | 和 |& 都是管道: // // cmd1 | cmd2 标准管道(stdout → stdin) // cmd1 |& cmd2 等价于 cmd1 2>&1 | cmd2 func (p *parser) parsePipeline() *Node { first := p.parseCommand() if first == nil { return nil } parts := []*Node{first} for !p.eof() && !p.aborted { p.skipBlanks() if p.eof() { break } // 检查管道操作符 | 或 |&(但不匹配 ||) ch := p.peek() if ch == '|' && p.peekAt(1) != '|' { if p.peekAt(1) == '&' { p.pos += 2 // |& } else { p.pos++ // | } // 管道后允许换行 p.skipNewlines() p.skipBlanks() next := p.parseCommand() if next == nil { break } parts = append(parts, next) } else { break } } if len(parts) == 1 { return parts[0] } // 构造 pipeline 节点 last := parts[len(parts)-1] pipeline := p.mk(NodePipeline, parts[0].Start, last.End, "") pipeline.Children = parts pipeline.Value = p.src[parts[0].Start:last.End] return pipeline } // parseCommand 解析单个命令. // // command → simple_command // // | compound_command({ ... }, ( ... ), if, for, while, case) // | function_def // // 为什么先检查复合命令: // // { 和 ( 在命令位置有特殊含义,而在参数位置是普通字符 func (p *parser) parseCommand() *Node { p.skipBlanks() if p.eof() || p.aborted { return nil } ch := p.peek() // 子 shell: (cmd) if ch == '(' { return p.parseSubshell() } // 复合命令: { cmd; } // 注意:{ 后必须有空白才是复合命令,否则是普通字符(如 ${var}) if ch == '{' && (p.peekAt(1) == ' ' || p.peekAt(1) == '\t' || p.peekAt(1) == '\n') { return p.parseCompoundBrace() } // 关键字命令 keyword := p.peekKeyword() switch keyword { case "if": return p.parseIf() case "for": return p.parseFor() case "while", "until": return p.parseWhileUntil() case "case": return p.parseCase() case "function": return p.parseFunction() } // 检查是否是终止关键字(只在复合命令内部才识别为终止符) // 在顶层,"done" "fi" 等只是普通的命令名(虽然不常见) if p.compoundDepth > 0 { switch keyword { case "then", "elif", "else", "fi", "do", "done", "esac": return nil } } // 简单命令 return p.parseSimpleCommand() } // peekKeyword 查看当前位置是否是一个 Bash 关键字. // // 关键字必须后面跟空白或行尾才算关键字(否则只是普通标识符前缀). // 例如 "ifeq" 不是 "if" 关键字. func (p *parser) peekKeyword() string { if p.eof() { return "" } // 收集连续的字母数字字符 i := p.pos for i < len(p.src) && isIdentChar(p.src[i]) { i++ } if i == p.pos { return "" } word := p.src[p.pos:i] // 检查后面是否是非标识符字符(空白,分隔符,EOF) if i < len(p.src) && isIdentChar(p.src[i]) { return "" // 还有更多字符,不是独立的关键字 } // 已知关键字列表 switch word { case "if", "then", "elif", "else", "fi", "for", "while", "until", "do", "done", "case", "esac", "in", "function", "select": return word } return "" } // parseSimpleCommand 解析简单命令. // // simple_command → assignment* word+ redirection* // // 关键设计: // - 先收集赋值前缀(VAR=value) // - 第一个非赋值的 word 是命令名 // - 后续 word 是参数 // - 重定向可以出现在命令的任意位置 func (p *parser) parseSimpleCommand() *Node { start := p.pos cmd := p.mk(NodeSimpleCommand, start, start, "") // 收集赋值前缀和命令的 word // 只有在命令名之前的 VAR=value 才是赋值前缀 foundCommandName := false for !p.eof() && !p.aborted { p.skipBlanks() if p.eof() { break } // 是否到达命令结束? if p.isCommandTerminator() { break } // 尝试解析重定向(可以出现在任意位置) redir := p.tryParseRedirection() if redir != nil { cmd.AddChild(redir) continue } // 尝试解析赋值(只在命令名之前) if !foundCommandName { assign := p.tryParseAssignment() if assign != nil { cmd.Assignments = append(cmd.Assignments, &Assignment{ Name: assign.Value, Value: assign.Children[0].Value, }) cmd.AddChild(assign) continue } } // 解析 word(命令名或参数) word := p.parseWord() if word == nil { break } foundCommandName = true cmd.AddChild(word) } if len(cmd.Children) == 0 && len(cmd.Assignments) == 0 { p.pos = start // 回退 return nil } cmd.End = p.pos cmd.Value = p.src[start:p.pos] return cmd } // isCommandTerminator 检查当前位置是否是命令终止符. // // 命令在以下情况终止: // - 行尾 \n // - 管道 | || // - 列表操作符 && ; // - 后台 &(非 && 和 &>) // - 右括号 ) }(复合命令结束) // - 注释 # // - 终止关键字(then, fi, do, done 等) func (p *parser) isCommandTerminator() bool { if p.eof() { return true } ch := p.peek() switch ch { case '\n': return true case '|': return true // | 或 || case '&': if p.peekAt(1) == '&' { return true // && } if p.peekAt(1) == '>' { return false // &> 是重定向,不是终止符 } return true // &(后台) case ';': return true case ')': return true case '#': return true } // 检查终止关键字(只在复合命令内部才识别) if p.compoundDepth > 0 { kw := p.peekKeyword() switch kw { case "then", "elif", "else", "fi", "do", "done", "esac": return true } } return false } // tryParseAssignment 尝试解析变量赋值(VAR=value). // // 赋值的形式: // - VAR=value 简单赋值 // - VAR="value" 带引号的赋值 // - VAR= 空赋值 // - VAR=(a b c) 数组赋值(简化处理) // // 赋值规则: // - 等号前必须是合法的变量名(字母或下划线开头,字母数字下划线) // - 等号前不能有空格 // - 如果不是赋值,返回 nil 且不消费任何字符 func (p *parser) tryParseAssignment() *Node { saved := p.pos // 变量名必须以字母或下划线开头 if p.eof() || !isIdentStart(p.peek()) { return nil } // 收集变量名 nameStart := p.pos for p.pos < len(p.src) && isIdentChar(p.src[p.pos]) { p.pos++ } // 必须紧跟 =(或 += 等,但我们只处理 =) if p.pos >= len(p.src) || p.src[p.pos] != '=' { p.pos = saved return nil } name := p.src[nameStart:p.pos] // 跳过 = p.pos++ // 解析值(可以为空) var valueStr string valueStart := p.pos if !p.eof() && p.peek() != ' ' && p.peek() != '\t' && p.peek() != '\n' && p.peek() != ';' && p.peek() != '&' && p.peek() != '|' && p.peek() != ')' { // 有值 - 解析为 word word := p.parseWord() if word != nil { valueStr = word.Value } } if valueStr == "" && p.pos == valueStart { valueStr = "" } node := p.mk(NodeAssignment, saved, p.pos, name) valueNode := p.mk(NodeWord, valueStart, p.pos, valueStr) node.AddChild(valueNode) return node } // tryParseRedirection 尝试解析重定向操作符及其目标. // // 重定向形式(按优先级排列): // - N>file 将 fd N 重定向到 file // - N>>file 追加 // - N&M fd 复制 // - &>file stdout+stderr 重定向(等价 >file 2>&1) // - &>>file 追加版 // - <&- 关闭 fd // // 为什么数字前缀要特殊处理: // // "2>file" 中 2 是 fd 号不是参数 // 但 "echo 2" 中 2 是参数 // 区分规则:数字后紧跟 > 或 < 才是 fd func (p *parser) tryParseRedirection() *Node { saved := p.pos // 检查数字前缀(fd 号) fdStr := "" fdStart := p.pos for p.pos < len(p.src) && p.src[p.pos] >= '0' && p.src[p.pos] <= '9' { p.pos++ } if p.pos > fdStart { // 数字后必须紧跟 > 或 <(无空格) if p.pos < len(p.src) && (p.src[p.pos] == '>' || p.src[p.pos] == '<') { fdStr = p.src[fdStart:p.pos] } else { p.pos = saved // 不是 fd 前缀,不能在这里消费数字 } } if p.eof() { p.pos = saved return nil } ch := p.peek() start := saved // &> 或 &>>(stdout+stderr 重定向) if fdStr == "" && ch == '&' && p.peekAt(1) == '>' { p.pos++ // 跳过 & p.pos++ // 跳过 > op := "&>" if p.peek() == '>' { p.pos++ // &>> op = "&>>" } return p.finishRedirection(start, op) } // << 系列(heredoc / here-string) if ch == '<' && p.peekAt(1) == '<' { if p.peekAt(2) == '<' { // <<< here-string p.pos += 3 return p.finishRedirection(start, "<<<") } if p.peekAt(2) == '-' { // <<- heredoc(去除 tab) p.pos += 3 return p.finishHeredoc(start, "<<-") } // << heredoc p.pos += 2 return p.finishHeredoc(start, "<<") } // > 系列 if ch == '>' { p.pos++ if p.peek() == '>' { p.pos++ // >> op := fdStr + ">>" if fdStr == "" { op = ">>" } return p.finishRedirection(start, op) } if p.peek() == '&' { p.pos++ // >& if p.peek() == '-' { p.pos++ // >&- node := p.mk(NodeRedirection, start, p.pos, p.src[start:p.pos]) node.RedirectOp = fdStr + ">&-" return node } // >& 后面可能是 fd 号 op := fdStr + ">&" if fdStr == "" { op = ">&" } return p.finishRedirection(start, op) } if p.peek() == '|' { p.pos++ // >| op := fdStr + ">|" if fdStr == "" { op = ">|" } return p.finishRedirection(start, op) } // 普通 > op := fdStr + ">" if fdStr == "" { op = ">" } return p.finishRedirection(start, op) } // < 系列 if ch == '<' { p.pos++ if p.peek() == '&' { p.pos++ // <& if p.peek() == '-' { p.pos++ // <&- node := p.mk(NodeRedirection, start, p.pos, p.src[start:p.pos]) node.RedirectOp = "<&-" return node } return p.finishRedirection(start, "<&") } if p.peek() == '(' { // <( 是进程替换,不是重定向 - 回退 p.pos = saved return nil } op := fdStr + "<" if fdStr == "" { op = "<" } return p.finishRedirection(start, op) } // 不是重定向 p.pos = saved return nil } // finishRedirection 解析重定向的目标部分. func (p *parser) finishRedirection(start int, op string) *Node { p.skipBlanks() // 解析目标 word target := p.parseWord() targetStr := "" if target != nil { targetStr = target.Value } node := p.mk(NodeRedirection, start, p.pos, p.src[start:p.pos]) node.RedirectOp = op node.RedirectTarget = targetStr if target != nil { node.AddChild(target) } return node } // finishHeredoc 解析 heredoc 重定向. // // 解析 << 后的分隔符,然后从预提取的 heredoc 列表中匹配 body. func (p *parser) finishHeredoc(start int, op string) *Node { p.skipBlanks() // 解析分隔符(可能被引号包裹) tagStart := p.pos tag, quoted := p.parseHeredocDelimiter() node := p.mk(NodeHeredoc, start, p.pos, p.src[start:p.pos]) node.RedirectOp = op node.HeredocTag = tag node.HeredocStripTabs = (op == "<<-") node.HeredocQuoted = quoted // 从预提取的 heredoc 列表中匹配 body. // 同时把原文字节区间 [BodyStart, BodyEnd) 透传到 Node, 让审计 / // 安全消费者可以定位 heredoc body 在用户源码的位置. // // Match body from the pre-extracted heredoc list, and forward the // ORIGINAL-source byte interval [BodyStart, BodyEnd) onto the Node // so audit / security consumers can locate the heredoc body within // the user's raw input. if p.heredocIdx < len(p.heredocs) { hd := p.heredocs[p.heredocIdx] if hd.Tag == tag { node.HeredocBody = hd.Body node.HeredocBodyStart = hd.BodyStart node.HeredocBodyEnd = hd.BodyEnd p.heredocIdx++ } } _ = tagStart return node } // parseHeredocDelimiter 解析 heredoc 分隔符. // // 支持: // - EOF 无引号 // - 'EOF' 单引号(阻止展开) // - "EOF" 双引号(阻止展开) // - \EOF 转义(阻止展开) func (p *parser) parseHeredocDelimiter() (string, bool) { if p.eof() { return "", false } ch := p.peek() // 单引号 if ch == '\'' { p.pos++ // 跳过开头 ' start := p.pos for p.pos < len(p.src) && p.src[p.pos] != '\'' { p.pos++ } tag := p.src[start:p.pos] if p.pos < len(p.src) { p.pos++ // 跳过结尾 ' } return tag, true } // 双引号 if ch == '"' { p.pos++ // 跳过开头 " start := p.pos for p.pos < len(p.src) && p.src[p.pos] != '"' { p.pos++ } tag := p.src[start:p.pos] if p.pos < len(p.src) { p.pos++ // 跳过结尾 " } return tag, true } // 反斜杠转义 if ch == '\\' { p.pos++ // 跳过 '\' start := p.pos for p.pos < len(p.src) && isHeredocDelimChar(p.src[p.pos]) { p.pos++ } return p.src[start:p.pos], true } // 无引号标识符 start := p.pos for p.pos < len(p.src) && isHeredocDelimChar(p.src[p.pos]) { p.pos++ } if p.pos == start { return "", false } return p.src[start:p.pos], false } // parseWord 解析一个 Bash "word". // // word 是 Bash 中的基本单元,可以由以下部分组成: // - 普通字符(字母,数字,路径字符等) // - 单引号字符串 'text' // - 双引号字符串 "text" // - ANSI-C 字符串 $'text' // - 变量展开 $VAR 或 ${VAR} // - 命令替换 $(cmd) 或 `cmd` // - 算术展开 $((expr)) // - 进程替换 <(cmd) 或 >(cmd) // - 转义字符 \x // // 这些部分可以连接在一起形成一个 word: // // hello"world"$VAR 是一个 word // // 为什么 word 如此复杂: // // Bash 的 word 不是简单的空格分割,引号,展开等都是 word 的组成部分. // "hello world" 是一个 word(含空格),hello\ world 也是一个 word. func (p *parser) parseWord() *Node { if p.eof() { return nil } start := p.pos var parts []*Node hasContent := false for !p.eof() && !p.aborted { ch := p.peek() // word 结束条件 if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' { break } if ch == '|' || ch == '&' || ch == ';' || ch == ')' || ch == '#' { break } // } 在命令位置结束 word(复合命令闭合) if ch == '}' { break } // > 和 < 开始重定向(除非在 word 内部--但在我们简化的解析中, // 重定向在 parseSimpleCommand 中处理,这里不会遇到独立的 > <) if ch == '>' || ch == '<' { // 进程替换 <(...) 和 >(...) 是 word 的一部分 if p.peekAt(1) == '(' { part := p.parseProcessSubstitution() if part != nil { parts = append(parts, part) hasContent = true continue } } break } // 单引号字符串 if ch == '\'' { part := p.parseSingleQuoted() parts = append(parts, part) hasContent = true continue } // 双引号字符串 if ch == '"' { part := p.parseDoubleQuoted() parts = append(parts, part) hasContent = true continue } // $ 开头的展开 if ch == '$' { part := p.parseDollarExpansion() if part != nil { parts = append(parts, part) hasContent = true continue } } // 反引号命令替换 if ch == '`' { part := p.parseBacktickSubstitution() parts = append(parts, part) hasContent = true continue } // 转义字符 if ch == '\\' && p.pos+1 < len(p.src) { p.pos++ // 跳过 '\' if p.peek() == '\n' { p.pos++ // 行续接,跳过换行 continue } p.pos++ // 跳过被转义的字符 hasContent = true continue } // 普通字符 p.pos++ hasContent = true } if !hasContent { p.pos = start return nil } value := p.src[start:p.pos] // 如果只有一个子部分(如纯引号字符串),直接返回那个节点 if len(parts) == 1 && start == parts[0].Start && p.pos == parts[0].End { return parts[0] } node := p.mk(NodeWord, start, p.pos, value) if len(parts) > 0 { node.Children = parts } return node } // parseSingleQuoted 解析单引号字符串 'text'. // // 单引号内一切都是字面量,不做任何展开. // 唯一的特殊处理是:单引号内无法包含单引号(即使转义也不行). // 常见的 workaround 是 'it'\”s'(三段拼接). func (p *parser) parseSingleQuoted() *Node { start := p.pos p.pos++ // 跳过开头 ' for p.pos < len(p.src) && p.src[p.pos] != '\'' { p.pos++ } if p.pos < len(p.src) { p.pos++ // 跳过结尾 ' } node := p.mk(NodeQuotedString, start, p.pos, p.src[start:p.pos]) node.Quoted = true // 单引号阻止展开 return node } // parseDoubleQuoted 解析双引号字符串 "text". // // 双引号内允许: // - $VAR 变量展开 // - $(cmd) 命令替换 // - `cmd` 反引号命令替换 // - $((expr)) 算术展开 // - ${var} 参数展开 // - \$ \" \\ \` \newline 转义 // // 不允许: // - 通配符展开(*, ?, [...]) // - 单词分割 func (p *parser) parseDoubleQuoted() *Node { start := p.pos p.pos++ // 跳过开头 " var children []*Node for p.pos < len(p.src) && p.src[p.pos] != '"' { ch := p.src[p.pos] // 转义字符(双引号内只有特定字符可转义) if ch == '\\' && p.pos+1 < len(p.src) { next := p.src[p.pos+1] if next == '$' || next == '"' || next == '\\' || next == '`' || next == '\n' { p.pos += 2 continue } } // $ 展开 if ch == '$' { part := p.parseDollarExpansion() if part != nil { children = append(children, part) continue } } // 反引号命令替换 if ch == '`' { part := p.parseBacktickSubstitution() children = append(children, part) continue } p.pos++ } if p.pos < len(p.src) { p.pos++ // 跳过结尾 " } node := p.mk(NodeQuotedString, start, p.pos, p.src[start:p.pos]) node.Quoted = false // 双引号不阻止变量展开 node.Children = children return node } // parseDollarExpansion 解析 $ 开头的展开. // // $VAR → 简单变量展开 // ${VAR} → 参数展开(支持默认值,替换等) // $(cmd) → 命令替换 // $((expr)) → 算术展开 // $'text' → ANSI-C 引号字符串 // $? $$ $! 等 → 特殊变量 func (p *parser) parseDollarExpansion() *Node { if p.peek() != '$' { return nil } start := p.pos p.pos++ // 跳过 $ if p.eof() { // 孤立的 $ - 作为普通字符 return p.mk(NodeWord, start, p.pos, "$") } ch := p.peek() // $(( - 算术展开 if ch == '(' && p.peekAt(1) == '(' { return p.parseArithmeticExpansion(start) } // $( - 命令替换 if ch == '(' { return p.parseCommandSubstitution(start) } // ${ - 参数展开 if ch == '{' { return p.parseBraceExpansion(start) } // $' - ANSI-C 引号 if ch == '\'' { return p.parseAnsiCQuoted(start) } // 特殊变量:$? $$ $! $# $- $@ $* $0-$9 if isSpecialVar(ch) { p.pos++ return p.mk(NodeVariableExpansion, start, p.pos, p.src[start:p.pos]) } // 普通变量名 $VAR if isIdentStart(ch) { nameStart := p.pos for p.pos < len(p.src) && isIdentChar(p.src[p.pos]) { p.pos++ } _ = nameStart return p.mk(NodeVariableExpansion, start, p.pos, p.src[start:p.pos]) } // 不是有效展开 - 回退 $,让调用者处理 p.pos = start // 消费 $ 作为普通字符 p.pos++ return p.mk(NodeWord, start, p.pos, "$") } // parseArithmeticExpansion 解析算术展开 $((expr)). // // 关键:内部的 << 是位移操作符,不是 heredoc! // 需要正确匹配 (( 和 )),支持嵌套括号. func (p *parser) parseArithmeticExpansion(start int) *Node { p.pos += 2 // 跳过 (( depth := 1 for p.pos < len(p.src) && depth > 0 { if p.src[p.pos] == '(' && p.peekAt(1) == '(' { depth++ p.pos += 2 continue } if p.src[p.pos] == ')' && p.peekAt(1) == ')' { depth-- if depth == 0 { p.pos += 2 break } p.pos += 2 continue } // 单个括号也需要计数(用于表达式内的分组) // 但对 )) 的匹配使用 depth 追踪外层 $(( )) p.pos++ } return p.mk(NodeArithmeticExpansion, start, p.pos, p.src[start:p.pos]) } // parseCommandSubstitution 解析命令替换 $(cmd). // // 需要正确匹配括号深度(支持嵌套): // // $(echo $(date)) 内部有嵌套的 $() func (p *parser) parseCommandSubstitution(start int) *Node { p.pos++ // 跳过 ( depth := 1 for p.pos < len(p.src) && depth > 0 { ch := p.src[p.pos] // 跳过引号内的内容 if ch == '\'' { p.pos++ for p.pos < len(p.src) && p.src[p.pos] != '\'' { p.pos++ } if p.pos < len(p.src) { p.pos++ } continue } if ch == '"' { p.pos++ for p.pos < len(p.src) && p.src[p.pos] != '"' { if p.src[p.pos] == '\\' && p.pos+1 < len(p.src) { p.pos++ } p.pos++ } if p.pos < len(p.src) { p.pos++ } continue } // 转义字符 if ch == '\\' && p.pos+1 < len(p.src) { p.pos += 2 continue } if ch == '(' { depth++ } else if ch == ')' { depth-- if depth == 0 { p.pos++ break } } p.pos++ } return p.mk(NodeCommandSubstitution, start, p.pos, p.src[start:p.pos]) } // parseBraceExpansion 解析参数展开 ${VAR} / ${VAR:-default} 等. func (p *parser) parseBraceExpansion(start int) *Node { p.pos++ // 跳过 { depth := 1 for p.pos < len(p.src) && depth > 0 { ch := p.src[p.pos] if ch == '\\' && p.pos+1 < len(p.src) { p.pos += 2 continue } // 跳过内部引号 if ch == '\'' { p.pos++ for p.pos < len(p.src) && p.src[p.pos] != '\'' { p.pos++ } if p.pos < len(p.src) { p.pos++ } continue } if ch == '"' { p.pos++ for p.pos < len(p.src) && p.src[p.pos] != '"' { if p.src[p.pos] == '\\' && p.pos+1 < len(p.src) { p.pos++ } p.pos++ } if p.pos < len(p.src) { p.pos++ } continue } if ch == '{' { depth++ } else if ch == '}' { depth-- if depth == 0 { p.pos++ break } } p.pos++ } return p.mk(NodeVariableExpansion, start, p.pos, p.src[start:p.pos]) } // parseAnsiCQuoted 解析 ANSI-C 引号字符串 $'text'. // // 支持转义序列:\n \t \r \a \b \e \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN // 这些转义在引号内求值(和双引号不同,$' 内的 $ 不做变量展开). func (p *parser) parseAnsiCQuoted(start int) *Node { p.pos++ // 跳过 '($ 已在调用前跳过) for p.pos < len(p.src) && p.src[p.pos] != '\'' { if p.src[p.pos] == '\\' && p.pos+1 < len(p.src) { p.pos += 2 // 跳过转义序列 continue } p.pos++ } if p.pos < len(p.src) { p.pos++ // 跳过结尾 ' } node := p.mk(NodeQuotedString, start, p.pos, p.src[start:p.pos]) node.Quoted = true // ANSI-C 引号:内部不做 $变量 展开 return node } // parseBacktickSubstitution 解析反引号命令替换 `cmd`. // // 关键:反引号内的 \` 是转义的反引号,不是结束符. func (p *parser) parseBacktickSubstitution() *Node { start := p.pos p.pos++ // 跳过开头 ` for p.pos < len(p.src) && p.src[p.pos] != '`' { if p.src[p.pos] == '\\' && p.pos+1 < len(p.src) { p.pos += 2 // 跳过转义(含 \`) continue } p.pos++ } if p.pos < len(p.src) { p.pos++ // 跳过结尾 ` } return p.mk(NodeCommandSubstitution, start, p.pos, p.src[start:p.pos]) } // parseProcessSubstitution 解析进程替换 <(cmd) 或 >(cmd). // // 进程替换在 word 位置有效: // // diff <(sort file1) <(sort file2) // // 关键区分: // // <(cmd) 是进程替换(< 紧跟 (,无空格) // < file 是输入重定向 // > file 是输出重定向 func (p *parser) parseProcessSubstitution() *Node { start := p.pos ch := p.peek() if ch != '<' && ch != '>' { return nil } if p.peekAt(1) != '(' { return nil } p.pos += 2 // 跳过 <( 或 >( depth := 1 for p.pos < len(p.src) && depth > 0 { c := p.src[p.pos] if c == '(' { depth++ } else if c == ')' { depth-- if depth == 0 { p.pos++ break } } else if c == '\'' { p.pos++ for p.pos < len(p.src) && p.src[p.pos] != '\'' { p.pos++ } if p.pos < len(p.src) { p.pos++ } continue } else if c == '"' { p.pos++ for p.pos < len(p.src) && p.src[p.pos] != '"' { if p.src[p.pos] == '\\' && p.pos+1 < len(p.src) { p.pos++ } p.pos++ } if p.pos < len(p.src) { p.pos++ } continue } p.pos++ } return p.mk(NodeProcessSubstitution, start, p.pos, p.src[start:p.pos]) } // ---- 复合命令 ---- // parseSubshell 解析子 shell (cmd). func (p *parser) parseSubshell() *Node { start := p.pos p.pos++ // 跳过 ( node := p.mk(NodeSubshell, start, start, "") // 解析子 shell 内的命令列表 p.skipWhitespace() for !p.eof() && !p.aborted { p.skipWhitespace() if p.peek() == ')' { break } stmt := p.parseList() if stmt == nil { // 跳过无法解析的字符 if !p.eof() && p.peek() != ')' { p.pos++ } continue } node.AddChild(stmt) // 消费分隔符 p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } if p.peek() == ')' { p.pos++ // 跳过 ) } node.End = p.pos node.Value = p.src[start:p.pos] return node } // parseCompoundBrace 解析花括号复合命令 { cmd; }. func (p *parser) parseCompoundBrace() *Node { start := p.pos p.pos++ // 跳过 { node := p.mk(NodeCompoundCommand, start, start, "") p.skipWhitespace() for !p.eof() && !p.aborted { p.skipWhitespace() if p.peek() == '}' { break } stmt := p.parseList() if stmt == nil { if !p.eof() && p.peek() != '}' { p.pos++ } continue } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } if p.peek() == '}' { p.pos++ // 跳过 } } node.End = p.pos node.Value = p.src[start:p.pos] return node } // parseIf 解析 if 语句. // // if condition; then body; [elif condition; then body;]* [else body;] fi func (p *parser) parseIf() *Node { start := p.pos p.pos += 2 // 跳过 "if" p.compoundDepth++ defer func() { p.compoundDepth-- }() node := p.mk(NodeIf, start, start, "") // condition(直到 then) p.skipWhitespace() for !p.eof() && !p.aborted { kw := p.peekKeyword() if kw == "then" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } // 跳过 then if p.peekKeyword() == "then" { p.pos += 4 } // body(直到 elif, else, fi) p.skipWhitespace() for !p.eof() && !p.aborted { kw := p.peekKeyword() if kw == "elif" || kw == "else" || kw == "fi" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } // elif/else 分支 for !p.eof() && !p.aborted { kw := p.peekKeyword() if kw == "elif" { p.pos += 4 // elif condition p.skipWhitespace() for !p.eof() { kw2 := p.peekKeyword() if kw2 == "then" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } // then if p.peekKeyword() == "then" { p.pos += 4 } // elif body p.skipWhitespace() for !p.eof() { kw2 := p.peekKeyword() if kw2 == "elif" || kw2 == "else" || kw2 == "fi" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } } else if kw == "else" { p.pos += 4 // else body p.skipWhitespace() for !p.eof() { kw2 := p.peekKeyword() if kw2 == "fi" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } break } else { break } } // fi if p.peekKeyword() == "fi" { p.pos += 2 } node.End = p.pos node.Value = p.src[start:p.pos] return node } // parseFor 解析 for 循环. // // for VAR in WORDS; do BODY; done // for VAR; do BODY; done // for (( init; cond; step )); do BODY; done func (p *parser) parseFor() *Node { start := p.pos p.pos += 3 // 跳过 "for" p.compoundDepth++ defer func() { p.compoundDepth-- }() node := p.mk(NodeFor, start, start, "") p.skipBlanks() // C-style for: for (( ... )) if p.peek() == '(' && p.peekAt(1) == '(' { // 跳到匹配的 )) p.pos += 2 depth := 1 for p.pos < len(p.src) && depth > 0 { if p.src[p.pos] == '(' && p.peekAt(1) == '(' { depth++ p.pos += 2 continue } if p.src[p.pos] == ')' && p.peekAt(1) == ')' { depth-- if depth == 0 { p.pos += 2 break } p.pos += 2 continue } p.pos++ } } else { // for VAR in WORDS // 跳过 VAR for p.pos < len(p.src) && !isWhitespace(p.src[p.pos]) { p.pos++ } p.skipWhitespace() // 可选的 in WORDS if p.peekKeyword() == "in" { p.pos += 2 p.skipBlanks() // 消费 words 直到 ; 或 do 或 换行 for !p.eof() { p.skipBlanks() kw := p.peekKeyword() if kw == "do" { break } if p.peek() == ';' || p.peek() == '\n' { p.pos++ break } w := p.parseWord() if w == nil { break } } } } // 消费 ; 或 换行 p.skipWhitespace() if p.peek() == ';' { p.pos++ } // do p.skipWhitespace() if p.peekKeyword() == "do" { p.pos += 2 } // body(直到 done) p.skipWhitespace() for !p.eof() && !p.aborted { kw := p.peekKeyword() if kw == "done" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } // done if p.peekKeyword() == "done" { p.pos += 4 } node.End = p.pos node.Value = p.src[start:p.pos] return node } // parseWhileUntil 解析 while/until 循环. // // while condition; do body; done // until condition; do body; done func (p *parser) parseWhileUntil() *Node { start := p.pos keyword := p.peekKeyword() p.pos += len(keyword) // 跳过 "while" 或 "until" p.compoundDepth++ defer func() { p.compoundDepth-- }() node := p.mk(NodeWhile, start, start, "") // condition(直到 do) p.skipWhitespace() for !p.eof() && !p.aborted { kw := p.peekKeyword() if kw == "do" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } // do if p.peekKeyword() == "do" { p.pos += 2 } // body(直到 done) p.skipWhitespace() for !p.eof() && !p.aborted { kw := p.peekKeyword() if kw == "done" { break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' || p.peek() == '\n' { p.pos++ } } // done if p.peekKeyword() == "done" { p.pos += 4 } node.End = p.pos node.Value = p.src[start:p.pos] return node } // parseCase 解析 case 语句. // // case WORD in // // PATTERN) BODY ;; // PATTERN|PATTERN) BODY ;; // // esac func (p *parser) parseCase() *Node { start := p.pos p.pos += 4 // 跳过 "case" p.compoundDepth++ defer func() { p.compoundDepth-- }() node := p.mk(NodeCase, start, start, "") // case word p.skipBlanks() w := p.parseWord() if w != nil { node.AddChild(w) } // in p.skipWhitespace() if p.peekKeyword() == "in" { p.pos += 2 } // case 分支 p.skipWhitespace() for !p.eof() && !p.aborted { p.skipWhitespace() kw := p.peekKeyword() if kw == "esac" { break } // 跳过 pattern 和 ) for !p.eof() { if p.peek() == ')' { p.pos++ // 跳过 ) break } p.pos++ } // body p.skipWhitespace() for !p.eof() { kw2 := p.peekKeyword() if kw2 == "esac" { break } // 检查 ;; 或 ;& 或 ;;& 分隔符 if p.peek() == ';' && p.peekAt(1) == ';' { p.pos += 2 if p.peek() == '&' { p.pos++ // ;;& } break } if p.peek() == ';' && p.peekAt(1) == '&' { p.pos += 2 // ;& break } stmt := p.parseList() if stmt == nil { if !p.eof() { p.pos++ } break } node.AddChild(stmt) p.skipBlanks() if p.peek() == ';' && p.peekAt(1) != ';' && p.peekAt(1) != '&' { p.pos++ } if p.peek() == '\n' { p.pos++ } } } // esac if p.peekKeyword() == "esac" { p.pos += 4 } node.End = p.pos node.Value = p.src[start:p.pos] return node } // parseFunction 解析函数定义. // // function NAME { BODY } // function NAME() { BODY } // NAME() { BODY } func (p *parser) parseFunction() *Node { start := p.pos p.pos += 8 // 跳过 "function" p.skipBlanks() node := p.mk(NodeFunction, start, start, "") // 函数名 nameStart := p.pos for p.pos < len(p.src) && !isWhitespace(p.src[p.pos]) && p.src[p.pos] != '(' && p.src[p.pos] != '{' { p.pos++ } if p.pos > nameStart { nameNode := p.mk(NodeWord, nameStart, p.pos, p.src[nameStart:p.pos]) node.AddChild(nameNode) } p.skipBlanks() // 可选的 () if p.peek() == '(' && p.peekAt(1) == ')' { p.pos += 2 } p.skipWhitespace() // 函数体 body := p.parseCommand() if body != nil { node.AddChild(body) } node.End = p.pos node.Value = p.src[start:p.pos] return node } // ---- 辅助函数 ---- // isIdentStart 判断字符是否可作为标识符开头(字母或下划线). func isIdentStart(ch byte) bool { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' } // isIdentChar 判断字符是否可作为标识符的一部分(字母,数字或下划线). func isIdentChar(ch byte) bool { return isIdentStart(ch) || (ch >= '0' && ch <= '9') } // isWhitespace 判断字符是否是空白字符. func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' } // isSpecialVar 判断字符是否可作为特殊变量($? $$ 等). func isSpecialVar(ch byte) bool { switch ch { case '?', '$', '@', '*', '#', '-', '!', '_': return true case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': return true } return false }