使用 Bash 规范化文件名和数据
URL 化:使用十六进制等效项将字母序列转换为安全的 URL。
这是我的第 155 篇专栏文章。这意味着我一直在为 Linux Journal 写作
$ echo "155/12" | bc
12
不,等等,那不对。让我们再试一次
$ echo "scale=2;155/12" | bc
12.91
是的,这么多年了。在 Linux 环境中编写关于 shell 脚本和轻量级编程的文章已经将近 13 年了。我涵盖了很多领域,但我想回到一些相当基础的东西,谈谈文件名和 Web。
过去,如果文件名中包含空格,就会发生糟糕的事情:“my mom's cookies.html”是灾难的根源,而不是美味的饼干——嗯,也不是那些 Web cookies!
然而,随着 Web 的发展,特殊字符的编码成为常态,每个 Web 浏览器都必须能够管理它,无论好坏。因此,空格变成了“+”或 %20 序列,所有不是常规字母数字字符的东西都被其十六进制 ASCII 等效项替换。
换句话说,“my mom's cookies.html”变成了“my+mom%27s+cookies.html”或“my%20mom%27s%20cookies.html”。许多符号也获得了第二生命,因此“&”、“=”和“?”都有了自己的含义,这意味着如果它们是原始文件名的一部分,也需要保护它们。如果你的原始文件名中包含“%”呢?啊,是的,编码事物的递归性质……。
因此,纯粹作为脚本编写练习,让我们编写一个脚本,将您交给它的任何字符串转换为“Web 安全”序列。在开始之前,先拿出一张纸,记下您将如何解决它。
为 Web 规范化文件名我的策略将很简单:将字符串分解为单个字符,分析每个字符以识别它是否是字母数字字符,如果不是,则将其转换为十六进制 ASCII 等效项,并在必要时在其前面加上“%”。
有很多方法可以将字符串分解为单个字母,但让我们使用 Bash 字符串变量操作,回忆一下 ${#var}
返回变量 $var
中的字符数,而 ${var:x:1}
将仅返回 $var
中位置 x
的字母。快点,索引是从零还是从一开始?
这是我将 $original
分解为其组成字母的初始循环
input="$*"
echo $input
for (( counter=0 ; counter < ${#input} ; counter++ ))
do
echo "counter = $counter -- ${input:$counter:1}"
done
回想一下,$*
是调用命令行中除命令名称本身之外的所有内容的快捷方式——一种让用户懒得引用参数的方法。它没有解决特殊字符的问题,但这就是引号的用途,对吧?
让我们用来自命令行的一些输入来试用一下这个片段脚本
$ sh normalize.sh "li nux?"
li nux?
counter = 0 -- l
counter = 1 -- i
counter = 2 --
counter = 3 -- n
counter = 4 -- u
counter = 5 -- x
counter = 6 -- ?
脚本中显然有一些调试代码,但通常最好保留它,直到您确定它按预期工作为止。
现在是时候区分 URL 中可接受的字符和不可接受的字符了。将字符转换为十六进制序列有点棘手,所以我使用了一系列相当晦涩的命令。让我们从命令行开始
$ echo '~' | xxd -ps -c1 | head -1
7e
现在,问题是“~”实际上是否是十六进制 ASCII 序列 7e。快速浏览 http://www.asciitable.com 证实,是的,7e 确实是波浪号的 ASCII 码。在该代码前加上百分号,转换的艰巨任务就完成了。
但是,您如何知道哪些字符可以直接使用?由于 ASCII 表的组织方式很奇怪,这将是三个范围:0-9 在表的某个区域,然后 A-Z 在第二个区域,a-z 在第三个区域。这是无法避免的,这就是三个范围测试。
在 Bash 中也有一个非常酷的方法来做到这一点
if [[ "$char" =~ [a-z] ]]
这里发生的事情是,这实际上是一个正则表达式 (=~
) 和一个范围 [a-z]
作为测试。由于我希望在每次测试后执行的操作是相同的,因此现在很容易实现所有三个测试
if [[ "$char" =~ [a-z] ]]; then
output="$output$char"
elif [[ "$char" =~ [A-Z] ]]; then
output="$output$char"
elif [[ "$char" =~ [0-9] ]]; then
output="$output$char"
else
显而易见,$output
字符串变量将被构建为具有所需的值。
还剩下什么?任何不是其他可接受字符的字符的十六进制输出。您已经看到了如何实现这一点
hexchar="$(echo "$char" | xxd -ps -c1 | head -1)"
output="$output%$hexchar"
快速运行一下
$ sh normalize.sh "li nux?"
li nux? translates to li%20nux%3F
看到问题了吗?如果不将十六进制转换为大写,它看起来会有点奇怪。“nux”是什么?这只是子 shell 调用中的另一个步骤
hexchar="$(echo "$char" | xxd -ps -c1 | head -1 | \
tr '[a-z]' '[A-Z]')"
现在,经过调整,输出看起来不错
$ sh normalize.sh "li nux?"
li nux? translates to li%20nux%3F
像元音变音或 n 波浪号这样的非 Latin-1 字符呢?让我们看看会发生什么
$ sh normalize.sh "Señor Günter"
Señor Günter translates to Se%C3B1or%200AG%C3BCnter
啊,当涉及到这些双字节字符序列时,脚本中存在一个错误,因为每个特殊字母都应该有两个十六进制字节序列。换句话说,它应该转换为 se%C3%B1or g%C3%BCnter
(我恢复了空格,以便更容易理解我在说什么)。
换句话说,这得到了正确的序列,但它缺少一个百分号——%C3B
应该是 %C3%B
,而 %C3BC 应该是 %C3%BC
。
毫无疑问,问题出在 hexchar
赋值子 shell 语句中
hexchar="$(echo "$char" | xxd -ps -c1 | head -1 | \
tr '[a-z]' '[A-Z]')"
是 xxd
的 -c1
参数吗?也许吧。我将把识别和修复问题作为留给您,亲爱的读者的练习。在您修复脚本以支持双字节字符的同时,为什么不也用“+”替换“%20”呢?
最后,为了使其尽可能有用,不要忘记在 URL 中也有许多符号是有效的,不需要转换,特别是“- _./!@#=&?” 这一组,因此您需要确保它们不会被十六进制化(这是一个词吗?)。