urlencode 函数

urlencode 函数

我需要一种在运行旧版本 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 ""
}

这或多或少工作得很好,但有一些缺陷:

  1. 某些字符会被跳过,例如“\”。
  2. 结果是一个字符一个字符地返回,所以速度非常慢。仅对批处理中的几个字符串进行 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&param2=foo/bar"
encoded_url=$(echo -n "$url" | jq -s -R -r @uri)

相关内容