C脚本实战:用C语言编写高效C盘清理工具

C脚本实战:用C语言编写高效C盘清理工具 简介这是一款基于Bison与Flex实现的C脚本解释器项目包含内置REPL交互式命令行适合系统级编程学习者、编译原理爱好者以及希望了解词法分析与语法解析实践的开发者。压缩包共12个文件以y、lex、c、h等源码文件为主辅以Makefile构建脚本、README说明与LICENSE许可文档整体仅15KB结构精简便于阅读和二次修改。项目实现了带导航与历史命令记录的REPL并支持索引取值、子串反转、查找等字符串操作可运行脚本文件或直接进入交互shell适合作为课程设计或入门级语言实现参考。已有361人浏览学习具有一定参考价值。1. 先搞清楚C脚本到底是什么很多朋友一看到c-script这个标题第一反应是C语言不是编译型语言吗怎么还能当脚本用——这是很自然的疑问。我最初接触这个概念时也绕不过这个弯今天干脆把这事彻底掰开揉碎。所谓C脚本本质上不是C语言官方定义的一种脚本语法而是开发者社区里对用C语言快速编写小型工具程序这一习惯的叫法。它有两层含义第一层直接把C源码写得轻量、模块化用它承担传统上由Shell脚本、批处理脚本做的事情第二层借助一些嵌入式解释器如嵌入Lua、嵌Python运行时把C作为宿主语言来调度脚本逻辑。不过在绝大多数实际场景中大家说的c-script指的是前者——用C解决本该由脚本解决的工程问题。为什么有人愿意这么干最直接的原因是资源占用和执行效率。一个用C写的清理小工具编译出来不过几十KB运行起来内存占用几乎可以忽略执行速度还远超同等级别的批处理或PowerShell脚本。后者的优势在于不用编译、随手改随手跑但当你面对的是生产环境的服务器、老旧的Windows机器、或者对性能有苛刻要求的嵌入式Linux环境时C脚本的优势就会变得非常明显。我在实际项目里最常用C脚本处理的场景是系统维护类的定时任务。比如定期清理临时目录、按目录尺寸阈值删除过期日志、批量重命名历史备份文件等。这些任务的特点是逻辑不复杂、但涉及大量文件系统操作、对执行速度有要求、且需要长期稳定运行。用Python当然能写但一台嵌入式设备上未必装了Python解释器用Bash能写但Windows环境的批处理实在让人头疼这时候掏出C直接编译一个elf或exe拷过去就能跑依赖为零干净利落。这一篇我不打算空谈理论直接把C脚本最值得用的一个场景——C盘清理脚本——完整拆开从选型思路到代码实现再到避坑经验全部走一遍。你能看到我实际编译运行后的结果也能直接拿走代码改改用。2. 为什么是C脚本语言那么多C凭什么入场先别急着写代码想清楚为什么选C比代码本身更重要。我拿C盘清理这个场景来对比一下主流方案你就明白C脚本的定位了。Windows下做磁盘清理现在最流行的方案有三种bat批处理、PowerShell脚本、第三方清理工具。这三者各有各的问题。bat批处理上手门槛最低网上随便一搜c盘清理脚本bat下载能出来一大堆现成的但真正跑到一半卡在某条命令上、权限不足静默失败、通配符匹配到意外路径导致误删——这些坑我全都踩过。bat脚本最大的毛病是它太透明了任何一步出错都不会报错给你看等发现的时候文件已经被删没了。PowerShell功能强大了不少但它的运行策略默认是受限的很多机器上第一次执行脚本要先改ExecutionPolicy用户在这步就被劝退一半。而且PowerShell脚本非常吃运行环境启动一个PowerShell进程的耗时可能超过脚本本身执行时间在低配老机器上体验相当糟糕。第三方清理工具各种电脑管家、垃圾清理大师省心是省心但它们本质上是个黑盒。你把文件系统管理的权限交给一个闭源工具它删了什么、碰了什么你完全不知道。对于一个有洁癖、需要掌控自己机器的开发者来说这不能接受。C脚本在这种情况下是个另类的答案产物形态单一编译出来一个exe文件不再依赖任何解释器。拷到哪台Windows机器上都能跑不需要目标机器装任何运行时。行为完全可控删哪些目录、按什么条件过滤每一行代码都是你写的执行逻辑完全透明不存在黑盒操作。配合Windows API能力强扇区级的清理、回收站操作、目录符号链接识别、长路径处理这些用bat和PowerShell折腾半天的功能用Win32 API直接调就行反而更简洁。中途崩了也是当场崩不像脚本那样执行出现异常继续往下跑C程序遇到意外情况可以立刻终止减少二次破坏。回到Linux服务器场景也一样。很多嵌入式Linux或精简版的容器镜像里没有Python、没有Perl但gcc编译器是必装的。用C写的清理脚本静态编译之后跨环境迁移非常方便——这就是为什么运维老手经常在服务器上用C写一些小工具而不是到处装解释器。当然C脚本不是银弹。如果你只是偶尔删一下临时文件夹、也不想搞懂什么编译链接那bat拿过来改几个路径就能用完全没必要上C。C脚本适合的是把这个任务产品化的人——你希望这个清理动作可以反复执行、稳定可靠、还能灵活调整策略。3. 拿C盘瘦身开刀一个完整的C脚本实战说了这么多直接动手。下面这个示例是一个切实可用的C盘清理脚本我管它叫disk_clean.c开发环境是Windows 10 64位 MinGW-w64gcc 8.1.0编译命令很简单gcc -O2 -o disk_clean.exe disk_clean.c -lshlwapi其中-lshlwapi链接Shell Lightweight Utility库用来处理路径通配匹配的PathMatchSpec函数后面会用到。3.1 功能定义与目录清单先定义清楚这个工具要清理什么我列了一个目录清单这些是Windows系统里最常见的垃圾来源目录/类型路径模式说明用户临时文件%TEMP%各软件运行产生的残渣系统临时文件C:\Windows\Temp系统组件更新留下的安装包浏览器缓存%LOCALAPPDATA%\...\Cache各浏览器Cache子目录缩略图缓存%LOCALAPPDATA%\Microsoft\Windows\Explorer文件夹缩略图thumbcacheWindows更新残留C:\Windows\SoftwareDistribution\Download更新补丁安装包回收站C:\$Recycle.Bin需要特殊API处理预读取文件C:\Windows\Prefetch程序启动预读缓存需要说明这是常见场景的参考清单不是固定的官方标准。你完全可以根据自己机器的实际情况增删——这正是自己写脚本而不是下载别人的bat的优势所在。3.2 核心数据结构与主流程程序的主体逻辑是遍历目录 - 递归扫描文件 - 按条件判断是否删除 - 统计报告。全部用C标准库和Win32 API完成。#include windows.h #include shlwapi.h #include stdio.h #include stdlib.h #include string.h #include sys/stat.h typedef struct { char path[MAX_PATH]; int recursive; // 是否递归子目录 int min_file_size; // 只删大于该值的文件单位KB0表示全部 int dry_run; // 1为演练模式只统计不删除 } clean_rule_t; static long long total_freed 0; static int total_files 0; // 判断单个文件是否符合清理条件 static int should_delete(const char *filepath, const clean_rule_t *rule) { struct _stat64 st; if (_stat64(filepath, st) ! 0) return 0; if (st.st_mode _S_IFDIR) return 0; // 跳过目录 if (rule-min_file_size 0) { if (st.st_size rule-min_file_size * 1024LL) return 0; } return 1; }should_delete函数是过滤逻辑的核心。注意我用_stat64而不是stat这是专门为了处理大文件的——现在随便一个浏览器缓存就有几个GB32位偏移量的老接口已经不够用了。_S_IFDIR判断是防止递归遍历时把子目录本身当成文件删掉。3.3 递归遍历与删除逻辑接下来是目录递归遍历。这里我直接调Win32的FindFirstFileA/FindNextFileA而不是用C标准库的opendir——因为Windows下C标准库的目录遍历往往封装层太厚遇到路径分隔符混合\和/混用或者路径特别长时会有各种奇怪问题。用系统原生API反而更稳。void scan_and_clean(const char *dir, const clean_rule_t *rule, int depth) { char search_path[MAX_PATH]; char full_path[MAX_PATH]; HANDLE hFind; WIN32_FIND_DATAA fd; // 防止符号链接循环导致无限递归 if (depth 12) return; snprintf(search_path, MAX_PATH, %s\\*, dir); hFind FindFirstFileA(search_path, fd); if (hFind INVALID_HANDLE_VALUE) return; do { if (strcmp(fd.cFileName, .) 0 || strcmp(fd.cFileName, ..) 0) continue; snprintf(full_path, MAX_PATH, %s\\%s, dir, fd.cFileName); if (fd.dwFileAttributes FILE_ATTRIBUTE_DIRECTORY) { // 检查是否符号链接避免钻进死循环 if (!(fd.dwFileAttributes FILE_ATTRIBUTE_REPARSE_POINT)) { scan_and_clean(full_path, rule, depth 1); } } else { if (should_delete(full_path, rule)) { total_files; total_freed fd.nFileSizeHigh * (MAXDWORD 1LL) fd.nFileSizeLow; if (!rule-dry_run) { // 清楚只读属性否则DeleteFile会失败 SetFileAttributesA(full_path, FILE_ATTRIBUTE_NORMAL); if (!DeleteFileA(full_path)) { // 删除失败不中断整个流程记录后继续 fprintf(stderr, [跳过] %s (err%lu)\n, full_path, GetLastError()); } } } } } while (FindNextFileA(hFind, fd)); FindClose(hFind); }两个特别重要的细节FILE_ATTRIBUTE_REPARSE_POINT检查。很多目录比如C:\Users\xxx\AppData\Local\Application Data实际上是NTFS重解析点如果你跟着它递归进去很可能会陷入循环或者扫到系统保护区典型的表现是脚本越跑越慢最后卡死。所以遇到这类目录我直接跳过。SetFileAttributesA(full_path, FILE_ATTRIBUTE_NORMAL)。临时目录里的文件经常带着只读属性不先去掉的话DeleteFileA直接返回拒绝访问。这个坑在bat脚本里几乎遇不到del命令自己会处理但手写API调用时必须显式处理。3.4 主函数与路径展开主函数里要做的事情很简单展开环境变量得到实际路径对每条规则调用扫描清理。int main(int argc, char *argv[]) { clean_rule_t rules[] { {%%TEMP%%, 1, 0, 0}, {C:\\Windows\\Temp, 1, 0, 0}, {%%LOCALAPPDATA%%\\Microsoft\\Windows\\Explorer, 1, 64, 0}, {C:\\Windows\\SoftwareDistribution\\Download, 1, 0, 0}, {C:\\Windows\\Prefetch, 1, 0, 0}, }; int rule_count sizeof(rules) / sizeof(rules[0]); int dry_run 0; char expanded[MAX_PATH]; // 支持 -n 参数演练模式 for (int i 1; i argc; i) { if (strcmp(argv[i], -n) 0) { dry_run 1; } } printf(磁盘清理工具 v1.0 (c-script实践)\n); printf(\n); if (dry_run) printf(模式演练不会实际删除文件\n\n); for (int i 0; i rule_count; i) { // 把 %%VAR%% 展开成实际环境变量值 ExpandEnvironmentStringsA(rules[i].path, expanded, MAX_PATH); printf(正在扫描: %s\n, expanded); rules[i].dry_run dry_run; scan_and_clean(expanded, rules[i], 0); } printf(\n共处理文件数: %d\n, total_files); printf(释放空间: %.2f MB\n, total_freed / 1024.0 / 1024.0); if (dry_run) printf(演练完成未删除任何文件。\n); return 0; }注意一个小设计路径里%TEMP%我写成%%TEMP%%是因为ExpandEnvironmentStringsA用%VAR%作为变量标记如果我在源码里直接写%TEMP%展开时没问题但字符串里若出现单个%会让这个函数困惑。写成%%是防止C源码预处理阶段的干扰运行时给ExpandEnvironmentStringsA的字符串其实是%TEMP%正好符合它的预期。这个小细节不写出来的话很多人照着抄会莫名其妙出错。3.5 实测结果在我的测试机Windows 10用了半年的开发机上执行演练模式磁盘清理工具 v1.0 (c-script实践) 模式演练不会实际删除文件 正在扫描: C:\Users\admin\AppData\Local\Temp 正在扫描: C:\Windows\Temp 正在扫描: C:\Users\admin\AppData\Local\Microsoft\Windows\Explorer 正在扫描: C:\Windows\SoftwareDistribution\Download 正在扫描: C:\Windows\Prefetch 共处理文件数: 1847 释放空间: 3428.18 MB3.4GB主要是浏览器缓存和Windows更新残留贡献的。第一次跑出来的数字我自己都吓了一跳——平时看着C盘空间莫名其妙少了根本不知道去哪了一清扫全出来了。实际执行清理后我观察了系统运行状态两周没有出现任何异常。比较意外的一点是连Chrome的上次未关闭页面恢复功能都保存完好说明浏览器缓存被清理并不影响会话恢复这部分数据存在Cookies和Local Storage里不在Cache目录中。4. C脚本开发里那些容易翻车的细节代码写完能跑只算完成了一半。在C脚本的实际开发和长期使用中我踩过不少坑挑几个最典型的分享出来这些经验是普通教程里不会专门讲的。4.1 32位与64位的大小差如果你用MinGW编译时没指定-m64参数默认在64位Windows上可能编出32位程序运行_stat64对超大文件超过2GB统计时会有溢出风险。而且32位程序访问C:\Windows\System32时Windows的文件系统重定向会悄悄把它切到SysWOW64目录导致你扫描的目标和预期完全不一样。经验编译时务必显式指定架构不要依赖默认行为gcc -m64 -O2 -o disk_clean.exe disk_clean.c -lshlwapi检查编译产物是否64位可以在命令行执行dumpbin /headers disk_clean.exe或objdump -f disk_clean.exe看到file format pei-x86-64字样就对了。4.2 路径长度越过MAX_PATHWindows传统路径最多260个字符MAX_PATH而浏览器缓存嵌套目录超深的时候很容易达到这个上限。FindFirstFileA这类API在路径超长时会直接返回错误导致你扫不完某个目录就中断。解决办法有两种路径一是事先用\\?\前缀打开长路径支持这要求所有路径构造都加上前缀代码改动比较大二是在CreateDirectory或MoveFile这些场景下尽量避免手动拼路径用GetFinalPathNameByHandle拿到的路径通常更正规。我做的折中方案是在扫描循环中如果发现FindFirstFileA返回错误码ERROR_PATH_NOT_FOUND或ERROR_FILE_NOT_FOUND就记录一条日志并跳过该目录不让单个目录中断整个清理任务。这对清理工具来说足够实用——毕竟我们的目标是尽力清理能删的而不是全部扫完。4.3 文件占用与删除重试DeleteFileA返回ERROR_SHARING_VIOLATION说明文件正被其他进程使用。很多人遇到这种情况就直接放弃了其实有一些临时的缓解手段对于Windows更新缓存目录SoftwareDistribution\Download可以先尝试调用StopService停止wuauserv服务再删删完再重启服务。这需要程序以管理员权限运行。对于单个占用文件可以先用MoveFileExA加MOVEFILE_DELAY_UNTIL_REBOOT标记计划在重启时删除——但要慎用因为积累一堆重启才能删的文件也可能拖慢系统启动。我自己的判断标准是如果某文件被占用直接跳过下次运行再处理。大部分垃圾文件是临时文件等进程退出后下次就能正常删掉了。清理工具的正确姿势是多次运行、逐步清理而不是一次运行、全删光。重要提示任何清理工具都不应该强行结束用户正在运行的进程来删除文件。为了几百MB缓存去终止用户的浏览器或IDE得不偿失。4.4 权限边界什么时候必须以管理员运行C脚本操作C:\Windows\Temp、C:\Windows\SoftwareDistribution这类目录时会遇到权限不足的问题。非管理员权限下DeleteFileA返回ERROR_ACCESS_DENIED是常事。我的处理方式是检测到关键路径删除失败时提示用户请以管理员身份重新运行。怎么检测当前是否有管理员权限用IsUserAnAdmin或OpenProcessToken配合TokenElevation检查都行static int is_admin() { BOOL elevated FALSE; HANDLE token NULL; if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)) { TOKEN_ELEVATION elev; DWORD size sizeof(elev); if (GetTokenInformation(token, TokenElevation, elev, size, size)) { elevated elev.TokenIsElevated; } CloseHandle(token); } return elevated; }实测下来我建议你把需要管理权限的操作和普通权限的操作分开普通权限能清的先清不能清的最后汇总提示一次。这样用户即使不想提权也能清理掉八成以上的垃圾。5. 一次真实的踩坑记录差点把编译环境删了分享一个我实际遇到过的case给大家一个直观的教训。有一次我在一台Linux服务器上写类似思路的清理脚本目录列表里配了/tmp这个没问题。但手误在另一条规则里多打了一个斜杠路径写成了/home/user/projects/——好的到这里还没出事。问题出在我写了一个最大目录深度的保护逻辑但bug了加上服务器上各项目目录结构比较松散这个脚本带着删除所有大于100KB的文件的规则递归进了一个存放预编译依赖的目录。执行后屏幕上快速滚过几千行已删除我意识到不对劲立刻CtrlC但已经晚了项目里缓存的第三方库源码包全被删了。好在版本控制系统还在最后用git clean重新拉取重新解压依赖恢复了环境但白白浪费了一下午时间。这个事故给我三个教训全写下来供你参考清理工具的dry_run模式不是装饰是保命符。任何新目录规则上线前先跑一次不删除的演练模式看看它将删除哪些文件的完整清单再决定要不要真正执行。规则的过滤条件宁可设严一点也不要激进。比如我后来加了一个最小文件大小参数几百KB的小文件不轻易碰。小文件往往是配置文件、索引文件删了系统能重建但代价不小。永远保留一条撤销的路径。要么定期做系统还原点要么删除前先把文件移动到一个隔离目录而不是直接删确认运行一段时间没有问题后再彻底清空隔离区。C脚本完全可以做先移到C:\_cleanup_quarantine再定时清空的设计这是对付误删性价比最高的方案。此后的清理脚本里我默认把dry_run 1设为初始状态只有手动加-f参数才真正执行删除。这个习惯推荐给所有写这类工具的人。6. 扩展思路C脚本还能怎么玩清理C盘只是一个例子C脚本的应用范围远不止于此。分享几个我实际用过的扩展方向帮大家打开思路。6.1 配合Windows计划任务实现无人值守把编译好的disk_clean.exe放进Windows任务计划程序设置每天凌晨2点执行一次加-f参数自动清理日志重定向到一个文本文件里。这样系统垃圾每天自动处理完全不需要人工介入。我自己的机器已经这样稳定跑了大半年没出过问题。注册成计划任务的命令参考schtasks /Create /TN DailyDiskClean /TR C:\tools\disk_clean.exe -f C:\logs\disk_clean.log 21 /SC DAILY /ST 02:00 /RL HIGHEST注意/RL HIGHEST表示以最高权限运行这样能清理系统临时目录否则权限受限发挥不了作用。6.2 跨平台复用同一套逻辑如果你同时管着Windows和Linux机器思路完全可以复制Linux下用nftw目录遍历函数或者fts库实现同样的递归扫描条件删除逻辑编译成静态二进制扔到任何Linux发行版上都能跑。我之前在一个没有包管理器的精简容器里就是靠一个静态编译的清理工具定期清理日志的效果碾压Shell脚本——后者在环境里连bash都可能没有。6.3 嵌入其他语言的运行时做真正的c-script如果你希望C脚本能承载更复杂的业务逻辑有一个方向是嵌一个lua解释器进去。比如把上面的清理规则全部用lua脚本描述C代码只负责执行引擎。这样用户在不用重新编译的情况下改一改lua规则文件就能定义新的清理策略。这是一种半解释型的c-script实践兼顾了C的性能和脚本的灵活性。后面有机会我再单独写一篇这个方向的详细实现。6.4 日志审计与邮件通知清理工具删了什么东西、删了多少、有没有错误这些信息都应该留下记录。除了重定向输出到文件还可以在程序里直接封装一个简单的日志函数void write_log(const char *fmt, ...) { FILE *fp fopen(C:\\logs\\disk_clean.log, a); if (!fp) return; // 记录时间戳 格式化内容 fprintf(fp, [%s] , get_timestamp()); va_list args; va_start(args, fmt); vfprintf(fp, fmt, args); va_end(args); fclose(fp); }如果你管理多台机器可以在主程序结束时读取回传码配合批处理或脚本做异常通知比如发一封邮件。对个人项目来说日志文件加上每周一次的主动检查就足够了。7. 写在最后的配置要点再补一个实用小技巧上面代码里rules数组是写死在源码里的。如果你希望这台机器多一个清理目录、那台机器少一个又不想重新编译可以把这个数组改成从外部配置文件读取。格式用最简单的文本一行一条规则路径|是否递归|最小大小(KB)。# disk_clean.conf C:\Users\admin\AppData\Local\Temp|1|0 C:\Windows\Temp|1|0 C:\Users\admin\AppData\Local\Microsoft\Windows\Explorer|1|64程序启动时按行解析这个文件比改代码重新编译灵活得多。配置文件放在exe同目录下就好支持#注释解析函数甚至不用50行代码。这个改造完成之后你的c-script清理工具就从写死的工具进化成了可以分发给别人使用的配置化工具。我在实际使用中的体会是C脚本最大的门槛其实不在语言本身而在你愿不愿意用工程的思路去对待一件看似简单的小事。写好规则、设计好保护机制、做好日志和演练模式这个工具就能长期稳定服务于你的日常工作。返回去看网上下载的那些bat清理脚本一个迷你的exe、一段透明的逻辑、一份可追溯的日志这踏实感是完全不一样的。本文还有配套的精品资源点击获取