Shell

urlencode 函式

  • November 24, 2021

我需要一種在執行舊版本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} 這樣的子字元串。

[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)
     }
   }'
}

如果你的設備沒有od,你可以在 shell 內做任何事情;這將顯著提高性能(減少對外部程序的呼叫——如果printf是內置程序則沒有)並且更容易正確編寫。我相信所有 Busybox 外殼都支持${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" "$@"
}

根據編譯選項,[(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" "$@"
}

並批量複製每個文欄位:

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" "$@"
}

我在我的路由器上進行了測試(MI​​PS 處理器、基於 DD-WRT 的分發、帶 ash 的 BusyBox、外部printf[)。每個版本都比前一個版本顯著提高了速度。遷移到單個分叉是最顯著的改進;它使函式幾乎立即響應(以人類術語),而不是在幾秒鐘後響應一個真實的長 URL 參數。

請注意,上面的程式碼在花哨的語言環境中可能會失敗(不太可能在路由器上)。export LC_ALL=C如果您使用非預設語言環境,您可能需要。

引用自:https://unix.stackexchange.com/questions/60653