简介:这是一份基于Bison与Flex构建的C脚本语言实现包,适合学习编译原理、词法/语法分析以及交互式解释器(REPL)设计的中高级开发者,也可作为系统级编程课程或编译原理实验的参考项目。项目源自高中系统级编程课程,代码组织完整,包含词法与语法规则定义、哈希表与工具函数实现、构建脚本和测试用例,能够直接运行并快速理解语言前端构造流程。压缩包共12个文件,主要类型为C源文件、头文件、Yacc/Lex规则文件、Makefile与说明文档,整体仅15KB,体积小巧,便于阅读、修改和二次开发。语言内置REPL,支持命令行历史导航,并实现了字符串索引、子串提取、逆序和查找等常用操作,可作为理解脚本引擎实现细节的微型示例。目前已有361人学习,对于想通过具体项目掌握Bison/Flex用法的读者,具有直接参考价值。 写C脚本(c-script)这个主题,得先从一个很现实的场景说起:C盘满了,网上到处都有人分享"C盘清理脚本bat下载",下载下来一看,无非就是几行del、rd命令,删的是Windows临时文件夹、浏览器缓存、系统更新残留。能用,但也就那回事。删的过程中经常遇到文件被占用、权限不足直接中断,有的bat代码里还混着一堆第三方下载链接,运行完还弹广告。我后来干脆放弃了纯bat方案,改用C语言写了一个"脚本化"的小工具——单文件、编译成exe、双击就跑,我把这类用C语言快速实现、像脚本一样即写即用的工具统称为C脚本(c-script)。这篇文章就把这套思路完整拆开,从概念、对比到代码实现,给出一套能直接落地的C盘清理方案。
1. C脚本到底是什么
1.1 我理解的c-script:用C语言写"即用即走"的工具
严格意义上,C是一门编译型语言,跟"脚本"这个词看起来不太搭。脚本通常指边解释边执行的代码,比如bat、Shell、Python。但在实际工作中,我理解的c-script是一种用法上的定位:用C语言编写一个功能单一、逻辑直接、面向具体任务的单文件程序,它的开发节奏完全是脚本式的——写、编译、运行、看结果,不行就改,30分钟出一个能用的小工具。它不需要搭建工程、不需要分层架构、不需要接口设计,本质上是把C语言当成"加强版脚本"在用。
这个定位和许多开源项目里叫"c-script"的东西也算对得上:把C代码嵌在文本文件里,通过一个解释器或加载器直接执行。比如有人用TCC(Tiny C Compiler)把C源码当脚本跑,有人写一个运行时动态加载.so/.dll,还有人做嵌入式系统里的C脚本引擎。它们的共同点是:保留C的表达能力,去掉"工程化"的包袱。
我在做C盘清理工具时用的就是这套思路。功能需求很明确:遍历指定目录、计算文件大小、删除过期临时文件、输出日志。这种逻辑用bat写会非常痛苦,变量处理、延迟展开、编码转换、错误码判断,每一处都在劝退。用Python写倒是舒服,但机器上不一定装了解释器,打包成exe体积动辄几十MB,杀毒软件还经常误报。C语言反而是那个"刚刚好"的选项——体积小、零依赖、直接调用Windows API处理文件路径和权限,天生的系统级操作能力。
1.2 和bat、Python相比,C脚本强在哪、弱在哪
拿C盘清理这个具体场景来对比,很多问题会看得更清楚。bat脚本的痛点在于:一是语法太简陋,几乎没有数据结构,复杂逻辑全靠标签跳转,写200行基本就是灾难现场;二是错误处理能力接近于零,del \s遇到权限不足只会打一行红字,没有try-catch,没有异常分支;三是编码问题顽固,现代Windows默认UTF-8,老系统又是GBK,中文路径在bat里来回乱码。
Python的问题在于分发:脚本模式需要目标机器装Python,exe模式打包体积大,而且PyInstaller打出来的程序常年被各种杀毒引擎标记为病毒,作为清理工具反而"名声在外"。
C脚本的优势比较明显:
- 编译产物是原生exe,体积可以做到几十KB到几百KB,一般不会触发杀软误报。
- 直接调用系统API,对Windows的文件权限、文件属性、回收站机制有最底层的控制力。
- 代码结构和错误处理是完整的编程语言级体验,写复杂逻辑不痛苦。
劣势也有:C的字符串处理、内存管理确实比现代脚本语言繁琐,写起来"手感"更重。但如果任务本身就是系统级的文件操作,C的标准库加Windows API基本是降维打击。
| 维度 | bat | Python脚本 | C脚本 |
|---|---|---|---|
| 运行依赖 | 系统自带 | 需解释器或打包 | 无依赖,原生exe |
| 体积 | 几KB | 打包后20-100MB | 30KB-1MB |
| 字符串与编码 | 灾难 | 优秀 | 一般但可控 |
| Windows API调用 | 受限 | 需第三方库 | 直接调用 |
| 错误处理 | 弱 | 强 | 强 |
| 开发速度 | 快但不可维护 | 较快 | 中等 |
这张表不是想说C有多万能,而是说明在"系统清理"这类需要高强度调用系统能力、又对分发体积有要求的场景里,C脚本是性价比很高的选择。
2. 写C盘清理工具之前,先把清理范围和边界想清楚
2.1 哪些目录值得清理,哪些不要碰
C盘"满"的原因很多,但大部分可安全清理的空间集中在几个固定位置。以Windows 10/11为例,我长期保持的经验值如下:
| 目录 | 说明 | 可清理性 |
|---|---|---|
C:\Windows\Temp | 系统临时文件,开机后很多已被释放 | 高,但部分文件被占用 |
%TEMP%(用户Temp) | 用户级临时文件,软件运行缓存 | 高 |
C:\Windows\SoftwareDistribution\Download | Windows更新下载缓存 | 高,但需先停止更新服务 |
C:\Windows\Prefetch | 预读取文件,加快启动速度用 | 中,删了会重建 |
| 回收站 | 用户删除的文件 | 高 |
C:\ProgramData\Microsoft\Windows\WER | Windows错误报告 | 高 |
| 浏览器缓存(Chrome/Edge) | 各类Web缓存 | 中,需识别具体路径 |
我选择的目标是前5种,并且有一个核心原则:只清空目录内的文件,不删除目录本身;只删除满足条件的文件,不按"全删"逻辑来处理。比如Prefetch里有个别正在被系统使用的文件,如果整体删除会报错中断,影响后面其他目录的清理流程。
2.2 安全底线:白名单优先,绝不盲删
C盘清理工具翻车案例网上太多了,基本都是"删了不该删的"。所以我在设计时的安全底线有三个:
第一,白名单优先。所谓白名单,是只清理我自己逻辑里明确指定的目录,而不是让用户输入一个路径就扫。这样可以防止手滑把C盘根目录整个扫一遍。
第二,按文件后缀和修改时间过滤。临时文件基本都有明确的特征,比如后缀是.tmp、.log、.old,或者文件名包含~号。我会把删除条件限定为"指定目录+指定后缀+修改时间超过7天",基本不会误伤。
第三,设计dry-run模式。也就是"演练模式",程序只统计、只报告、不实际删除。实际体验过的人都会发现,多这个模式等于多了一条命。
2.3 Windows API选型:搞定文件遍历和删除
在Windows上遍历目录,最常用的不是C标准库的opendir,而是Windows API的FindFirstFileW和FindNextFileW。原因很简单:标准库处理不了长路径和非UTF-8编码的系统文件名。用宽字符版本的API,配合GetFinalPathNameByHandleW,基本能覆盖绝大多数场景。
删除文件时用DeleteFileW,删除目录时用RemoveDirectoryW。如果要"清空目录但保留目录结构",就写成递归逻辑:先遍历删除文件,再递归进入子目录,最后子目录删空了就删掉整个空目录。
这里有一个很容易踩的坑:很多临时文件被设置成了只读属性。DeleteFileW遇到只读文件会直接失败,所以删除前需要调用SetFileAttributesW把FILE_ATTRIBUTE_READONLY清掉。这个细节在bat脚本里通常没法优雅处理,在C里就一行代码的事。
3. 实操:从零写一个可复用的C脚本清理工具
3.1 环境准备:MinGW-w64就够了
写C脚本不需要Visual Studio那种重量级IDE,装一个MinGW-w64编译器就够。我长期用的组合是:
- 编译器:MinGW-w64(X86_64编译器)或TCC(Tiny C Compiler,更轻)
- 编辑器:任意文本编辑器
- 构建方式:命令行gcc/gcc命令
MinGW-w64的安装方式这里不展开,装好后在命令行验证一下gcc --version能输出版本号即可。
用TCC也是一种很有意思的实践,它的编译速度极快,几百行的C代码编译时间小于0.1秒,非常接近"解释执行"的体验。如果你追求极致的"脚本感",TCC值得一试。后面说的编译命令以gcc为例,换成tcc基本也是同一套用法。
3.2 核心代码逐段拆解
下面这套代码是我实际在用的一个简化版,删掉了一些与具体机器相关的特殊目录,保留主干逻辑,方便直接参考复现。
#define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #include <string.h> #include <time.h> int dry_run = 1; // 1=演练模式,0=实际删除 long long total_freed = 0; // 释放空间统计 // 检查文件是否超过 N 天未修改 int is_old_enough(const char* path, int days) { WIN32_FILE_ATTRIBUTE_DATA attr; if (!GetFileAttributesExA(path, GetFileExInfoStandard, &attr)) return 0; SYSTEMTIME st; FILETIME lt; FileTimeToLocalFileTime(&attr.ftLastWriteTime, <); FileTimeToSystemTime(<, &st); time_t now = time(NULL); struct tm tm_last = {0}; tm_last.tm_year = st.wYear - 1900; tm_last.tm_mon = st.wMonth - 1; tm_last.tm_mday = st.wDay; time_t last_t = mktime(&tm_last); return (difftime(now, last_t) > days * 86400) ? 1 : 0; }这段函数的核心是时间判断。系统临时文件有个特点——很多文件在程序退出后就永远没人访问了,但它们不会自动消失。用"最后写入时间超过7天"作为过滤条件,能有效避开正在被运行的软件频繁写入的文件,同时把真正没用的旧文件筛出来。
接下来是删除逻辑和目录遍历:
void clean_directory(const char* base_path, int days) { char pattern[MAX_PATH]; char full_path[MAX_PATH]; WIN32_FIND_DATAA ffd; HANDLE hFind; snprintf(pattern, MAX_PATH, "%s\\*", base_path); hFind = FindFirstFileA(pattern, &ffd); if (hFind == INVALID_HANDLE_VALUE) return; do { if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue; snprintf(full_path, MAX_PATH, "%s\\%s", base_path, ffd.cFileName); if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // 递归清理子目录 clean_directory(full_path, days); // 子目录清空后可尝试删除该目录本身 if (!dry_run) { SetFileAttributesA(full_path, FILE_ATTRIBUTE_NORMAL); RemoveDirectoryA(full_path); } } else { // 文件:先判断时间,再删除 if (!is_old_enough(full_path, days)) continue; LARGE_INTEGER fsize; fsize.LowPart = ffd.nFileSizeLow; fsize.HighPart = ffd.nFileSizeHigh; if (dry_run) { printf("[模拟] %s (%.2f MB)\n", full_path, fsize.QuadPart / 1024.0 / 1024.0); } else { SetFileAttributesA(full_path, FILE_ATTRIBUTE_NORMAL); if (DeleteFileA(full_path)) { printf("[删除] %s\n", full_path); total_freed += fsize.QuadPart; } } } } while (FindNextFileA(hFind, &ffd) != 0); FindClose(hFind); }这段代码值得注意的有三个设计点:
一是先清空子目录再删空目录的顺序。如果不先递归清理子目录,直接尝试删除目录会因为目录非空而失败。这也是很多bat脚本清理不彻底的原因——它们用rd /s删除目录,一旦中间某一步被占用就整个中断,而不是"跳过占用文件,继续删其他文件"。
二是删除前先清掉只读属性。Windows临时目录里只读文件极其常见,bat里遇到只读文件会报"拒绝访问",C里一行SetFileAttributesA就解决了。
三是dry_run模式。运行时不加任何参数是所有命令的危险时刻,如果默认实际删除,很容易翻车。把默认值设为1(模拟模式),用户得明确加--exec参数才会真正执行删除,这个习惯可以推广到所有这类工具上。
3.3 主函数与参数设计:让工具像脚本一样灵活
接着是主函数。一个"脚本感"明确的小工具,参数设计应该遵循"默认安全、显式执行"的原则:
int main(int argc, char* argv[]) { printf("C脚本 - C盘清理工具\n"); if (argc > 1 && strcmp(argv[1], "--exec") == 0) { dry_run = 0; } const char* targets[] = { "C:\\Windows\\Temp", "C:\\Windows\\SoftwareDistribution\\Download", "C:\\Windows\\Prefetch" }; int days = 7; for (int i = 0; i < 3; i++) { printf("\n== 清理 %s ==\n", targets[i]); clean_directory(targets[i], days); } if (dry_run) { printf("\n[演练模式] 以上为待清理文件。确认无误后,使用 --exec 参数实际执行。\n"); } else { printf("\n清理完成,共释放 %.2f MB\n", total_freed / 1024.0 / 1024.0); } return 0; }运行方式非常简单:
# 先演练,看看会删什么 clean_c_drive.exe # 确认没问题后实际执行 clean_c_drive.exe --exec如果觉得每次都手动切到目录里运行太麻烦,可以把编译好的exe放到C:\tools\下,再去PATH里加上这个路径,以后在任意一个命令行窗口里敲clean_c_drive --exec就能清理,这个用法已经非常接近"C脚本"的定位了。
3.4 编译与分发:单exe怎么瘦身、怎么签名
编译命令很简单,我通常用下面这一行:
gcc -O2 -s -o clean_c_drive.exe clean_c_drive.c-O2是优化,-s是strip掉调试符号,能把体积从几十KB削到几KB。编译出来的exe复制到任何Windows 10/11机器上都能直接运行,不需要安装任何运行时。
如果还想更进一步,可以把几个小工具合并成一个exe,通过子命令区分,比如cleaner.exe temp、cleaner.exe cache、cleaner.exe all。这种用法的本质是一个"命令分发器",跟脚本工具集的思路很像,也是我后续扩展的方向。
要注意的是,如果分发给别人用,建议加个自定义数字证书或者至少加版本资源。未经签名的exe在SmartScreen上会弹"Windows已保护你的电脑",虽然几行代码的事却能极大降低使用者的心理防线。加版本资源需要写一个.rc资源文件,再配合windres工具,这里不展开,想了解可以查MinGW的资源编译文档。
3.5 和bat配合:让C脚本驱动旧批处理流程
有人会问:既然都有C脚本了,为什么还要提bat?实际使用中,bat依然有它的生态价值:双击执行、右键菜单、任务计划程序都原生支持bat。最优实践是"bat做壳,exe干活"。
写一个启动器bat:
@echo off chcp 65001 >nul cd /d C:\tools clean_c_drive.exe --exec pausebat只负责三件事:切代码页、切目录、调用exe。真正的清理逻辑全在C里面。这样既享受了bat的"入口便利性",又规避了bat写复杂逻辑的所有缺点。
更进一步的玩法是配合Windows任务计划程序,让这个工具每周一凌晨自动运行一次。任务计划里执行的对象直接指向clean_c_drive.exe --exec即可,不需要中间套bat。这样C盘清理就变成了无人值守的定时任务。
4. 常见问题与排查技巧实录
4.1 文件被占用导致删除失败,怎么不中断整个流程
现象:清理C:\Windows\Temp时,总有那么几个文件正在被系统或软件占用,DeleteFileA返回失败,但后续文件仍然可以正常删除。我在代码里对删除失败没有做特殊处理,只是跳过了,日志里继续打印下一个文件。
这里有个设计原则:清理工具的核心是"能删多少删多少",而不是"必须全删干净"。bat脚本常见的毛病就是遇到一个失败就终止整个循环。我建议在关键地方记录失败次数,最后汇总输出"有N个文件被占用,跳过",而不是用失败来打断后面的清理。
4.2 杀毒软件把exe误报了,怎么处理
MinGW静态编译出来的exe,理论上非常干净,但某些杀毒软件会基于"行为特征"判定:一个程序试图批量删除文件,本身就是"病毒行为"。遇到这种误报,常见的处理路径有三个:
- 代码层面:减少可疑API的调用密度,能合并的合并,删除前加日志输出,调整行为模式。
- 打包层面:UPX压缩虽然能减小体积,但反而更容易触发杀软脱壳引擎的扫描,非必要不用。
- 签名层面:申请一个代码签名证书,签名后的exe误报率会大幅下降。
我实际踩过的坑是UPX:为了把300KB压到100KB,结果360、Windows Defender双双拉黑。去掉UPX之后用-s参数,100多KB的体积不压了,反而一点问题没有。
4.3 bat调用中文路径乱码,怎么解决
很多人的实际工作流里,bat只是个启动器。如果bat里写了中文字符串,比如"清理完成",在中文Windows上经常先乱码再出错。根本原因是Windows命令行默认代码页是GBK(936),而源文件保存为UTF-8。解决方法是文件另存为UTF-8 with BOM格式,或者像前面代码里那样,在bat开头加一句chcp 65001 >nul切换到UTF-8代码页。
如果把逻辑全挪到C脚本里,bat只做调用,就没有乱码问题了。因为C源码文件里的字符串编码是编译时定死的,UTF-8中文在Windows控制台里可以通过SetConsoleOutputCP(CP_UTF8)来正常显示,不依赖外部编码设置。
// 在main()开头加一句,保证控制台正确显示UTF-8中文 SetConsoleOutputCP(CP_UTF8);4.4 内存越界和字符串缓冲区:C脚本的高频事故现场
C脚本虽然用起来爽,但也容易栽在字符串上。我在最初版本里用了不少char path[MAX_PATH],当遍历到深层目录时,路径超过260字节会导致缓冲区溢出。Windows 10支持长路径,但需要注册表开启LongPathsEnabled,同时API要用\\?\前缀。代码层面最稳妥的做法是动态分配路径缓冲区,不要写死大小。
另外一个实用的习惯是:每操作一张路径字符串,都顺手加一个if (strlen(src) >= MAX_PATH) continue;。虽然在工具里有点啰嗦,但能避免半夜清理C盘时突然崩溃的尴尬。
5. 后续扩展的一点建议
这套C脚本的框架一旦跑通,扩展是很自然的事。我现在除了C盘清理,已经把它扩展成了一个小工具集:一个exe支持clean temp、clean cache、clean update、clean recycle四个子命令,后续还计划加一个stats模式,专门输出C盘各目录的空间占用统计。再往下走,可以让它自动读取一个clean.ini配置文件,把白名单、保留天数、是否启用回收站清理都写成配置项,这样普通用户不需要碰代码也能自己调整策略。
从"一个C脚本清理C盘"出发,本质上是在示范一种开发方式:当你觉得写脚本语言太飘、写正式工程太重,C语言作为中间一档的"硬核脚本",在系统工具类场景里非常能打。它没有华丽的外壳,但稳定、直接、可控,就像工具箱里的扳手,不显眼但处处都用得上。
本文还有配套的精品资源,点击获取