// Heredoc 预处理和提取. // // Heredoc 是 Bash 中跨多行的特殊语法,主解析器逐行处理时难以正确处理. // 本模块在主解析之前预处理源文本: // 1. 扫描所有 heredoc 标记(< 0 && found { body += "\n" // 闭合 heredoc body 以换行结尾 (synthetic, matches bodyEnd-bodyStart). } if !found { // 未闭合 (宽容处理): body 无 trailing '\n', 长度 = bodyEnd-bodyStart - (若 lines 间缺尾换行则无差) - 以实际 join 结果为准. body = strings.Join(bodyLines, "\n") } heredocs = append(heredocs, HeredocInfo{ Tag: p.tag, Body: body, BodyStart: bodyStart, BodyEnd: bodyEnd, }) } } return result.String(), heredocs } // ExtractHeredocs 从源文本中提取所有 heredoc(不修改源文本). func ExtractHeredocs(source string) []HeredocInfo { _, heredocs := PreprocessHeredocs(source) return heredocs } // scanHeredocMarkers 扫描一行中的所有 heredoc 标记. // // 识别以下变体: // - < 0 { if line[i] == '(' && i+1 < len(line) && line[i+1] == '(' { depth++ i += 2 continue } if line[i] == ')' && i+1 < len(line) && line[i+1] == ')' { depth-- i += 2 continue } i++ } continue } // 检查 << if ch == '<' && i+1 < len(line) && line[i+1] == '<' { // 排除 <<< (here-string) if i+2 < len(line) && line[i+2] == '<' { i += 3 continue } i += 2 // 跳过 << // 检查 <<- 变体 stripTabs := false if i < len(line) && line[i] == '-' { stripTabs = true i++ } // 跳过空白 for i < len(line) && (line[i] == ' ' || line[i] == '\t') { i++ } if i >= len(line) { continue // 行尾没有分隔符,忽略 } // 解析分隔符 tag, quoted, newI := parseHeredocTag(line, i) if tag == "" { continue } i = newI result = append(result, heredocPending{ tag: tag, stripTabs: stripTabs, quoted: quoted, }) continue } i++ } return result } // parseHeredocTag 从位置 i 开始解析 heredoc 分隔符. // // 支持以下形式: // - EOF → tag="EOF", quoted=false // - 'EOF' → tag="EOF", quoted=true // - "EOF" → tag="EOF", quoted=true // - \EOF → tag="EOF", quoted=true(转义等同引号) // - MY_TAG123 → tag="MY_TAG123", quoted=false // // 返回 (tag, quoted, newI),tag 为空表示解析失败. func parseHeredocTag(line string, i int) (string, bool, int) { if i >= len(line) { return "", false, i } ch := line[i] // 单引号包裹 if ch == '\'' { i++ // 跳过开头引号 start := i for i < len(line) && line[i] != '\'' { i++ } if i >= len(line) { return "", false, i // 未闭合 } tag := line[start:i] i++ // 跳过结尾引号 return tag, true, i } // 双引号包裹 if ch == '"' { i++ // 跳过开头引号 start := i for i < len(line) && line[i] != '"' { i++ } if i >= len(line) { return "", false, i // 未闭合 } tag := line[start:i] i++ // 跳过结尾引号 return tag, true, i } // 反斜杠转义的分隔符 if ch == '\\' { i++ // 跳过反斜杠 start := i for i < len(line) && isHeredocDelimChar(line[i]) { i++ } if i == start { return "", false, i } return line[start:i], true, i } // 无引号的标识符 start := i for i < len(line) && isHeredocDelimChar(line[i]) { i++ } if i == start { return "", false, i } return line[start:i], false, i } // isHeredocDelimChar 判断字符是否可作为 heredoc 分隔符的一部分. // // Bash 对 heredoc 分隔符字符比较宽松:几乎所有非元字符都可以. // 停止字符:空白,重定向,管道,列表操作符,引号,括号. func isHeredocDelimChar(ch byte) bool { switch ch { case ' ', '\t', '\n', '\r', '<', '>', '|', '&', ';', '(', ')', '\'', '"', '`', '\\', '$': // $ 不应出现在未引号的分隔符中 return false } return ch > 0x20 // 排除控制字符 }