- 开发工具
- CLI
【免费下载链接】ctags
A maintained ctags implementation
Universal Ctags 为 Scheme(包括 Racket、Guile、Gauche 等方言)提供了专门的内置解析器。本文基于仓库中的 ctags-lang-scheme 手册页 展开,讲解如何激活 Scheme 语言、识别其文件扩展名与别名、理解解析器支持的 kind(标签类型)与 field(字段),并结合 parsers/scheme.c 源码和 Units/parser-scheme.r 测试用例,说明解析器在define、set!等常见表单上的实际行为。读完本文,你可以为.scm、.rkt、.sch等 Scheme 源码准确生成 tags,并读懂--list-kinds-full=Scheme等命令的输出。
概览:这份手册在讲什么
ctags-lang-scheme(7)是 Universal Ctags 众多"语言专项手册"之一,定位是"关于用 Universal Ctags 为 Scheme 源码生成标签的零散笔记"(Random notes about tagging Scheme source code)。它属于手册第 7 节(Manual section: 7),Manual group 为 Universal Ctags,文档当前标注版本为 6.2.0。
与 ctags(1) 这样的通用命令手册不同,语言专项手册聚焦于单一语言解析器的启用方式、文件映射和版本变更记录。Scheme 手册全文虽短,但配合源码与测试用例,可以还原出完整的解析器行为画像。
启用 Scheme 解析器
Universal Ctags 的 Scheme 解析器默认启用,因此多数场景下无需额外配置即可直接使用。手册的 SYNOPSIS 部分给出了三种显式控制方式,适用于需要精确指定语言或定制映射的场景。
按需启用:--languages=+Scheme
如果出于性能或冲突考虑全局禁用了某些语言,可以用--languages选项精确控制启用集合,+前缀表示在已有集合上追加:
ctags ... --languages=+Scheme ...强制指定:--language-force=Scheme
当输入文件扩展名不在 Scheme 的默认映射表内(例如无扩展名文件、.foo之类自定义后缀,或内容与扩展名不一致)时,可用--language-force强制按 Scheme 解析:
ctags ... --language-force=Scheme ...从源码看,--language-force与--language是同义选项,强制将后续输入文件按指定语言处理,跳过语言猜测流程。相关选项的完整说明见 ctags(1)。
自定义扩展名映射:--map-Scheme
--map-Scheme用于把额外扩展名关联到 Scheme 语言。手册给出了 8 条示例命令,+.xxx表示在默认映射基础上追加扩展名(-前缀则为删除):
ctags ... --map-Scheme=+.SCM ... ctags ... --map-Scheme=+.SM ... ctags ... --map-Scheme=+.sch ... ctags ... --map-Scheme=+.scheme ... ctags ... --map-Scheme=+.scm ... ctags ... --map-Scheme=+.sm ... ctags ... --map-Scheme=+.rkt ...这些命令列出的正是解析器内置的默认扩展名集合,与源码中SchemeParser()注册的extensions数组完全一致(见 parsers/scheme.c 第 113-115 行):
static const char *const extensions [] = { "SCM", "SM", "sch", "scheme", "scm", "sm", "rkt", NULL };也就是说,大写SCM/SM与小写scm/sm同时被识别。实际项目中如果你使用.ss、.sps等其他 Scheme 常见后缀,可通过同样的--map-Scheme=+.ss方式追加。查看当前生效映射可用:
ctags --list-map-extensions=Scheme语言别名:gosh / guile / racket
Scheme 解析器还注册了 3 个别名,见 parsers/scheme.c 第 116-118 行:
static const char *const aliases [] = { "gosh", "guile", "racket", NULL };gosh(Gauche shell)、guile(GNU Guile)、racket(Racket)分别对应三大 Scheme 方言/实现。别名可参与语言名匹配(例如--language-force=racket是否可用取决于版本对别名的处理策略),执行ctags --list-aliases=Scheme可查看当前实际生效的别名列表。
解析器架构:Lisp 家族共享的元解析器
Scheme 解析器本身并不重新实现全部词法逻辑,而是复用了 Lisp 家族的公共扫描器。从 parsers/scheme.c 第 93-109 行可以看到,findSchemeTags()构建了一个struct lispDialect方言描述符,然后调用findLispTagsCommon()(定义于 parsers/lisp.c):
static void findSchemeTags (void) { struct lispDialect scheme_dialect = { .definer2kind = scheme_hint2kind, .case_insensitive = true, .namespace_sep = 0, .unknown_kind = K_UNKNOWN, .definer_field = SchemeFields + F_DEFINER, .skip_initial_spaces = false, .lambda_syntax_sugar = true, .is_def = scheme_is_def, .get_it = lispGetIt, .scope = CORK_NIL, }; findLispTagsCommon (&scheme_dialect); }struct lispDialect的接口定义在 parsers/x-lisp.h,它通过一组回调把各 Lisp 方言的差异点参数化:
| 字段 | Scheme 取值 | 含义 |
|---|---|---|
definer2kind | scheme_hint2kind | 根据定义形式(如DEFINE、SET!)推断 kind |
case_insensitive | true | 定义关键字大小写不敏感(define/DEFINE均识别) |
namespace_sep | 0 | 无命名空间分隔符(区别于 Common Lisp 的:) |
unknown_kind | K_UNKNOWN | 无法归类定义时回退到unknownkind |
definer_field | SchemeFields + F_DEFINER | 记录"定义者"的definer字段 |
lambda_syntax_sugar | true | 支持(define ((f) ...))这类 lambda 语法糖的嵌套括号跳过 |
is_def | scheme_is_def | 判断行首是否形如(def...或(set!... |
正是lambda_syntax_sugar = true这一项,让 Scheme 解析器能处理(define ((c0)) 1)这类 Lisp 家族中较"激进"的嵌套定义写法——公共扫描器会在识别到定义关键字后跳过连续的开括号与空白,直到定位到真正的定义名。
支持的标签类型(kinds)
Scheme 解析器注册了 3 种 kind,定义于 parsers/scheme.c 第 27-48 行:
typedef enum { K_UNKNOWN, K_FUNCTION, K_SET } schemeKind; static kindDefinition SchemeKinds [] = { { true, 'Y', "unknown", "unknown type of definitions", .version = 1 }, { true, 'f', "function", "functions" }, { true, 's', "set", "sets" } };| 字母 | 名称 | 含义 | 典型来源 |
|---|---|---|---|
Y | unknown | 无法确定具体类型的定义(版本 1 新增) | 形如(defsomething ...)但无法归为已知 kind 的定义形式 |
f | function | 函数 | (define (foo args) ...)、(define foo ...) |
s | set | 赋值/绑定 | (set! foo ...) |
function:define 的两种形态
K_FUNCTION由scheme_hint2kind()在定义形式为DEFINE时产生(parsers/scheme.c 第 72-91 行),覆盖 Scheme 中define的两种主流写法:
- 变量定义:
(define a0 1)→ 标签名a0,kindf; - 函数定义:
(define (b0) 1)、(define (b1 a) 1)→ 标签名b0/b1,kindf; - 带点参(rest 参数)函数:
(define (b3 a . b) 1)→ 标签名b3。
set:set! 赋值
K_SET由scheme_is_def()在行首匹配到(set!时返回(parsers/scheme.c 第 57-70 行),scheme_hint2kind()也会对(SET! ...)形式单独返回K_SET:
static struct lispIsDefResult scheme_is_def (struct lispDialect *dialect, const unsigned char *strp) { bool cis = dialect->case_insensitive; bool is_set = ( (strp [1] == 's' || (cis && strp [1] == 'S')) && (strp [2] == 'e' || (cis && strp [2] == 'E')) && (strp [3] == 't' || (cis && strp [3] == 'T')) && (strp [4] == '!')); if (is_set) return (struct lispIsDefResult){ .is_def = true, .kind = K_SET, }; return lispIsDef (dialect, strp); }unknown:版本 0.0 之后的类型兜底
unknownkind(字母Y)是解析器自"0.0"版本以来新增的兜底类型:当行首是(def...形式、但既不是DEFINE也不是SET!时,scheme_hint2kind()返回K_UNKNOWN,标签仍会被生成,只是归为unknown类型。这样(define-syntax ...)、(define-record-type ...)、(defmacro ...)等各类宏/派生定义形式不至于被漏掉,同时lispIsDef()会明确排除default、definition这类误匹配(见 parsers/lisp.c 第 144-173 行)。
查看当前生效的 kinds 定义,可用:
ctags --list-kinds-full=Scheme解析器字段:definer
Scheme 解析器注册了唯一的 parser 字段definer(parsers/scheme.c 第 32-41 行):
typedef enum { F_DEFINER, } schemeField; static fieldDefinition SchemeFields[] = { { .name = "definer", .description = "the name of the function or macro that defines the unknown/Y-kind object", .enabled = true, .version = 1 }, };字段描述原文为:"产生 unknown/Y 类型对象的那条函数或宏定义的名称"(the name of the function or macro that defines the unknown/Y-kind object)。也就是说,definer字段与unknownkind 是配套设计:当某对象被归为unknown时,记录下到底是由哪个定义形式(如define-syntax)产生的它。
其底层机制见 parsers/lisp.c 第 361-379 行lispGetIt()的实现——当推断出的 kind 等于dialect->unknown_kind时,取定义形式字符串作为definer,随后通过attachParserFieldToCorkEntry()把它挂到对应 cork 条目的字段上:
if (kind == dialect->unknown_kind) { definer = vStringValue(kind_hint->str); if (definer[0] == '(') definer++; } if (kind != KIND_GHOST_INDEX) { index = makeSimpleTag (name, kind); if (dialect->definer_field && dialect->definer_field->enabled && definer && index != CORK_NIL) attachParserFieldToCorkEntry (index, dialect->definer_field->ftype, definer); }因为definer默认enabled = true,默认输出(--output-format=u-ctags)下就会带上;在 JSON 输出中可通过--fields-Scheme=+{definer}显式确认或调整。查看字段定义:
ctags --list-fields=Scheme注意definer是 parser 特定字段,仅对 Scheme(以及同样基于 Lisp 元解析器的 Lisp、EmacsLisp、Clojure 等)生效,对其他语言不可用。
解析行为验证:从测试用例看边界处理
Units/parser-scheme.r下汇集了 Scheme 解析器的测试用例,既有.d目录型用例(对照expected.tags),也有.b断言语义型用例(断言输入不应产生任何标签)。它们印证了解析器的三类关键行为。
define 的多行与嵌套形式
scheme-simple-define.d 的输入覆盖了define的各种布局:普通变量、引号列表('(a)、'(a . b))、跨行书写、定义名换行、(define (f a . b) ...)点参函数,以及(define ((c0)) 1)这类 lambda 语法糖嵌套形式。其expected.tags表明所有形态都能产出fkind 标签,例如:
a0 input.scm /^(define a0 1)$/;" f b0 input.scm /^(define (b0) 1)$/;" f c0 input.scm /^(define ((c0)) 1)$/;" f注意define关键字本身被单独置一行((define换行后再写名字)也能正确提取标签名,这得益于公共扫描器对空白与开括号的跳过逻辑。
set! 的跨行与不完整输入
scheme-simple-setbang.d 用 30 余种set!写法验证skind 的提取,包括跨行参数、引号换行、(set!后换行等,expected.tags中全部标记为s:
a0 input.scm /^(set! a0 1)$/;" s ab input.scm /^ ab $/;" s ac input.scm /^ ac '(1 2 . 3))$/;" s该用例还以"不完整输入"(文件末尾是未闭合的(set!)结尾,验证解析器遇到 EOF 时不会崩溃或产生伪标签。
注释与字符串中的代码不会被误报
- scheme-srfi-30-comment.b:输入为 SRFI-30 嵌套块注释
#| ... |#,注释体内虽然含有(define a 1)和(set! b #f),但expected.tags为空——块注释内容被正确跳过,不会误报标签; - scheme-string.b:输入为一段跨行字符串字面量,字符串内的
(define ...)、(set! ...)同样不产生标签。
这两个.b用例的"空期望"从正面确认了解析器不会在字符串与块注释内部生成伪标签。
常见使用场景与命令速查
# 对单个 .scm 文件生成默认 tags 文件 ctags input.scm # 强制按 Scheme 解析一个无扩展名文件 ctags --language-force=Scheme input_noext # 追加自定义扩展名映射 ctags --map-Scheme=+.ss --map-Scheme=+.sps . # 查看 Scheme 的 kinds、字段、扩展名与别名 ctags --list-kinds-full=Scheme ctags --list-fields=Scheme ctags --list-map-extensions=Scheme ctags --list-aliases=Scheme # 在 JSON 输出中显式启用 definer 字段 ctags --output-format=json --fields-Scheme=+{definer} input.scm版本变更记录(自 "0.0" 起)
手册 VERSIONS 一节记录了解析器自初始版本以来的变更,共两条,均与本文前述内容对应:
- 新增
unknownkind:对应源码中K_UNKNOWN(字母Y),为(def...形式但无法归类的定义提供兜底标签; - 新增
definer字段:对应SchemeFields中的definer,记录产生 unknown 对象的定义形式名称。
这两个能力在 parsers/scheme.c 中都标注了.version = 1,与手册记录吻合。后续如需查询更新的行为差异,可阅读仓库根目录的 NEWS.rst 与 docs/news.rst。
延伸阅读
- ctags(1):Universal Ctags 主命令手册,涵盖全部语言相关选项
- parsers/scheme.c:Scheme 解析器完整实现
- parsers/lisp.c 与 parsers/x-lisp.h:Lisp 家族共享元解析器及其方言接口
- Units/parser-scheme.r:Scheme 解析器测试用例集
- source.mak:构建系统中 Scheme 解析器的注册位置(
parsers/scheme.c条目) - ctags-lang-lisp.7.rst:同为 Lisp 家族的手册,可对照理解方言差异
- 开发工具
- CLI
【免费下载链接】ctags
A maintained ctags implementation
相关推荐
Universal Ctags 对 Fortran 源码的标签生成指南:ctags-lang-fortran 手册与 linkName 额外标签解析
Universal Ctags 对 Fortran 源码的标签生成指南:ctags lang fortran 手册与 linkName 额外标签解析 本指南以
开发工具CLI使用 Universal Ctags 为 Automake(Makefile.am)源码生成标签:Automake 解析器完全指南
使用 Universal Ctags 为 Automake(Makefile.am)源码生成标签:Automake 解析器完全指南 本指南系统讲解 Univer
开发工具CLIUniversal Ctags 的 SystemTap 脚本标签解析:ctags-lang-systemtap(7) 手册深度解读
Universal Ctags 的 SystemTap 脚本标签解析:ctags lang systemtap 7 手册深度解读 本文围绕 Universal
开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考