
简介这款插件名为 Geany-JSON-Prettifier专门面向 Linux 环境下使用 Geany 的开发者核心用途是把凌乱或压缩成单行的 JSON 数据重新排版并附带格式校验与缩小功能。针对实际编辑中常见的格式混乱问题它允许用户只格式化当前选中部分也可一次处理同一文件里的多个独立 JSON 实体缩进方式、正斜杠转义等选项均可按需调整能够较好适配不同团队的代码风格处理第三方接口返回或日志片段时尤为实用。压缩包以 zip 方式提供共收录一百七十六个文件大小约一百六十三 KB从文件构成看以 C 语言源码和头文件为主同时包含多组 JSON 输入样例、对应的 gold 预期输出以及 CMake、configure 等构建脚本和说明文档便于查看插件实现与编译部署。该资源在平台已有四百零二人学习下载对于想改造 Geany 编辑体验的开发者和研究 C 语言插件开发的读者都具备参考价值。通过结合源码与测试用例可以观察到插件如何借助 yajl 库完成解析、格式化、压缩与验证流程为理解具体实现或二次扩展提供比较完整的线索。1. 为什么我不再用外部工具直接在 Geany 里格式化 JSON以前我改 JSON 配置文件的习惯是先打开一个小网站右键粘贴、格式化、再粘回 Geany。直到某天我意识到这个流程有个致命问题而它恰好是 Geany-JSON-Prettifier 解决的文件在编辑器这边格式化在网页那边两边没有对照改到一半想验证 JSON 只能整段复制粘贴。这个插件直接在当前文档上做格式化、美化、缩小和验证全程不离开编辑器。适合每天和 JSON 格式打交道的人嵌入式开发、自动化测试、写配置文件的运维以及所有不想为了 JSON 格式化再开一个 GUI 工具的人。2. JSON 格式化不是字符串替换解析器选型与 Geany 插件机制2.1 Geany 插件本质上是什么Geany 的插件不是一个独立进程而是一个共享库 .so。Geany 启动时扫描插件目录dlopen 每一个 .so然后从里面找 plugin_set_info 函数注册插件。写插件的人唯一要做的是把库编译出来、丢到插件目录、在插件管理器里打勾。对新手来说最迷惑的是 geany_data、geany_functions 这几个全局变量哪来的它们是 Geany 在 dlopen 插件之后填充的插件只需要声明不要在代码里自己初始化。声明之后插件里的任何一个函数都能拿到主窗口、当前文档、Scintilla 控件引用这是 Geany 给插件开的后门。一个最小插件骨架长这样#include geanyplugin.h GeanyPlugin *geany_plugin; GeanyData *geany_data; GeanyFunctions *geany_functions; static void json_format_document(GeanyDocument *doc, gboolean minify) { g_return_if_fail(doc ! NULL); /* 取当前文档全文交给 JSON 核心处理再写回 */ gsize len 0; const gchar *src sci_get_contents(doc-editor-sci, len); /* 调用 transform_json 得到新文本再用 set_text 写回 */ } static void on_menu_format(GtkMenuItem *item, gpointer user_data) { GeanyDocument *doc document_get_current(); json_format_document(doc, FALSE); } void plugin_init(GeanyData *data) { /* 往 Tools 菜单里挂一个菜单项 */ } void plugin_cleanup(void) { /* 释放自己申请的资源Geany 不帮你兜底 */ } void plugin_set_info(GeanyPlugin *plugin) { plugin-funcs-init plugin_init; plugin-funcs-cleanup plugin_cleanup; plugin-info-name Geany-JSON-Prettifier; plugin-info-description _(JSON prettify, minify and validate); plugin-info-version 0.1; plugin-info-author ...; }代码后的关键点plugin_init 在插件被启用时调用一次做菜单和状态栏的注册plugin_cleanup 在插件被禁用或 Geany 退出时调用负责释放资源plugin_set_info 是 Geany 识别一个 .so 是不是合法插件的关键name、description、version 都会显示在插件管理器的列表里。注意 Geany 大版本变换时这个结构体的字段会增删所以不能拿旧头文件编新插件否则插件管理器里会灰显。2.2 字符串替换式格式化为什么是玄学很多人第一次写 JSON 美化器会走捷径扫描字符串数花括号深度遇到{或[就加换行和缩进。文件小看不出问题一旦碰到 JSON 里的字符串值就会翻车。看这个例子{ msg: {\code\:0}, url: https://example.com/a?b{c} }如果用「数花括号」的方式处理字符串里的{}也被当成结构符号缩进立刻错乱更麻烦的是转义引号\和普通引号混在一起时字符串边界的判断很容易写错——你以为遇到双引号就结束了实际那是字符串内容的一部分。哪怕勉强处理对了还要自己实现转义字符保留、Unicode 处理、键值之间空格的规则等于重写一遍 JSON 规范。正确做法是先解析成语法树再从语法树重新输出文本解析器负责处理所有合法格式输出端只负责排版。2.3 json-c 和 cJSON 怎么选我一般选 json-c理由很直接。json_tokener_parse_ex 能拿到解析失败的错误类型和出错位置验证功能要靠它原生提供 JSON_C_TO_STRING_PRETTY美化输出不用自己拼缩进Debian/Ubuntu/Fedora 都有 libjson-c-dev 包apt 或 dnf 直接装不用往项目里扔第三方源码。cJSON 更轻适合嵌入式这种几 KB 内存的场景但它的解析错误定位能力弱一截对「验证并报错」这个需求不够。这个插件项目里用的是 json-c 路线我跟着这个方向走。有人会问不是有 jq 吗jq 确实能把 JSON 格式化得很好但它是外部进程在编辑器场景里用起来是另一套交互切到终端、跑命令、读输出、再贴回编辑器。插件和 jq 不是竞争关系插件解决的是「不离开编辑器的即时反馈」jq 解决的是「脚本管道里的文本处理」。我两个都用日常改配置用插件流水线脚本里用 jq。核心处理函数长这样#include json-c/json.h #include string.h /* 返回值TRUE 转换成功*out 里是格式化或缩小后的文本 */ static gboolean transform_json(const gchar *src, gchar **out, gboolean minify, gchar **error_msg) { struct json_object *root json_tokener_parse(src); if (root NULL) { *error_msg g_strdup(json_tokener_parse failed: not valid JSON); return FALSE; } if (minify) { *out g_strdup(json_object_to_json_string(root)); } else { *out g_strdup(json_object_to_json_string_ext( root, JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED)); } json_object_put(root); /* 释放解析出的对象树 */ return TRUE; }逻辑说明第一步 json_tokener_parse 把整段文本解析成 json_object 树这一步同时就是验证解析失败返回 NULL第二步根据 minify 参数选择输出形式缩小用紧凑模式美化用 PRETTY 标志最后 json_object_put 释放对象树避免内存泄漏。两个输出函数的区别在于json_object_to_json_string 单行输出、无多余空白加上 JSON_C_TO_STRING_PRETTY 之后会按两空格缩进输出多行文本数组和对象的结构层次一眼能看出。JSON_C_TO_STRING_SPACED 控制键值冒号后是否加空格一般和 PRETTY 一起用。这段值得多说一句解析成功但序列化后内容和原文不逐字节一致是正常的JSON 对象本身没有「原样」概念{}之间的空白、缩进、换行在解析后全部丢弃重新输出时按我们的排版规则生成。这一点正是格式化器区别于文本替换器的价值。2.4 验证路径要单独实现格式化时 parse 返回 NULL 只告诉我们「这段 JSON 坏了」用户体验上还差一步坏在哪一行。json-c 的 tokener 能做更细的错误定位struct json_tokener *tok json_tokener_new(); struct json_object *obj json_tokener_parse_ex(tok, src, strlen(src)); enum json_tokener_error err json_tokener_get_error(tok); if (err ! json_tokener_success) { int pos json_tokener_get_parse_end(tok); /* 用 pos 算行号列号拼成 line 3 column 12: unexpected character */ } json_tokener_free(tok);参数说明json_tokener_new 创建解析器上下文parse_ex 比 parse 多返回错误状态json_tokener_get_parse_end 返回的是错误发生处的字节偏移 pos注意是字节不是字符中文等多字节字符需要按 UTF-8 边界处理拿到 pos 后从文本开头数换行符就能算出第几行第几列这也是后面做「点击错误自动跳转」的基础。验证路径建议独立于格式化走先验证、再格式化避免两者耦合成一个按钮用户想看错误详情时反而被格式化结果干扰。顺便说一句选中区域格式化也是这类插件最常见的需求实现上只需要把 sci_get_contents 换成 sci_get_selected_text处理后用 sci_replace_sel 替换选中区核心 transform_json 不用改动入口层换两个函数就够了。3. 从源码编译成 .so 并装上 Geany依赖、路径与版本匹配3.1 编译需要哪三样东西编译这个插件要三类依赖。第一是 Geany 的开发头文件Debian/Ubuntu 系叫 libgeany-devCentOS/Fedora 系可能是 geany-develArch 上直接装 geany 本体就带它提供 geanyplugin.h 和 geany.pc没有它就没有 GeanyPlugin 结构体和 sci_get_contents 这些封装。第二是 json-c 开发包Debian/Ubuntu 叫 libjson-c-dev提供 json.h 和 pkg-config 元数据。第三是 pkg-config 本身用来把上面两个库的编译参数展开到命令行。装完之后先验证一步能同时查到 geany 和 json-c 才算齐了sudo apt install geany libgeany-dev libjson-c-dev pkg-config pkg-config --modversion geany json-c第二行命令会输出两个版本号如果某个报 not found说明对应开发包装的位置不在 pkg-config 搜索路径里。常见做法是把 PKG_CONFIG_PATH 指到 .pc 文件所在目录比如/usr/lib/x86_64-linux-gnu/pkgconfig。3.2 编译命令与 makefile 模板整个插件可以只从一个 .c 文件编出来gcc -stdc11 -fPIC -shared \ -o json_prettifier.so json_prettifier.c \ $(pkg-config --cflags --libs geany json-c)参数含义-stdc11 指定 C 标准-fPIC 是生成共享库必需的位置无关代码-shared 告诉编译器产物是 .so 而不是可执行文件pkg-config --cflags 展开出 -I 头文件路径--libs 展开出 -l 链接库和 -L 库路径。这里不用 -Wl,-soname 之类的花活Geany 按文件名识别插件。最容易翻车的地方是 geanyplugin.h 找不到现象是编译直接报 fatal error先确认 libgeany-dev 装了、pkg-config 能查到 geany。我一般建议把编译命令沉淀成 makefile方便改一个参数就重编CC ? gcc CFLAGS ? -stdc11 -Wall -Wextra -O2 PLUGIN : json_prettifier.so SRCS : json_prettifier.c $(PLUGIN): $(SRCS) $(CC) -fPIC -shared $(CFLAGS) \ -o $ $(SRCS) \ $(shell pkg-config --cflags --libs geany json-c) clean: rm -f $(PLUGIN) install: $(PLUGIN) install -Dm755 $(PLUGIN) $(DESTDIR)$(PLUGIN_DIR)说明CFLAGS 里加 -Wextra 是为了把 json-c 返回值忽略这类警告露出来install 目标用 DESTDIR 支持打包PLUGIN_DIR 在命令行传下面讲路径时用得上。实际编译时最常遇到的错误是 geany.pc 不在 pkg-config 搜索路径里发行版把 .pc 装到了 /usr/lib/pkgconfig 而系统走了 /usr/lib/x86_64-linux-gnu/pkgconfig这时把 PKG_CONFIG_PATH 指过去即可。3.3 .so 该放哪插件管理器从哪读Geany 查插件目录靠编译时的前缀和用户数据目录不同版本路径不完全一样常见做法是这两条系统级/usr/lib/geany/ 或者 /usr/lib/x86_64-linux-gnu/geany/用户级新版 Geany 习惯放 ~/.local/share/geany/plugins/老教程里常见的 ~/.config/geany/plugins/ 在部分发行版已经不被扫描最稳的确认方式不是猜路径而是跑一下geany --print-prefix这个命令输出 Geany 自己认为的安装前缀插件目录一般就是$prefix/lib/geany。把编译好的 .so 复制过去之后先做一次文件类型确认防止编译器因为参数问题产出 0 字节文件file json_prettifier.so readelf -d json_prettifier.so | grep -i neededreadelf 的 needed 段列出它链接的共享库如果缺 libjson-c.so 或者 libgeany 的版本号对不上加载时会直接报错这一步能提前暴露问题。Windows 和 macOS 上路径逻辑类似但 Geany 的插件机制这两个平台支持较弱如果主力是 Windows我更建议直接用 Linux 环境或者 WSL 编译省掉一半折腾。3.4 启用与验证Geany 菜单「工具 - 插件管理器」找到 Geany-JSON-Prettifier 打勾。这里有个习惯改插件状态后重启一次编辑器再确认菜单项因为部分版本启停只做了一半菜单没有即时刷新。验证成功的标志是 Tools 菜单下出现 Format JSON、Minify JSON、Validate JSON 三个动作。如果列表里这个名字灰显或者干脆看不到回头查 3.3 的放置路径和 readelf 结果多数是路径或依赖问题。启用之后第一件事是打开一个已知损坏的 JSON 文件跑 Validate确认消息窗口输出的错误行号和列号是对的再打开一个正常文件跑 Format确认缩进写回正常。这两步过了插件才算真正装好。4. 格式化、缩小、验证三合一快捷键绑定与参数调优4.1 三个动作的分工Format JSON美化整个文档处理缩进换行最常用。Minify JSON压成单行紧凑文本适合塞进命令行参数或者减少日志体积。Validate JSON只验证不重写出错时在状态栏和消息窗口给出错误行号列号。三个动作共用同一套核心只是输出端参数不同。我倾向于验证单独做因为验证失败时你不想失去原文件的任何信息。格式化是写回文档验证是只读操作两者语义要分清。有人会想做成一个按钮「坏了就格式化没坏就报错」实际体验很别扭你想确认文件有没有问题时按钮一旦顺手点成格式化缩进就被改了git diff 一片噪音。分开之后「我就想看看这个文件坏没坏」这个高频动作才足够安全。4.2 菜单与快捷键怎么绑Geany 主菜单挂菜单项有现成封装也可以在 plugin_init 里用 Gtk 容器直接加static void on_menu_minify(GtkMenuItem *item, gpointer user_data) { GeanyDocument *doc document_get_current(); json_format_document(doc, TRUE); } void plugin_init(GeanyData *data) { GtkWidget *tools_menu geany-main_widgets-tools_menu; GtkWidget *item gtk_menu_item_new_with_mnemonic(_(Minify JSON)); g_signal_connect(item, activate, G_CALLBACK(on_menu_minify), NULL); gtk_container_add(GTK_CONTAINER(tools_menu), item); gtk_widget_show(item); }说明geany-main_widgets-tools_menu 是 Geany 主窗口的 Tools 菜单容器把 GtkMenuItem 挂进去就出现在菜单里g_signal_connect 绑定 activate 信号点击菜单触发回调回调里 document_get_current() 拿当前文档传给核心处理函数。菜单项文字里的下划线表示助记符比如 _(Format JSON) 在菜单里显示为 F 带下划线AltF 触发。真正的自定义快捷键一般交给用户做Geany 的 keybindings 配置写在 ~/.config/geany/keybindings.conf插件注册的 keybinding 组会出现在「首选项 - 键位绑定」里用户可以改成任何组合。插件自己别硬编码全局快捷键容易和系统热键撞车。我一般设默认 CtrlAltJFormat、CtrlAltMMinify、CtrlAltVValidate真冲突了就让用户在键位绑定面板里改。4.3 参数怎么调缩进、数组换行、键排序json-c 的 PRETTY 输出默认两空格缩进这对很多人不够有人习惯四空格、有人要 Tab。常见做法是自己遍历 json_object 树做输出控制而不是吃 json_object_to_json_string_ext 的默认输出。下面这段是我常用的一种方式利用 json_object_object_foreach 遍历对象并手工控制缩进static void append_indent(GString *out, int depth, int width) { g_string_append_len(out, , depth * width); } static void dump_json_object(const struct json_object *obj, GString *out, int depth, int width) { enum json_type type json_object_get_type(obj); if (type json_type_object) { g_string_append_c(out, {); json_object_object_foreach((struct json_object *)obj, key, val) { g_string_append_c(out, \n); append_indent(out, depth 1, width); g_string_append_printf(out, \%s\: , key); dump_json_object(val, out, depth 1, width); } if (json_object_object_length(obj) 0) g_string_append_c(out, \n); append_indent(out, depth, width); g_string_append_c(out, }); } else if (type json_type_array) { /* 数组分支每个元素一行或按配置决定 */ } else { g_string_append(out, json_object_to_json_string(obj)); } }参数说明depth 当前嵌套深度width 是每级缩进的空格数运行时通过配置项传入json_object_object_foreach 是 json-c 的宏按插入顺序遍历对象键值对这正是「格式化后键不重排」的关键叶子节点直接复用 json_object_to_json_string 让 json-c 处理字符串转义和数字格式。这段输出逻辑和 json-c 内置 PRETTY 最大的区别是 width 可配置同时保留了字符串内的转义处理。数组每个元素占一行的开关也可以在这个框架里做判断数组元素是对象还是标量对象就每个占一行标量数组保持一行视觉上更紧凑这是我在实际用过一段时间后觉得最舒服的折中。4.4 配置项清单与持久化暴露在设置对话框里的参数我用这些配置项可选值说明indent_width2 / 4 / 8 / tab每级缩进宽度走 4.3 的自定义输出函数array_per_lineon / off数组元素强制每个占一行长数组更清晰sort_keyson / off对象键按字典序排序默认 off 保持原顺序allow_trailing_commaon / off容忍尾逗号默认 offJSONC 场景可开jump_to_erroron / off验证失败时把光标跳到错误行默认 onsort_keys 实现时注意json_object_object_foreach 是插入序要排序得先取出所有 key 放 g_strcmp0 排序再遍历。allow_trailing_comma 用纯 json-c 标准模式做不了常见做法是在源码里先剥掉最后一个逗号再解析副作用是错误行号偏移所以打开这个开关后错误定位要重新计算。配置持久化我用 Geany 的 prefs 机制本质是写 ~/.config/geany/geany.conf插件启动时读一次设置对话框里改完立即写回。提示改了缩进参数后建议先用 Validate 跑一遍样本文件再格式化正式文件防止自定义输出函数在数组嵌套边界出 bug。参数改完后用一个小测试集回归空对象、嵌套数组、带转义字符串、Unicode 中文、数字边界。我一般会准备五六个样本文件跑一遍格式化再跑一遍验证确认输出和 jq 的结果一致。回归这个动作不花多少时间但能挡住「改排序把转义弄坏」这类翻车。5. 避坑与排查版本不匹配、编码错乱、注释误报5.1 插件管理器里 .so 灰显现象把编译好的 json_prettifier.so 放进目录插件管理器列表里能看见名字但复选框是灰的点不动。原因用旧版本的 geanyplugin.h 编出的 .so 拿到新版本 Geany 上加载GeanyPlugin 结构体字段对不上插件机制认为它不是合法插件也可能是缺符号json-c 或 libgeany 的 soname 版本和编译时不匹配dlopen 直接失败。解决先跑 geany --version 确认当前版本用相同版本的 libgeany-dev 重编然后看启动日志Geany 用 --debug 参数启动时会打印 dlopen 失败的具体原因undefined symbol 会明确写出缺哪个函数。重编时顺手跑一下 readelf -d json_prettifier.so看 NEEDED 里的 libgeany 版本号和 /usr/lib 下实际存在的对比。这个坑我踩过两次每次都是升级系统后 libgeany-dev 变了、旧 .so 忘了重编。5.2 格式化后中文全变乱码现象原本正常的配置文件格式化后中文注释或字符串值变乱码英文数字正常有时还出现方框字符。原因文件不是 UTF-8 编码比如 Windows 传过来的 GBK 文件。Geany 用 UTF-8 读格式化后按 UTF-8 写回字节被二次解释自然乱码。解决动手前先看编辑器窗口右下角的编码提示如果不是 UTF-8先「文档 - 设置编码 - 转换为 UTF-8」再格式化。这是老玩家的条件反射换编码永远先转换再操作而不是直接保存。还要注意 BOM文件头有 BOM 时 json-c 会把 \xEF\xBB\xBF 当成非法字符报错位置在第一个字符看起来很像文件坏了实际只是 BOM 没剥。5.3 合法 JSON 被报错原来是注释和尾逗号现象前端工程里常见的带 // 注释、对象尾逗号的 json 文件插件报 parse failed错误位置指向注释或逗号附近。原因JSON 标准和 JavaScript 对象字面量不是一个东西标准 JSON 不允许注释也不允许尾逗号解析器严格按 RFC 8259 来。这类文件严格说叫 JSONC很多工程工具默认容忍但 json-c 的标准 tokener 不认。解决两个选择一是先把注释和尾逗号清掉再用二是打开配置里的 allow_trailing_comma。尾逗号可以用预处理剥掉注释只能靠逐行分析摘除但要注意字符串值里的 // 不能被误伤判断逻辑必须跟踪当前是否在字符串内部逃不开 2.2 节说的字符串边界问题。工程上如果是自己的项目我一般建议顺手把这类文件改成标准 JSON格式化器也好、其他工具也好兼容成本最低。5.4 大文件格式化把 Geany 卡死现象打开一个十几 MB 的 JSON 日志点格式化编辑器整个无响应几十秒期间 CPU 100%风扇狂转。原因json-c 把整段文本解析成完整对象树再序列化成另一个字符串内存和 CPU 都随文件规模二次放大Geany 是单进程编辑器主线程被占用界面就冻结。中途没有任何取消入口只能等它跑完。解决先用 Validate 确认文件是合法 JSON 再格式化避免「解析失败还要浪费一次完整遍历」如果文件确实大先 Minify 一次文本体积降下来再跑 PRETTY内存峰值能小不少。真正几十 MB 的日志型 JSON 不适合在编辑器内处理我一般的做法是先用脚本按顶层 key 切出问题片段格式化片段而不是整个文件这样既不卡git diff 也干净。5.5 格式化后 git diff 满屏飘红现象格式化完提交git diff 显示整个文件每个行都变了reviewer 根本没法看语义改动。原因原文件用的 CRLF 换行被写成了 LF或者缩进宽度从 4 变 2diff 按行比对时全部命中。不是格式化逻辑错了是换行符和缩进策略动了全局。解决提交前用 git diff --ignore-all-space -w 对比肉眼确认只有缩进变化如果不想污染历史把格式化限制在改动区块只格式化你正在改的对象。从此我每次动完 JSON 配置都会看一眼 diff 的统计行数如果改动行数远超实际修改范围优先怀疑换行符被整体改写而不是内容变了。6. 进阶技巧错误行跳转与命令行批量格式化6.1 验证失败自动跳到出错行json_tokener_get_parse_end 拿到的是字节偏移把偏移换成行号是验证体验的关键。行号换算用普通循环数换行符即可然后通过 Geany 的 Scintilla 封装把光标移过去static void jump_to_pos(GeanyDocument *doc, int byte_pos) { int line 0; const gchar *text sci_get_contents(doc-editor-sci, NULL); for (int i 0; i byte_pos text[i]; i) if (text[i] \n) line; sci_send_message(doc-editor-sci, SCI_GOTOLINE, line, 0); }说明sci_send_message 把 SCI_GOTOLINE 指令发给当前文档的 Scintilla 控件光标跳到出错行参数 line 是从 0 计数的行号所以不用加一。这个函数配合验证流程点一下 Validate 就能看到错误位置比状态栏里的一行文字直观太多。6.2 把核心函数复用成命令行工具插件里的 transform_json 完全可以在命令行场景复用。抽出相同逻辑编译一个独立小工具配合 find 批量格式化配置文件find . -name *.json -not -path */node_modules/* -print0 | \ xargs -0 -n 1 -P 4 jsonfmt --mode pretty --indent 4说明-P 4 表示四个并发进程处理几十个 json 文件时比逐个跑快得多--indent 4 对应插件里的 indent_width 配置命令行工具的缩进参数和 GUI 参数保持同一个默认值。用这种方式CI 里也能顺手加一步 jsonfmt --mode validate格式不合规直接失败。要从插件工程里长出一个 CLI只需要把 transform_json 放进单独 .c 文件main 里读文件、调 transform_json、写回整个改动不到一百行。这个便利是我后来体会到的最大的隐性收益插件和 CLI 共用同一套解析和输出逻辑GUI 里验证过的规则命令行批量跑一遍结果完全一致不会有两边行为漂移。从那以后我每次动 JSON 配置都强制走一遍验证、格式化、git diff -w 三步再批量交给 CLI 收尾这套流程用了很久都没翻过车希望帮到你。本文还有配套的精品资源点击获取