我需要一种在运行旧版本 busybox 的 OpenWRT 设备上使用 shell 脚本对字符串进行 URL 编码的方法。现在我得到了以下代码:
urlencode() {
echo "$@" | awk -v ORS="" '{ gsub(/./,"&\n") ; print }' | while read l
do
c="`echo "$l" | grep '[^-._~0-9a-zA-Z]'`"
if [ "$l" == "" ]
then
echo -n "%20"
else
if [ -z "$c" ]
then
echo -n "$l"
else
printf %%%02X \'"$c"
fi
fi
done
echo ""
}
这或多或少工作得很好,但有一些缺陷:
- 某些字符会被跳过,例如“\”。
- 结果是一个字符一个字符地返回,所以速度非常慢。仅对批处理中的几个字符串进行 url 编码大约需要 20 秒。
我的 bash 版本不支持像 ${var:x:y} 这样的子字符串。
答案1
[TL,DR:使用urlencode_grouped_case
最后一个代码块中的版本。]
awk 可以完成大部分工作,但令人烦恼的是它缺乏将字符转换为数字的方法。如果od
您的设备上存在 ,您可以使用它将所有字符(更准确地说,字节)转换为相应的数字(以十进制编写,以便 awk 可以读取它),然后使用 awk 将有效字符转换回文字并引用字符转换为正确的形式。
urlencode_od_awk () {
echo -n "$1" | od -t d1 | awk '{
for (i = 2; i <= NF; i++) {
printf(($i>=48 && $i<=57) || ($i>=65 && $i<=90) || ($i>=97 && $i<=122) ||
$i==45 || $i==46 || $i==95 || $i==126 ?
"%c" : "%%%02x", $i)
}
}'
}
如果您的设备没有od
,您可以在外壳内执行所有操作;这将显着提高性能(减少对外部程序的调用——如果printf
是内置程序则没有调用)并且更容易正确编写。我相信所有 Busybox shell 都支持${VAR#PREFIX}
从字符串中修剪前缀的构造;使用它重复删除字符串的第一个字符。
urlencode_many_printf () {
string=$1
while [ -n "$string" ]; do
tail=${string#?}
head=${string%$tail}
case $head in
[-._~0-9A-Za-z]) printf %c "$head";;
*) printf %%%02x "'$head"
esac
string=$tail
done
echo
}
如果printf
不是内置实用程序而是外部实用程序,则通过对整个函数仅调用一次而不是每个字符调用一次,您将再次获得性能。构建格式和参数,然后对printf
.
urlencode_single_printf () {
string=$1; format=; set --
while [ -n "$string" ]; do
tail=${string#?}
head=${string%$tail}
case $head in
[-._~0-9A-Za-z]) format=$format%c; set -- "$@" "$head";;
*) format=$format%%%02x; set -- "$@" "'$head";;
esac
string=$tail
done
printf "$format\\n" "$@"
}
就外部调用而言,这是最佳的(只有一个调用,除非您愿意枚举所有需要转义的字符,否则您无法使用纯 shell 构造来完成此操作)。如果参数中的大部分字符要原样传递,则可以批量处理它们。
urlencode_grouped_literals () {
string=$1; format=; set --
while
literal=${string%%[!-._~0-9A-Za-z]*}
if [ -n "$literal" ]; then
format=$format%s
set -- "$@" "$literal"
string=${string#$literal}
fi
[ -n "$string" ]
do
tail=${string#?}
head=${string%$tail}
format=$format%%%02x
set -- "$@" "'$head"
string=$tail
done
printf "$format\\n" "$@"
}
根据编译选项,[
(又名test
)可能是外部实用程序。我们仅将其用于字符串匹配,这也可以在 shell 中使用case
构造完成。以下是重写的最后两种方法,以避免test
内置的方法,首先逐个字符:
urlencode_single_fork () {
string=$1; format=; set --
while case "$string" in "") false;; esac do
tail=${string#?}
head=${string%$tail}
case $head in
[-._~0-9A-Za-z]) format=$format%c; set -- "$@" "$head";;
*) format=$format%%%02x; set -- "$@" "'$head";;
esac
string=$tail
done
printf "$format\\n" "$@"
}
并批量复制每个文字段:
urlencode_grouped_case () {
string=$1; format=; set --
while
literal=${string%%[!-._~0-9A-Za-z]*}
case "$literal" in
?*)
format=$format%s
set -- "$@" "$literal"
string=${string#$literal};;
esac
case "$string" in
"") false;;
esac
do
tail=${string#?}
head=${string%$tail}
format=$format%%%02x
set -- "$@" "'$head"
string=$tail
done
printf "$format\\n" "$@"
}
我在我的路由器上进行了测试(MIPS 处理器、基于 DD-WRT 的发行版、带有 ash、外部printf
和 的BusyBox [
)。每个版本都比前一个版本有显着的速度改进。转向单一分叉是最显着的改进;它使函数几乎立即响应(以人类的术语来说),而不是在几秒钟后响应实际的长 URL 参数。
请注意,上面的代码可能会在奇特的语言环境中失败(在路由器上不太可能)。export LC_ALL=C
如果您使用非默认区域设置,则可能需要。
答案2
虽然busybox
实用程序通常是相应 POSIX 实用程序的精简版本(尽管通常带有 GNU 扩展),但 busyboxawk
实际上是 POSIX API 的成熟的几乎兼容版本awk
,甚至在顶部还带有扩展。
因此,您应该能够在一次awk
调用中完成所有操作:
urlencode() {
LC_ALL=C awk -- '
BEGIN {
for (i = 1; i <= 255; i++) hex[sprintf("%c", i)] = sprintf("%%%02X", i)
}
function urlencode(s, c,i,r,l) {
l = length(s)
for (i = 1; i <= l; i++) {
c = substr(s, i, 1)
r = r "" (c ~ /^[-._~0-9a-zA-Z]$/ ? c : hex[c])
}
return r
}
BEGIN {
for (i = 1; i < ARGC; i++)
print urlencode(ARGV[i])
}' "$@"
}
这里在单独的输出行上打印每个参数的 URL 编码。
答案3
基于帖子这里,一个更优雅的解决方案:
如果你有jq
安装了(轻量级且灵活的命令行 JSON 处理器)和 Perl,则可以使用以下方法 --请注意-n
命令的参数echo
:
url="http://example.com/resource?param1=value¶m2=foo/bar"
encoded_url=$(echo -n "$url" | jq -s -R -r @uri)