urlencode 函数

Ser*_*erg 6 shell shell-script openwrt busybox ash

我需要一种在运行旧版 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 ""
}
Run Code Online (Sandbox Code Playgroud)

这或多或少地工作正常,但有一些缺陷:

  1. 某些字符会被跳过,例如“\”。
  2. 结果是逐个字符返回,因此速度非常慢。对一批中的几个字符串进行 url 编码大约需要 20 秒。

我的 bash 版本不支持像 ${var:x:y} 这样的子字符串。

Gil*_*il' 7

[TL,DR:使用urlencode_grouped_case最后一个代码块中的版本。]

Awk 可以完成大部分工作,只是它缺乏一种将字符转换为数字的方法。如果od存在于您的设备上,您可以使用它将所有字符(更准确地说,字节)转换为相应的数字(以十进制写入,以便 awk 可以读取它),然后使用 awk 将有效字符转换回文字并引用字符转换成适当的形式。

urlencode_od_awk () {
  echo "$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)
      }
    }'
}
Run Code Online (Sandbox Code Playgroud)

如果您的设备没有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
}
Run Code Online (Sandbox Code Playgroud)

如果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" "$@"
}
Run Code Online (Sandbox Code Playgroud)

这在外部调用方面是最佳的(只有一个,除非您愿意枚举所有需要转义的字符,否则不能使用纯 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" "$@"
}
Run Code Online (Sandbox Code Playgroud)

根据编译选项,[(aka 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" "$@"
}
Run Code Online (Sandbox Code Playgroud)

并批量复制每个文字段:

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" "$@"
}
Run Code Online (Sandbox Code Playgroud)

我在我的路由器(MIPS 处理器、基于 DD-WRT 的发行版、BusyBox with ash、external printfand [)上进行了测试。每个版本都比前一个版本显着提高了速度。转向单叉是最显着的改进;它是使函数几乎立即响应(用人类术语)而不是几秒钟后响应真实的长 URL 参数的函数。