问题由来

前几天,青云一面的时候,面试官问了一个看似很简单——然而暗藏杀机的问题——如何删除C源文件里的注释?

我一听到这个,脑子里第一想法是「欸?这么简单吗?用 Py + 状态机分分钟可以撸出来吧?但是杀鸡焉用宰牛刀,那还是用 sed 吧。」

然后瞎说了一把用 「sed」 一套操作轻松带走——然而面试结束后一想——等等……不对劲啊……多行怎么处理呢……等等……我是不是走到了沟里……

实验

昨天跑去复习 Py 去了,今天才想起来,然后写了个测试用例,去做了做实验——完了,正常操作完全不能搞定啊!

于是 Google 了一波——发现有人说 sed 做不到这种操作,只能用 awk 保存状态来做,地址在这里;然后还有人写了一堆没有考虑特殊情况的代码,例如

  • sed '/\/\*/{:1;N;/\*\//!b1;s/\/\*.*\*\///}' 如果有夹在两段/**/注释中间的话,则会被删除
  • 下面的代码,不仅没有考虑到字符串内,而且也会出现如上问题
1
2
3
4
5
6
sed -e '/\/\*/{
:start
N;
/\*\//!b start
s@/\*.*\*/@@g;
}'

等等。

增加冗余解决问题

当然,如果 sed 不能做到的话,就自然没有这篇小文了,不过在实现的过程中,问题也比较多一点,下面一一列出

问题 解决
字符串内的注释 使用 b 标签 来无条件跳过
两段 /**/ 夹着代码,导致代码被删除 增加空 /**/ 冗余,使得 /**/ 成双成对删除
在变量声明里/前/后夹杂着 /**/ /**/ 前后都增加 \n(增加空行冗余) ,使得独立成行

由于 sed 是基于字节流处理,所以增加冗余基本上不会影响到性能(事实上这点冗余也无所谓)

于是,问题就这么被解决了。

测试、代码和结果

测试文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// 上面为空行
// 单行注释
int a = 1; // 后注释

int b = 2; /* 后注释 */

/* 前注释 */int c = 3;
/*单行注释*/

/*前后注释*/int d = 4;/*前后注释*/int e = 5;/*前后注释*/

/*两边包围的注释*/
int f = 6;
/*两边包围的注释*/

int /*神经病注释*/g /*神经病注释*/ = 7;

char* h = "/*字符串里*/";
char* i = "//字符串里";

/*
多行注释
*/

脚本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/bash
# 作者:Thoxvi
# 邮箱:A@Thoxvi.com
# 时间:2018年 04月 01日 星期日 13:15:50 CST
# 参数
# $1: 需要删除注释的 C/C++ 源程序
# 输出:删除注释和空行后的源程序

cat $1|
# 在 /**/ 的前后添加 \n 防止误删同行数据,要使用 g ,否则如果碰上前后注释则无法全部处理
# 当遇到了 "/**/" 和 "//xxx" 的情况,要用 b 来跳过,否则字符串里的数据也会被当成注释清楚
# 这里因为无法处理“神经病注释”,所以使用了增加冗余的方式,使所有“两边包围注释”成对出现,使得可以完成删除功能
# 不过唯一的遗憾就是无法保持 int a=1;/**/int b=2; 的 a,b 同行的效果——虽然这么可读写不怎么高
sed '
/"\/\*.*\*\/"/b skip
s/\/\*/\n\/\*\*\/\n\/\*/g
s/\*\//\*\/\n\/\*\*\/\n/g
:skip'|
# 删除处理后的注释、空行
sed '
/"\/\*.*\*\/"/b skip
/"\/\/.*"/b skip
s/\/\/.*//
/\/\*/,/\*\//d
/^\s*$/d
:skip'

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
➜  桌面 ./remove_comments.sh test_data.h
int a = 1;
int b = 2;
int c = 3;
int d = 4;
int e = 5;
int f = 6;
int
g
= 7;
char* h = "/*字符串里*/";
char* i = "//字符串里";

测试表明:就算是注释掺杂在变量的声明里、夹着代码,程序也可以正常工作。