
SerenityOS uniq 命令完全指南相邻重复行过滤的用法、选项与源码实现【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity本篇技术指南围绕 SerenityOSSerenity Operating System内置的uniq命令展开基于系统手册页 Base/usr/share/man/man1/uniq.md 整理并深入其实现源码 Userland/Utilities/uniq.cpp 与测试用例 Tests/Utilities/TestUniq.cpp。读完本文你将掌握uniq的全部命令行选项、字段/字符跳过规则的精确语义、典型管道用法以及它在 SerenityOS 内核源码树中的实现原理与验证方式。一、uniq 是什么相邻重复行的过滤器uniq是 SerenityOS 用户态工具集中用于文本处理的经典工具其职责是过滤掉输入中重复出现的相邻行repeated adjacent lines。输入来源指定的INPUT文件或标准输入stdin输出去向指定的OUTPUT文件或标准输出stdout。这里有一个需要重点强调的语义差异uniq只识别**相邻adjacent**的重复行而不是全局去重。也就是说同样的内容若在文件中分散出现、中间隔着其他行uniq并不会把它们合并。因此手册页明确建议先用sort(1)对输入排序再交给uniq处理这样才能得到全局去重的效果。对应的sort工具同样位于 SerenityOS 用户态手册页为 Base/usr/share/man/man1/sort.md支持-u不输出重复行、-n数值排序、-r逆序、-k指定排序字段等选项两者常组合成经典的sort | uniq数据清洗管道。命令语法Synopsis手册页给出的完整语法为$ uniq [-c] [-d|-u] [-f skip-fields] [-s skip-chars] [--version] [INPUT] [OUTPUT]方括号表示可选参数INPUT与OUTPUT均为可选位置参数缺省时分别使用标准输入与标准输出-d与-u是互斥的输出模式选项详见下文语义说明。二、选项详解Options手册页完整列出了 8 个选项其中短选项与长选项一一对应下表为全部选项及其含义选项长选项功能-c--count在每行输出前加上该行出现的次数-d--repeated只输出出现过重复的行-u--unique只输出唯一的行默认行为-i--ignore-case比较行时忽略大小写-f N--skip-fields N比较前跳过每行的前 N 个字段field-s N--skip-chars N比较前跳过每行的前 N 个字符char--help—显示帮助信息后退出--version—打印版本信息从实现源码看这些选项由 SerenityOS 的LibCore::ArgsParser统一注册解析见 Userland/Utilities/uniq.cppCore::ArgsParser args_parser; args_parser.add_option(duplicates_only, Only print duplicated lines, repeated, d); args_parser.add_option(unique_only, Only print unique lines (default), unique, u); args_parser.add_option(ignore_case, Ignore case when comparing lines, ignore-case, i); args_parser.add_option(print_count, Prefix each line by its number of occurrences, count, c); args_parser.add_option(skip_chars, Skip N chars, skip-chars, s, N); args_parser.add_option(skip_fields, Skip N fields, skip-fields, f, N); args_parser.add_positional_argument(inpath, Input file, input, Core::ArgsParser::Required::No); args_parser.add_positional_argument(outpath, Output file, output, Core::ArgsParser::Required::No); args_parser.parse(arguments);其中--help与--version并不需要工具自身注册而是由 ArgsParser.cpp 统一内置的通用选项Display help message and exit/Print version任何基于LibCore::ArgsParser的 SerenityOS 命令都自动获得这两个选项行为保持一致。关于 -d 与 -u 的互斥语义源码在参数解析完成后有一段关键逻辑Userland/Utilities/uniq.cppif (!unique_only !duplicates_only) { unique_only true; } else if (unique_only duplicates_only) { // Printing duplicated and unique lines shouldnt print anything return 0; }即两个模式都不指定时默认进入-uunique模式同时指定-d与-u时二者语义完全相反、无法同时满足程序直接返回成功退出码 0且不输出任何内容——这是一个符合逻辑的空集处理而非报错。输出过滤的核心逻辑无论选择哪种模式最终输出都收敛到write_line_content这个函数Userland/Utilities/uniq.cppstatic ErrorOrvoid write_line_content(StringView line, size_t count, bool duplicates_only, bool print_count, Core::File outfile) { if (duplicates_only count 1) return {}; if (print_count) TRY(outfile.write_until_depleted(ByteString::formatted({} {}\n, count, line))); else TRY(outfile.write_until_depleted(ByteString::formatted({}\n, line))); return {}; }-d模式下出现次数count 1的行被静默跳过只保留出现过重复的行-c模式下每行输出以次数 空格 行内容的格式前置计数其余组合默认-u模式、-i、-f、-s都在行比较阶段完成这里只负责决定打印/不打印与打印格式。三、跳过字段与字符的精确语义-f N与-s N是uniq最有技术含量、也最容易用错的两个选项手册页对它们的定义是-f N--skip-fields N比较前跳过每行的前 N 个字段-s N--skip-chars N比较前跳过每行的前 N 个字符。字段field如何界定字段的划分在源码的skip()函数中实现Userland/Utilities/uniq.cppstatic StringView skip(StringView line, unsigned char_skip_count, unsigned field_skip_count) { line line.trim(\nsv); if (field_skip_count) { bool in_field false; int field_index 0; unsigned current_field 0; for (size_t i 0; i line.length(); i) { char c line[i]; if (is_ascii_space(c)) { in_field false; field_index i; if (current_field field_skip_count) break; } else if (!in_field) { in_field true; } } line line.substring_view(field_index); } char_skip_count min(char_skip_count, line.length()); return line.substring_view(char_skip_count); }实现细节值得注意字段分隔符是 ASCII 空白字符。is_ascii_space定义于 AK/CharacterTypes.h涵盖空格、制表符tab等空白字符相邻多个空白符连续出现时只要仍处于空白状态就不会开启新字段通过in_field标志维持字段内部/字段间空白状态机字段数量达到N时从当前空白位置截断即被跳过的 N 个字段之间的分隔空白也被一并裁掉字段跳过完成后再执行字符跳过且char_skip_count会被限制为不超过剩余行长min(char_skip_count, line.length())因此-s的数值即使大于行长也不会越界比较前还会先把行尾换行符\ntrim 掉保证换行符不参与比较。手册示例的逐字符推演手册页给出了一个非常精巧的例子$ echo XXXX ABCD\nZZZZ BCAD | uniq -f1 -s4 ZZZZ BCAD按源码逻辑逐步推演两行步骤第一行XXXX ABCD第二行ZZZZ BCAD原始内容XXXX ABCDZZZZ BCAD-f1跳过第 1 个字段跳过XXXX剩余ABCD跳过ZZZZ剩余BCAD-s4再跳过 4 个字符跳过ABCD空格 3 个字符剩余D跳过BCAD空格 3 个字符剩余D跳过之后两行用于比较的内容都是D完全相等因此被视为重复行。按照-u默认模式只输出唯一行的规则重复行只保留第一行但最终输出的是ZZZZ BCAD而非XXXX ABCD这里需要说明手册页这个示例的语义重心在于演示-f1 -s4组合如何让两行在比较层面被判定为重复两个D相等输出内容展示的是保留下来的那一行原始文本。结合源码主循环见下文第四节可见uniq比较的是skip()之后的比较视图而输出的是未经跳过的完整原始行——这是理解-f/-s的关键跳过只影响比较不影响输出内容。测试用例佐证测试文件 Tests/Utilities/TestUniq.cpp 对字段与字符跳过做了精确断言TEST_CASE(skip_chars_flag) { run_uniq({ -s1 }, AAA\nAaA\nsv, AAA\nAaA\nsv); // 跳过1字符后 A vs a 不同 → 两行都输出 run_uniq({ -s2 }, AAA\nAaA\nsv, AAA\nsv); // 跳过2字符后 A vs A 相同 → 合并为一行 run_uniq({ -s200 }, AAA\nAaA\nsv, AAA\nsv); // 跳过量超过行长 → 视为全等 } TEST_CASE(skip_fields_flag) { run_uniq({ -f1 }, 1 AA\n2 AA\nsv, 1 AA\nsv); // 跳过首字段AA AA → 合并 run_uniq({ -f1 }, 1 a AA\n2 b AA\nsv, 1 a AA\n2 b AA\nsv); // 跳过首字段后 a AA vs b AA → 不同 run_uniq({ -f2 }, 1 a AA\n2 b AA\nsv, 1 a AA\nsv); // 跳过2字段后 AA AA → 合并 run_uniq({ -f200 }, 1 AA\n2 AA\nsv, 1 AA\nsv); // 跳过量超过字段数 → 视为全等 }这些用例同时验证了两个边界行为跳过量超过行长/字段数时剩余内容为空两行被判为相等如-s200、-f200跳过后再比较仍不相等时各行独立输出如-s1、-f1的第二组。四、源码主循环一次遍历完成相邻行统计uniq的核心算法是典型的滑动窗口单遍扫描位于 Userland/Utilities/uniq.cpp// The count starts at 1 since each line appears at least once. // Otherwise the -d and -c flags are off by one. size_t count 1; ByteBuffer previous_buf TRY(ByteBuffer::create_uninitialized(1024)); ByteBuffer current_buf TRY(ByteBuffer::create_uninitialized(1024)); StringView previous TRY(infile-read_line_with_resize(previous_buf)); StringView previous_to_compare skip(previous, skip_chars, skip_fields); while (!infile-is_eof()) { StringView current TRY(infile-read_line_with_resize(current_buf)); StringView current_to_compare skip(current, skip_chars, skip_fields); bool lines_equal ignore_case ? current_to_compare.equals_ignoring_ascii_case(previous_to_compare) : current_to_compare previous_to_compare; while (lines_equal current.length() 0) { // The docs say The second and succeeding copies of repeated adjacent input // lines shall not be written, therefore keep reading lines while they match previous. current TRY(infile-read_line_with_resize(current_buf)); current_to_compare skip(current, skip_chars, skip_fields); lines_equal ignore_case ? current_to_compare.equals_ignoring_ascii_case(previous_to_compare) : current_to_compare previous_to_compare; count; } TRY(write_line_content(previous, count, duplicates_only, print_count, *outfile)); count 1; swap(current_buf, previous_buf); // The StringViews cant be swapped since read_line_with_resize // potentially changes the location of the buffers due to reallocation. previous StringView { previous_buf.span().trim(current.length()) }; previous_to_compare skip(previous, skip_chars, skip_fields); }几个值得展开的实现要点行比较的三种模式默认按字节精确相等比较-i时改用equals_ignoring_ascii_case做 ASCII 大小写不敏感比较测试中AAA\nAaA\n在-i下合并为一行见ignore_case_flag用例。连续重复行的归并内层while循环持续读取与上一行比较视图相等的行每多读一行count直到出现不等的行或读到空行current.length() 0条件同时防止对 EOF 后残留的空串继续计数。计数从 1 开始源码注释特别说明计数初始为 1否则-d与-c会整体偏小 1——这是对 POSIX 行为对齐的一个细节。双缓冲交换技巧工具使用两个 1024 字节起步、可按需扩容read_line_with_resize的ByteBuffer每轮通过swap(current_buf, previous_buf)复用缓冲。由于扩容重分配会改变缓冲区地址StringView不能直接交换而是基于新缓冲重新构造视图previous StringView { previous_buf.span().trim(current.length()) }这一注释在源码中明确写明了原因。输出示例验证对应手册页的-c示例$ echo Well\nWell\nWell\nHello Friends! | uniq -c 3 Well 1 Hello Friends!三行连续的Well被归并为一行并计为 3Hello Friends!只出现一次计为 1与count_flag测试用例AAA\nAAA\nBBB\n→2 AAA\n1 BBB\n完全吻合。输入/输出打开方式工具通过LibCore::File的open_file_or_standard_stream打开输入输出Userland/Utilities/uniq.cpp该接口在未指定路径时自动回退到标准输入/标准输出输入侧额外包装了Core::InputBufferedFile以获得带缓冲的逐行读取。进程启动时还通过Core::System::pledge(stdio rpath wpath cpath)声明所需系统能力符合 SerenityOS 各用户态工具的权限最小化惯例。五、完整示例从手册到实战以下示例全部取自手册页 Base/usr/share/man/man1/uniq.md 并补充运行说明。1. 过滤文件中的重复相邻行并输出到标准输出$ uniq README.md从README.md读取过滤相邻重复行后打印到终端不修改原文件。2. 过滤后写入新文件$ uniq README.md UNIQUE.md第一个位置参数是输入文件第二个是输出文件若UNIQUE.md已存在则被覆盖进程 pledge 了wpath/cpath以支持写文件。3. 统计各连续相同块的出现次数$ echo Well\nWell\nWell\nHello Friends! | uniq -c 3 Well 1 Hello Friends!-c把每行出现次数作为前缀输出适合快速统计排序后文本中各行的频次分布。4. 跳过首字段与紧随的字符后再比较$ echo XXXX ABCD\nZZZZ BCAD | uniq -f1 -s4 ZZZZ BCAD-f1跳过第一个字段XXXX/ZZZZ-s4再跳过 4 个字符空格 及ABC/BCA两行剩余的比较内容均为D判定为重复详见第三节推演。5. 配合 sort 实现全局去重推荐用法$ sort file.txt | uniquniq只处理相邻行因此先排序、再去重是手册明确推荐的组合配合sort -u也可达到类似效果但uniq还能进一步输出计数-c、仅输出重复行-d等sort不具备的能力。6. 仅保留出现重复的行 / 仅保留唯一行$ uniq -d file.txt # 只输出有重复出现的行每组重复行仅打印一次 $ uniq -u file.txt # 只输出唯一行默认模式可省略六、构建与测试如何在仓库中验证uniq是 SerenityOS 用户态命令集Utilities的一员其构建源文件登记在 Userland/Utilities/CMakeLists.txt 的CMD_SOURCES_CPP列表中并通过第 240 行的安装清单随系统工具一并安装。文件本身仅依赖AK如StringView、ByteBuffer、CharacterTypes与LibCoreArgsParser、File、System库。针对uniq的行为验证集中在 Tests/Utilities/TestUniq.cpp测试通过LibTest框架驱动真实命令进程Core::Command::create(uniqsv, ...)并比对 stdout覆盖了以下场景两组相邻重复行合并two_duplicate_lines大小写不同视为不同行two_unique_lines超长行2047 字符与 65535 字符两档后一档超过内部流缓冲 64 KiB的正确归并long_line、line_longer_than_internal_stream_buffer-i忽略大小写ignore_case_flag-d仅输出重复行duplicate_flag-s/-f跳过字符与字段及其边界值skip_chars_flag、skip_fields_flag-c计数输出count_flag。这些测试既验证了用户可见行为也间接印证了双缓冲扩容read_line_with_resize在超长行场景下的正确性——这正是源码中StringView 不能直接交换注释所针对的边界条件。若要在本地复现可按仓库 Documentation/BuildInstructions.md 构建 SerenityOS 系统后运行对应测试集。七、小结SerenityOS 的uniq是一个麻雀虽小、五脏俱全的文本工具从手册页看它提供-c/-d/-u/-i/-f/-s六个核心行为选项加通用的--help/--version从源码看它用单遍滑动窗口 双缓冲的朴素算法实现了符合 POSIX 语义的相邻行过滤字段与字符跳过逻辑精确到空白字符级。理解只处理相邻行、跳过仅影响比较不影响输出、-d与-u互斥为空这三个关键点再配合sort | uniq管道就能充分发挥这个经典工具的威力。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考