用C语言手写Unix wc命令:从文件I/O到状态机

发布时间:2026/8/30 2:11:26

用C语言手写Unix wc命令:从文件I/O到状态机
把 Unix 自带的wc命令用 C 语言重新实现一遍是学习文件 I/O、命令行参数解析和文本状态机时非常值得完成的一个小项目。wc表面上只做三件事数行、数词、数字节但真正动手后会发现缓冲读取、标准输入、多个文件聚合、错误处理、输出列宽对齐这些细节一个都绕不开。这篇文章记录的是一个不借助 AI 完成的项目手工实现一个 Unix wc clone命名为mywc并用系统自带的wc做差分验证。文章会按“先理解行为、再准备环境、然后实现、接着验证、最后排错和扩展”的顺序展开。读者不需要很深的 C 基础只要能写循环、结构体和函数并愿意阅读命令行工具的帮助文档就能跟着走完整个项目。学完之后你不仅拥有了一个能跑的命令行工具更重要的是理解了一个 Unix 标准命令背后隐藏的边界条件和设计取舍。1. 先理解 wc 到底在统计什么复刻前必须先对齐行为1.1 wc 不是“数几行文字”这么简单很多初学者第一次执行wc -l file时会以为它在“统计文件里有多少行文本”。这个理解在大多数情况下能对上结果但在无尾换行文件、空文件、CRLF 换行文件面前会立刻失准。Unix 的wc统计的是字符特征而不是人类语义。行数统计的是换行符\n的个数不是“屏幕上的行数”。词数统计的是由空白字符分隔的连续非空白序列数量。字节数统计的是从输入里读到的原始字节个数。字符数-m统计的是当前 locale 下的多字节字符个数和字节数不一定相等。一个典型的反直觉例子文件内容只有hello没有结尾换行符。此时wc -l输出 0wc -w输出 1wc -c输出 5。如果你还停留在“一行文本就应该算一行”的思路里这个结果会让你困惑。1.2 行、词、字节与字符的语义边界把几个核心统计项的定义逐条列出来代码才能写得准统计项精确定义典型易错点行数输入中\n的出现次数无尾换行时不会因为“有文本”就记一行词数被空白字符分隔的连续非空白片段数连续多个空格、Tab 不能产生多个词字节数读入的原始char数量中文字符按 UTF-8 编码时一个字符占 3 字节字符数通过多字节编码解析出的字符数量需要 locale 和mbrtowc等函数支持词数尤其容易写错。如果只写“遇到空格就加一”那么hello world中间的连续空机会让你多数词。正确做法是维护一个状态当前是否处于“正在读取一个词”的状态。遇到空白字符进入“不在词中”状态遇到非空白且之前不在词中才把词数加一。1.3 常用参数与默认行为wc的常用参数如下参数含义与-c的区别-l只输出行数只数\n-w只输出词数按空白分隔-c只输出字节数原始字节不解析编码-m只输出字符数需要多字节编码解析-L输出最长行的长度GNU 扩展选项如果不带任何参数GNUwc默认按“行数、词数、字节数”三列输出每一列右对齐到固定宽度。传入多个文件时最后一列会多出一行total汇总。没有文件参数时从标准输入读取并且不打印文件名。理解这些行为之后才能进入编码阶段。否则写出来的程序在几个“常规文件”上看起来正常一遇到边界用例就立刻暴露问题。2. 明确这份 C 项目的技术边界和标准再开始动笔2.1 学习环境与工具链准备这个项目主要使用 POSIX 接口getopt、unistd.h、stdio.h的文件操作。最合适的环境是 Linux、macOS或者在 Windows 上使用 WSL。开始前先确认工具链存在gcc --version make --version如果提示找不到命令说明编译器或构建工具没有安装。在 Ubuntu/Debian 上可以安装build-essentialmacOS 安装 Xcode Command Line Tools。对于使用 VS Code 的初学者建议安装 C/C 扩展但实际编译不要依赖扩展自带的一键运行而是打开终端窗口直接执行gcc或make。这样你能清楚看到编译过程、告警信息和工作目录排查问题时更有把握。注意Windows 原生环境下getopt不一定可用。如果要在 Windows 上编译建议使用 MinGW-w64 提供的 POSIX 环境或者直接进入 WSL 操作否则还要额外移植参数解析代码。2.2 项目文件结构先建立一个干净的项目目录wc-clone/ ├── mywc.c ├── Makefile └── test/ ├── empty.txt ├── basic.txt ├── no_newline.txt └── multi.txttest目录用来存放边界测试用例。后面验证阶段会频繁用到这些文件建议从一开始就建好。empty.txt空文件测试最基础的零值输出。basic.txt包含多行、多词、连续空格的普通文本。no_newline.txt内容正常但末尾没有换行符。multi.txt一个有换行符的普通文件用于多文件 totals 测试。文件内容不要一开始就编得很复杂越简单越容易对照预期结果。2.3 用 Makefile 控制编译选项Makefile 不只用来减少输入命令的次数还负责固定编译标准、告警等级和输出名称CC gcc CFLAGS -stdc11 -Wall -Wextra -pedantic -O2 TARGET mywc $(TARGET): mywc.c $(CC) $(CFLAGS) -o $ $ clean: rm -f $(TARGET) .PHONY: clean编译标准用-stdc11告警开启-Wall -Wextra -pedantic。这组选项会把未使用变量、隐式转换、缺少函数原型等常见 C 问题尽早暴露出来。对第一次用 C 写完整项目的人来说编译器的告警就是第一层代码评审。3. 核心实现从读取输入到统计输出的完整链路3.1 用 getopt 解析命令行参数C 程序接收命令行参数后不能直接假设argv[1]就是文件。因为选项可能出现在文件前面也可能混合摆放。getopt是 POSIX 提供的标准参数解析函数它能处理-l -w -c这种分开写的选项-lwc这种合并写的选项选项结束后剩下的非选项参数就是文件列表。先定义全局状态和选项标记#include stdio.h #include stdlib.h #include string.h #include ctype.h #include locale.h #include getopt.h #include unistd.h #include errno.h #define BUF_SIZE 65536 #define COLUMN_WIDTH 7 typedef struct { unsigned long long lines; unsigned long long words; unsigned long long bytes; } Counts; static int opt_lines 0; static int opt_words 0; static int opt_bytes 0; static int has_opt 0; static int exit_status EXIT_SUCCESS;这里用unsigned long long作为计数类型避免大文件把int溢出。exit_status用于记录是否发生过文件打开或读取失败最后返回给操作系统。主函数里用getopt循环解析static void usage(const char *prog) { fprintf(stderr, Usage: %s [-lwc] [file...]\n, prog); } int main(int argc, char **argv) { int opt; setlocale(LC_CTYPE, ); while ((opt getopt(argc, argv, lwcm)) ! -1) { switch (opt) { case l: opt_lines 1; has_opt 1; break; case w: opt_words 1; has_opt 1; break; case c: opt_bytes 1; has_opt 1; break; case m: /* 本版本先不实现多字节字符统计 */ fprintf(stderr, %s: -m is not implemented in this version\n, argv[0]); return EXIT_FAILURE; default: usage(argv[0]); return EXIT_FAILURE; } } /* 暂未实现 -m其余代码略 */ }has_opt非常重要它记录用户是否显式指定了某个选项。如果为 0后面默认输出三列如果为 1只输出被指定的列。GNUwc在同时指定多个选项时列的顺序固定为行数、词数、字符数、字节数、最大行长度而不是按用户在命令行写的顺序。3.2 单个文件计数函数核心计数逻辑应该独立成一个函数只负责从一个FILE *读取数据并更新计数结构static int count_stream(FILE *fp, Counts *cnt) { char buf[BUF_SIZE]; size_t n; int in_word 0; memset(cnt, 0, sizeof(*cnt)); while ((n fread(buf, 1, sizeof(buf), fp)) 0) { for (size_t i 0; i n; i) { unsigned char ch (unsigned char)buf[i]; if (ch \n) { cnt-lines; } if (isspace(ch)) { in_word 0; } else if (!in_word) { in_word 1; cnt-words; } cnt-bytes; } } if (ferror(fp)) { return -1; } return 0; }这里有几个关键设计用fread分块读取而不是单字节fgetc循环避免每个字节都走一次文件指针层和系统调用路径。行数统计只认\n。\r在 Unix 文本里不作为行结束标志。词数用in_word状态变量控制解决连续空白字符造成重复计数的问题。ch强制转成unsigned char然后传给isspace。C 标准要求isspace这类函数接收的参数必须是unsigned char或EOF如果直接把char传进去遇到负数时是未定义行为。3.3 支持标准输入和多个文件外层需要一个“文件包装函数”负责打开文件、区分标准输入、关闭文件和错误输出static int count_file(const char *name, Counts *cnt) { FILE *fp; if (name NULL || strcmp(name, -) 0) { fp stdin; } else { fp fopen(name, rb); if (fp NULL) { fprintf(stderr, mywc: %s: %s\n, name, strerror(errno)); exit_status EXIT_FAILURE; return -1; } } int rc count_stream(fp, cnt); if (name ! NULL strcmp(name, -) ! 0) { fclose(fp); } if (rc ! 0) { fprintf(stderr, mywc: %s: read error\n, name ? name : -); exit_status EXIT_FAILURE; return -1; } return 0; }name NULL表示没有传任何文件名从标准输入读取。name为-时也读标准输入但输出时要显示文件名为-。打开模式使用rb。二进制模式在 Windows 上可以避免\r\n被自动转换成\n保证字节统计和系统wc一致。单个文件出错时不应该终止整个程序而是输出错误信息、设置退出码然后继续处理后续文件。主函数根据optind判断文件列表是否为空int main(int argc, char **argv) { int opt; setlocale(LC_CTYPE, ); while ((opt getopt(argc, argv, lwcm)) ! -1) { switch (opt) { case l: opt_lines 1; has_opt 1; break; case w: opt_words 1; has_opt 1; break; case c: opt_bytes 1; has_opt 1; break; case m: fprintf(stderr, mywc: -m is not implemented in this version\n); return EXIT_FAILURE; default: usage(argv[0]); return EXIT_FAILURE; } } Counts total {0, 0, 0}; int file_count 0; if (optind argc) { Counts cnt; if (count_file(NULL, cnt) 0) { print_counts(cnt, ); } return exit_status; } for (int i optind; i argc; i) { Counts cnt; if (count_file(argv[i], cnt) 0) { print_counts(cnt, argv[i]); total.lines cnt.lines; total.words cnt.words; total.bytes cnt.bytes; file_count; } } if (file_count 1) { print_counts(total, total); } return exit_status; }这里的 totals 累加只统计成功读到的文件符合常见wc的行为文件打开失败时对应文件不产生计数但程序仍然继续。3.4 输出格式对齐系统 wc输出函数的重点有两个列宽和是否显示文件名。static void print_counts(const Counts *cnt, const char *name) { if (has_opt) { if (opt_lines) printf(%*llu , COLUMN_WIDTH, cnt-lines); if (opt_words) printf(%*llu , COLUMN_WIDTH, cnt-words); if (opt_bytes) printf(%*llu , COLUMN_WIDTH, cnt-bytes); if (name ! NULL *name ! \0) printf(%s, name); putchar(\n); } else { printf(%*llu %*llu %*llu, COLUMN_WIDTH, cnt-lines, COLUMN_WIDTH, cnt-words, COLUMN_WIDTH, cnt-bytes); if (name ! NULL *name ! \0) printf( %s, name); putchar(\n); } }%*llu中的*表示宽度由参数决定这里统一用 7。这样即使行数从个位变成四位数输出的列对齐依然稳定。文件名只在非空时打印标准输入没有文件名时就不输出额外空格。4. 关键代码详解状态机、边界条件和字节与字符的区别4.1 用状态机判断词边界避免漏词和多数词词计数是wc实现里最容易写错的部分。直觉写法是“遇到空白字符就认为一个词结束”但这样无法处理连续空白。正确的思路是维护两个状态当前状态输入字符类型下一状态是否增加词数不在词中空白不在词中否不在词中非空白在词中是在词中空白不在词中否在词中非空白在词中否这个状态机保证了每个词只被记录一次边界只发生在“从不在词中变为在词中”的那一瞬间。连续空白、Tab、换行都不会造成重复计数。测试时要覆盖这些输入hello world hello world hello\tworld \nhello world\n尤其第二行中间有连续四个空格。如果实现成“遇到空格就记一个词结束”这一行会得到错误的词数。4.2 字节计数与字符计数的区别基础版本统计的是字节数。对于英文文本一个字符等于一个字节没有问题。对于中文UTF-8 编码下一个汉字占 3 个字节因此wc -c和wc -m的结果会不同。例如文件内容为你好wc -c输出 7因为“你好”占 6 字节加上换行符 1 字节。而wc -m输出 3因为“你好”是两个字符加上一个换行字符。要支持-m就不能单纯按字节遍历需要使用多字节转换函数。下面是一个扩展思路#include wchar.h #include wctype.h static int count_stream_mb(FILE *fp, Counts *cnt) { char buf[BUF_SIZE]; mbstate_t state; memset(cnt, 0, sizeof(*cnt)); memset(state, 0, sizeof(state)); int in_word 0; size_t n; while ((n fread(buf, 1, sizeof(buf), fp)) 0) { const char *p buf; const char *end buf n; while (p end) { wchar_t wc; size_t r mbrtowc(wc, p, end - p, state); if (r (size_t)-1 || r (size_t)-2) { /* 非法或不完整的多字节序列按单字节处理 */ cnt-bytes; cnt-chars; p; memset(state, 0, sizeof(state)); continue; } if (r 0) { r 1; } cnt-bytes r; cnt-chars; p r; if (wc L\n) cnt-lines; if (iswspace(wc)) in_word 0; else if (!in_word) { in_word 1; cnt-words; } } } if (ferror(fp)) return -1; return 0; }这段代码用于说明思路正式落地还需要考虑多字节字符跨缓冲区边界时mbrtowc返回(size_t)-2的情况。正确做法是把未完成的字节保留到下一次读取这会让代码复杂不少。对这个项目来说先把-l -w -c三个基础项做扎实更有价值。4.3 缓冲读取的策略和边界条件基础版本使用fread每次读取 64KB。这个值为什么不是 1 字节也不是整个文件如果每次读 1 字节程序会对每个字节都执行一次文件指针层的处理大文件下性能很差。如果一次性把文件读入内存遇到几十 GB 的日志文件时会直接内存溢出。64KB 是性能和内存占用之间比较均衡的选择。它让系统调用次数保持在可控范围又不会让缓冲区占用过多内存。在统计逻辑里还需要注意一次fread不一定读满整个缓冲区。返回值n表示实际读到的字节数循环只能遍历[0, n)不能假设buf被填满。5. 编译、运行与差分验证拿系统 wc 当裁判5.1 编译开关与常见告警使用 Makefile 编译make clean make如果只写了第一版代码大概率会看到几类告警未使用变量、函数没有原型、隐式声明。-Wall -Wextra -pedantic会把这些都暴露出来。开发阶段还可以临时加上 sanitizer 检查内存问题gcc -stdc11 -Wall -Wextra -O0 -g -fsanitizeaddress,undefined -o mywc mywc.c这个命令会在内存越界、未定义行为发生时输出具体位置适合找那种“运行看起来正常但偶尔崩溃”的隐患。5.2 构造覆盖边界条件的测试用例在test放几个固定文件并手工算出预期结果测试文件预期输出说明empty.txt0 0 0 empty.txt空文件所有计数都是 0no_newline.txt0 1 5 no_newline.txt没有\n时行数为 0词数为 1basic.txt看具体内容包含多行、多词、连续空格multi.txt看具体内容用于多文件 totals 验证先手工验证一个关键用例printf hello world\n test/basic.txt ./mywc test/basic.txt预期输出1 2 12 test/basic.txt三个数字分别是 1 行、2 个词、12 字节。hello world\n共 12 字节其中空格和换行各占 1 字节。再验证无尾换行文件printf hello test/no_newline.txt ./mywc test/no_newline.txt预期输出0 1 5 test/no_newline.txt5.3 与系统 wc 做 diff 验证手算只能覆盖小文件。要验证程序在很多文件上的行为最直接的办法是拿系统自带的wc当裁判用diff逐文件对比输出for f in test/*.txt; do diff (wc $f) (./mywc $f) echo OK: $f done还可以对比标准输入diff (printf a b\nc\n | wc) (printf a b\nc\n | ./mywc)如果diff没有任何输出说明两个程序的输出完全一致。这个方法比写单元测试更直观因为系统wc本身就是一个极好的“参考实现”。注意diff对比的是程序输出文本不是文件内容。只要输出的数字和文件名一致就说明计数逻辑正确。6. 常见问题与排查从现象倒推根因6.1 典型错误对照表第一次实现 wc clone 时常见的错误集中在以下几类| 问题现象 | 常见原因 | 检查方式 | 处理建议 | | --- | --- | ---

相关新闻

AI辅助写作技术教材:从大语言模型到RAG的工程实践

AI辅助写作技术教材:从大语言模型到RAG的工程实践

2026/8/30 2:01:26

你可能会在技术社区看到这样一个问题:我写了一本 AI 教科书,还要过多久 AI 能把它写得比我更好?我把这个问题当作一个工程问题来看,而不是一个预言类话题。答案是,从“生成一篇看起来像样的章节”到“生成一本读者愿意…

告别卡文:用工程化写作与AI辅助搭建小说创作工作台

告别卡文:用工程化写作与AI辅助搭建小说创作工作台

2026/8/30 2:01:26

写小说最容易卡住的地方,很多人一开始就忽略了。我说一个判断:卡文从来不是灵感问题,而是工程问题。 很多写作者遇到卡住,第一反应是“再等等灵感”“换个时间再写”,或者硬着头皮写下去,结果越写越别扭。…

2026前端面试实战:从八股到系统能力的考察逻辑与答题策略

2026前端面试实战:从八股到系统能力的考察逻辑与答题策略

2026/8/30 2:01:26

先说实话,去年到今年我陆陆续续面了三十多场前端岗,从大厂到创业公司都走过,也帮几个朋友改过简历、做过模拟面试。市面上的“前端面试题2026”汇总一大堆,但大部分只是把八股文堆在一起,背完就忘,面完就废…

AI听说训练机怎么选?从复读机到语音评测的技术拆解

AI听说训练机怎么选?从复读机到语音评测的技术拆解

2026/8/30 3:31:30

很多家长和英语学习者在挑“学习硬件”时,会陷入一种典型的参数思维:屏幕越大越好、内存越大越好、资源越多越好。看到“5.5英寸大屏AI精准听力口语训练机|MP3复读机|同步听力全科视频拍照搜题|64G内存|251…

MySQL误操作数据恢复实战:binlog回滚全流程解析

MySQL误操作数据恢复实战:binlog回滚全流程解析

2026/8/30 3:31:30

“I got a bad idea..”这句话,几乎每个干过数据库运维或后端开发的人都默默在心里说过。尤其是当你刚执行完一条 UPDATE 或 DELETE,突然发现 WHERE 条件写错了、忘记加条件、或者连错了环境时,那句“糟了”会瞬间涌上来。这篇文章就来复盘一…

告别Thread.sleep:用分区串行与版本号保证并发写库顺序

告别Thread.sleep:用分区串行与版本号保证并发写库顺序

2026/8/30 3:31:30

某个版本的批量导入模块上线后,业务方连续反馈数据被覆盖。我检查了日志,同一用户的两条记录,明明先提交的那条,最后生效的却是后提交的旧数据。代码逻辑没有明显问题,唯一可疑的是当时那个“I got a bad idea”&#…

电子墨水屏UI开发规范:从显示原理到Python实战指南

电子墨水屏UI开发规范:从显示原理到Python实战指南

2026/8/30 3:31:30

之前做墨水屏信息看板时,我把普通网页的 UI 设计思路原样搬了过去,结果几乎处处碰壁:刷新时整屏黑闪、放下十几分钟后还留着上一帧的残影、小字号中文糊成一团、强光下浅灰色内容完全看不清。后来陆续做了几个电子墨水屏项目,才慢…

C#开发Windows平台BLE调试助手:从设备扫描到GATT通信实战

C#开发Windows平台BLE调试助手:从设备扫描到GATT通信实战

2026/8/30 3:31:30

简介:这是一套基于C#开发的低功耗蓝牙(BLE)调试助手源码,面向嵌入式通信、物联网设备调试及Windows平台蓝牙应用开发者,专为解决HC-08等BLE模块在Win10环境下的快速连接、服务发现与数据收发验证难题而设计。资源包共6…

实验室副主厨:AI如何把实验协议变成可执行步骤?

实验室副主厨:AI如何把实验协议变成可执行步骤?

2026/8/30 3:21:29

最近有一个项目在 Hacker News 的 Show HN 版块出现,名字叫 Sous.bio,一句话介绍是“your sous chef in the lab”。这个名字初看有趣,细想却切中了一个经常被低估的痛点:实验室里的科研工作者,日常工作节奏其实非常像…

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

2026/8/30 0:01:07

每年校招季我都会接触不少准备数据库方向笔试的同学,看到最多的状态就是:简历上写着“熟悉 MySQL”“了解索引优化”,一碰到数据库管理工程师的笔试卷,却在索引、事务、锁、备份恢复这些题目上翻车。网易这套 2018 校园招聘数据库…

数字电路时序基石:深入理解建立时间与保持时间

数字电路时序基石:深入理解建立时间与保持时间

2026/8/30 0:01:07

1. 这不是“背公式”的事:时间参数到底在约束什么你翻过数字电路教材,一定见过这两个词:建立时间(Setup Time)和保持时间(Hold Time)。它们常被并列写在触发器(Flip-Flop&#xff09…

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

2026/8/30 0:01:07

1. 项目缘起:从赛题到超声波测距机的诞生第八届蓝桥杯单片机设计与开发国赛的题目,我至今记忆犹新。它没有直接给出一个花哨的名字,而是用“超声波测距机”这个朴实无华的功能描述,精准地勾勒出了考核的核心。对于当时备赛的我而言…

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析

2026/8/30 0:01:07

每年校招季我都会接触不少准备数据库方向笔试的同学,看到最多的状态就是:简历上写着“熟悉 MySQL”“了解索引优化”,一碰到数据库管理工程师的笔试卷,却在索引、事务、锁、备份恢复这些题目上翻车。网易这套 2018 校园招聘数据库…

数字电路时序基石:深入理解建立时间与保持时间

数字电路时序基石:深入理解建立时间与保持时间

2026/8/30 0:01:07

1. 这不是“背公式”的事:时间参数到底在约束什么你翻过数字电路教材,一定见过这两个词:建立时间(Setup Time)和保持时间(Hold Time)。它们常被并列写在触发器(Flip-Flop&#xff09…

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

2026/8/30 0:01:07

1. 项目缘起:从赛题到超声波测距机的诞生第八届蓝桥杯单片机设计与开发国赛的题目,我至今记忆犹新。它没有直接给出一个花哨的名字,而是用“超声波测距机”这个朴实无华的功能描述,精准地勾勒出了考核的核心。对于当时备赛的我而言…

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

2026/8/28 7:35:26

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…

导师推荐!2026最新AI论文工具测评与实用推荐

导师推荐!2026最新AI论文工具测评与实用推荐

2026/8/28 7:34:51

2026年真正好用的AI论文工具,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

告别游戏崩溃:XCOM 2模组管理器的智能革命

告别游戏崩溃:XCOM 2模组管理器的智能革命

2026/8/28 7:34:35

告别游戏崩溃:XCOM 2模组管理器的智能革命 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/xc/xcom2-lau…