源码链接
概述
这个cpp文件在src/common/backend/parser目录下,它提供了词法分析中的常用函数,这些函数被用来处理查询语句中的转义字符。在这篇博客里,我将介绍文件中的第一个函数scanstr()。
源码
//代码清单1
char* scanstr(const char* s)
{
char* newStr = NULL;
int len, i, j;
if (s == NULL || s[0] == '\0')
return pstrdup("");
len = strlen(s);
newStr = (char*)palloc(len + 1); /* string cannot get longer */
for (i = 0, j = 0; i < len; i++) {
if (s[i] == '\'') {
/*
* Note: if scanner is working right, unescaped quotes can only
* appear in pairs, so there should be another character.
*/
i++;
newStr[j] = s[i];
} else if (s[i] == '\\') {
i++;
switch (s[i]) {
case 'b':
newStr[j] = '\b';
break;
case 'f':
newStr[j] = '\f';
break;
case 'n':
newStr[j] = '\n';
break;
case 'r':
newStr[j] = '\r';
break;
case 't':
newStr[j] = '\t';
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7': {
int k;
unsigned long octVal = 0;
for (k = 0; s[i + k] >= '0' && s[i + k] <= '7' && k < 3; k++){
octVal = (octVal << 3) + (s[i + k] - '0');
}
i += k - 1;
newStr[j] = ((char)octVal);
} break;
default:
newStr[j] = s[i];
break;
} /* switch */
} /* s[i] == '\\' */
else {
newStr[j] = s[i];
}
j++;
}
newStr[j] = '\0';
return newStr;
}
解析
对于这个函数,如果传入的字符串 s 有转义字符,那么它会将转义字符映射到实际字符,这个是什么意思呢?
举个例子,比如字符串 "a\b’’c" ,现在各个字符对应的十进制ASCII码值为:
a —— 97
\ —— 92
b —— 98
’ —— 39
’ —— 39
c —— 99
将转义字符映射到实际字符之后,就变成了:
a —— 97
\b —— 8
’ —— 39
c —— 99
相信大家对 '\b' 并不感到陌生,它是退格符,但有一个疑惑,为什么两个单引号凑一起经转义后就变成了一个单引号,这是因为一个单引号它是SQl查询语句中的转义字符,就像两个反斜杠凑一起由 '\\' 转义变成了一个反斜杠 '\' 。
对于代码清单1中的代码,由于传入的字符串指针 s 是const char*类型的,这也就是说这个字符串是不可更改的常量,所以我们在第4行定义了一个指针变量newStr,并在第12行为它分配了内存空间,这样我们就可以用它存储指针 s 指向的字符串的副本,通过对这个副本进行操作,从而不会影响源字符串的内容。第7、8行,指的是如果指针 s 为空或者说它指向的字符串为空,那么结束程序。
然后就是第14至66行核心的for循环结构,主体上又分为3个判断结构:
1、字符为单个单引号
2、字符为单个反斜杠
3、其他普通字符
第一种情况
那么相应地,就出现了三种应对方式,先讲第一种:
//代码清单2
//字符为单引号
if (s[i] == '\'')
{
i++;
newStr[j] = s[i];
}
代码清单2中,如果一个字符为单个单引号,那么这个单引号在还未转义的字符串中出现,应当是成对出现的,所以有 i++ 的操作,因为 s[i] 应当也是单个单引号。
第二种情况
//代码清单3
//字符为单个反斜杠
else if (s[i] == '\\')
{
i++;
switch (s[i])
{
case 'b':
newStr[j] = '\b';
break;
case 'f':
newStr[j] = '\f';
break;
case 'n':
newStr[j] = '\n';
break;
case 'r':
newStr[j] = '\r';
break;
case 't':
newStr[j] = '\t';
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
{
int k;
unsigned long octVal = 0;
for (k = 0; s[i + k] >= '0' && s[i + k] <= '7' && k < 3; k++)
{
octVal = (octVal << 3) + (s[i + k] - '0');
}
i += k - 1;
newStr[j] = ((char)octVal);
} break;
default:
newStr[j] = s[i];
break;
}
}
在代码清单3中,当 s[i] 为单个反斜杠字符时(注:这里单个反斜杠字符是用 '\\' 来表示的),跟在它后面的字符可能是字母也可能是数字,所以这里又有三种情况:
1、当它的后一个字符为字母时,我们只需要判断其为 'b'、'f'、'n'、'r'、't' ,即判断其空白字符的种类,'\b' 即退格符,'\f' 即换页符,'\n' 即换行符,'\r' 即回车,'\t' 即水平制表符;
2、当它的后一个字符为数字时,最多能跟3个0~7的数,代表的是一个八进制的数;
3、其他种类的转义字符。
对于第1种情况,自然是将单个反斜杠字符和字母字符转义成特定的ASCII码即可,对应代码清单3中第8~22行。而对于第2种情况,第35~38行的for循环结构是十分重要的,它将反斜杠后连在一起的数由八进制转换为十进制。说到第3种情况,对于其它的转义字符比如 '\0' ,在我们将字符 '0' 前面的反斜杠符号存入数组后,还需要做的就是将字符 '0' 也存入数组,这就是第43行代码的用处。
第三种情况
//代码清单4
//其他普通字符
else
{
newStr[j] = s[i];
}
这个结构和第二种情况分类中的最后一个很相似,但不同的是,这个结构处理的是普通的数字字符或字母字符。
总结
通过对 scanstr() 函数的解析,我们清楚地发现这个函数只是负责处理空白字符和八进制数字转义字符,其它字符不做修改。