web-dev-qa-db-ja.com

urlencode関数

古いバージョンのbusyboxを実行しているOpenWRTデバイスでシェルスクリプトを使用して文字列を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}のような部分文字列をサポートしていません。

4
Serg

[TL、DR:最後のコードブロックでurlencode_grouped_caseバージョンを使用します。]

Awkは、文字からその数字に変換する方法が煩わしく欠けていることを除いて、ほとんどの仕事を行うことができます。 odがデバイスに存在する場合は、それを使用してすべての文字(より正確にはバイト)を対応する数値(awkが読み取れるように10進数で記述)に変換してから、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がない場合は、シェル内ですべてを実行できます。これにより、パフォーマンスが大幅に向上し(外部プログラムへの呼び出しが少なくなり、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が組み込みではなく外部ユーティリティである場合、文字ごとに1回ではなく、関数全体に対して1回だけ呼び出すことで、パフォーマンスが再び向上します。フォーマットとパラメータを作成してから、printfを1回呼び出します。

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)が外部ユーティリティである場合があります。これは文字列照合にのみ使用しており、シェル内でcase構文を使用して実行することもできます。 testビルトインを回避するために書き直された、最後の2つのアプローチは、次のとおりです。

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を使用したBusyBox、外部printfおよび[)でテストしました。各バージョンは、前のバージョンに比べて速度が大幅に向上しています。シングルフォークへの移行は最も重要な改善です。これは、現実的な長いURLパラメーターの数秒後とは対照的に、関数をほぼ瞬時に(人間の観点から)応答させるものです。