はじめに
この記事では、シェルスクリプトで URL エンコードをもっと簡単に行う方法について解説しました。手っ取り早く答えを知りたい方はここ をクリックしてください。
シェルスクリプトで URL エンコードの仕方を調べると色々な方法が見つかります。例えばこのようなものです。
$ echo "日本語" | jq -Rr @uri
%E6%97%A5%E6%9C%AC%E8%AA%9E
$ echo "日本語" | nkf -WwMQ | sed 's/=$//g' | tr = % | tr -d '\n'
%E6%97%A5%E6%9C%AC%E8%AA%9E
たしかにこれは URL エンコードです。しかし、みなさん、本当にこれで満足しているのでしょうか? 使いやすいでしょうか? 実際に URL エンコードを使っている場面を思い出してみてください。
URL エンコードだけでは実用的ではない
Google の検索 URL を見てみましょう。次のようになっています。
# URL エンコードした状態
https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E&ie=UTF-8
# URL デコードした状態
https://www.google.com/search?q=日本語&ie=UTF-8
では、この URL エンコードした文字列作るにはどうしたら良いでしょうか?
$ echo "https://www.google.com/search?q=日本語&ie=UTF-8" | jq -Rr @uri
https%3A%2F%2Fwww.google.com%2Fsearch%3Fq%3D%E6%97%A5%E6%9C%AC%E8%AA%9E%26ie%3DUTF-8
できませんよね?
理由をわかっている人はすぐに正解を出せるでしょう。次のように書きます。
$ echo "https://www.google.com/search?q=$(echo "日本語" | jq -Rr @uri)&ie=UTF-8"
https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E&ie=UTF-8
少々読みにくいので、シェルスクリプトで書くならこのように書くでしょうか?
q=$(echo "日本語" | jq -Rr @uri)
echo "https://www.google.com/search?q=${q}&ie=UTF-8"
このように書かなければいけない理由は、URL に含まれている :
、/
、?
、&
などが URL エンコードの対象の文字だからです。この問題に近い話は JavaScript では encodeURI()
と encodeURIComponent()
の違いとして知られています。
$ node
Welcome to Node.js v20.5.0.
Type ".help" for more information.
> encodeURI("https://www.google.com/search?q=日本語&ie=UTF-8")
'https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E&ie=UTF-8'
> encodeURIComponent("https://www.google.com/search?q=日本語&ie=UTF-8")
'https%3A%2F%2Fwww.google.com%2Fsearch%3Fq%3D%E6%97%A5%E6%9C%AC%E8%AA%9E%26ie%3DUTF-8'
私達が本当に欲しいものは encodeURI
・・・ というわけでもなく、クエリーパラメーター自体にエンコード対象の文字が含まれている場合はエンコードをしなければなりません。つまりどの部分に含まれているかでエンコードするかしないかが変わってくるということです。今回は一つのパラメータだけをエンコードすれば十分ですが、エンコードするパラメータが複数あったとしたら・・・。
query=$(echo "日本語" | jq -Rr @uri)
genre=$(echo "和書" | jq -Rr @uri)
author=$(echo "作者" | jq -Rr @uri)
comment=$(printf '%s\n' "1行目" "2行目" | jq -sRr @uri)
url="https://www.example.com/search?q=${query}&g=${genre}&a=${author}&c=${comment}"
パラメータの数が増えるたびに行が増え可読性が悪くなっていきます。また jq
コマンドを何度も呼び出しているのでパフォーマンスも悪いです。echo
コマンドは移植性が低いから printf "%s"
にしなければいけないとか、改行文字を扱う(HTML フォームの textarea では改行が使えます)には -s
オプションが必要などの細かい話もあり、それらに対処していくとますます可読性は下がってしまいます。
URL の組み立てを簡単に行う
多くの場合、本当に欲しいものは URL エンコードだけではありません。URL エンコード機能が含まれた「URL の組み立て」です。
jq コマンドを使う方法
jq
コマンドはパラメータを一つ一つ URL エンコードせずとも、まとめて行う方法がちゃんと用意されています。
$ jq --arg q 日本語 --arg ie UTF-8 -nr '@uri "https://www.google.com/search?q=\($q)&ie=\($ie)"'
https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E&ie=UTF-8
$ jq --arg q $'foo\nbar' --arg ie UTF-8 -nr '@uri "https://www.google.com/search?q=\($q)&ie=\($ie)"'
https://www.google.com/search?q=foo%0Abar&ie=UTF-8 ← 改行文字 %0A も扱えます
jq
コマンドを使う場合、URL の組み立てにはこの方法を使うことをおすすめします。
url コマンド(自作)を使う方法
jq
コマンドも悪くないのですが少々冗長です。そこでもっと簡単に使える url
コマンドを作りました(ソースコードはこちら)。実装はシェルスクリプトです。jq
コマンドは使用しておらず awk
のみを使っているためどの環境でも動作するはずです。
URL の組み立てはこのように書くことができます。クエリーパラメーターが自動的に URL エンコードされます。ちなみにクエリーパラメーターのキーもエンコードの対象です。
$ url 'https://www.google.com/search' -q 日本語 -ie UTF-8
https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E&ie=UTF-8
$ url 'https://example.com/' -キー 123
https://example.com/?%E3%82%AD%E3%83%BC=123
最初の引数(URL)もエンコード対象ですが、こちらは JavaScript でいう encodeURI
相当のエンコードを行い、:
.
/
などはエンコードしません。
$ ./url 'https://ja.wikipedia.org/wiki/URLエンコード'
https://ja.wikipedia.org/wiki/URL%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89
クエリーパラメーターの形式になっていない変則的なクエリストリングやフラグメントにも対応しています。
$ url 'https://www.google.com/search' '=日本語' '#フラグメント'
https://www.google.com/search?%E6%97%A5%E6%9C%AC%E8%AA%9E#%E3%83%95%E3%83%A9%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%88
--printf
オプションを指定すると printf mode となり、printf
コマンドと同じように書式を用いて URL エンコードを行うことができます。printf
コマンドと同じように引数を多く指定するとその分繰り返して出力されます。
$ url --printf 'https://www.google.com/search?q=%s&ie=%s\n' 日本語 UTF-8 英語 UTF-8
https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E&ie=UTF-8
https://www.google.com/search?q=%E8%8B%B1%E8%AA%9E&ie=UTF-8
最初の引数は URL である必要はないので、URL エンコードのみを行うこともできます。もちろん引数に改行文字が含まれていても対応可能です。入力を標準入力(パイプ)で受け取る方法だとこうはいきません。
$ url --printf '%s\n' あいうえお アイウエオ $'a\nb'
%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A
%E3%82%A2%E3%82%A4%E3%82%A6%E3%82%A8%E3%82%AA
a%0Ab
これを応用すると複数の URL エンコードした文字列をシェル変数に代入することができます。エンコードしたい文字列の数が多いときに一つ一つエンコードするよりも速いです。
$ url --printf '%s="%s"\n' hiragana あいうえお katakana アイウエオ
hiragana="%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A"
katakana="%E3%82%A2%E3%82%A4%E3%82%A6%E3%82%A8%E3%82%AA"
encoded=$(url --printf '%s="%s"\n' hiragana あいうえお katakana アイウエオ)
eval "$encoded"
echo "$hiragana" # => %E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A
echo "$katakana" # => %E3%82%A2%E3%82%A4%E3%82%A6%E3%82%A8%E3%82%AA
その他に、空白を %20
ではなく +
に変換する -s
オプション、改行を \r\n
に統一する -n
オプションを持っています。
$ url --printf -s -n '%s\n' 'foo bar' $'foo\nbar'
foo+bar
foo%0D%0Abar
おまけ URL デコードはどうするの?
URL のパースとしては不完全な方法ですが nkf --url-input
を使うのが簡単です。なぜ不完全なのかというと、URL エンコードされた文字列全体を、そのまま URL デコードしても妥当な形式にならないからです。例えばクエリーパラメーターの値として &
をエンコードした %26
が含まれている時、それを URL デコードすると困った URL が生成されてしまいます。
$ jq --arg q 'try&error' --arg ie UTF-8 -nr '@uri "https://www.google.com/search?q=\($q)&ie=\($ie)"
https://www.google.com/search?q=try%26error&ie=UTF-8
$ echo 'https://www.google.com/search?q=try%26error&ie=UTF-8' | nkf --url-input
https://www.google.com/search?q=try&error&ie=UTF-8
【読みやすくスペースを入れると】
https://www.google.com/search ? q=try & error & ie=UTF-8
error
という値なしのキー(?)が作られてしまいます。もし error
の文字列が err=or
だったりしたら err
キーの登場です。つまり URL をパースする時は先にクエリーパラメーターを分解してから、個々のパラメータごとに URL デコードを行わなければならないわけです。
で、対話シェルでそんなユースケースはあるんですか?と。対話シェルから URL デコードを行いたい場合の多くはおそらくログなどからどのような文字列であるかを知りたい時だと思います。URL として正しく解釈する必要はなく「大雑把に読めれば良い」という使い方で十分なら nkf --url-input
に投げてしまえばそれで十分です。
その反対に URL としてきちんと解釈したい場合、例えば CGI やスクレイピングでクエリーパラメーターを読み取る場合は、ただの URL デコードではなく URL のパース機能が必要になります。URL エンコードされた文字列には改行が含まれることもありますし、同じ名前のパラメータが複数あったり、順番が重要だったり、さまざまな考慮事項が必要になります。
$ echo 'https://www.google.com/search?q=try%0Aerror&ie=UTF-8' | nkf --url-input
https://www.google.com/search?q=try
error&ie=UTF-8
つまり
- 対話シェルから雑に URL エンコードが含まれた URL を読み取りたい
- シェルスクリプトから URL のクエリーパラメーターを解釈したい
この 2 つで必要となるものは別なのです。nkf --url-input
は URL デコードを行い人が読めるようにしますが、それだけでは URL を正しくパースすることはできません。
で、正しく URL をパースする方法は考えてはいるのですが、シェルスクリプトで必要な人っているのでしょうか? 必要になるのは CGI やスクレイピングをするときぐらいで、そういう用途なら別の言語を使ったほうが良いでしょう。個人的には興味があるのでやってみたいと思ってはいますが私個人の優先順位は高くありません。URL エンコードは curl
(--data-urlencode
では足りない場合がある)や wget
コマンドの呼び出しで必要になったので作りました。
URL のパースが可能な本格的なものを作るのは後回しということで、単に読みたいだけであれば nkf --url-input
を使ってくださいというのが本記事での答えとなります。他にもシェル言語だけや POSIX コマンドだけで実現する方法も探せば見つかります。
さいごに
以上 URL エンコードをもっと簡単に行う方法でした。url のライセンスは 0BSD にしているのでご自由にお使いください。url
コマンドをインストールしたくないという人もいるかもしれませんが、内部のシェル関数 urlbuild
と urlprintf
を再利用しやすいように作っており「あなたのシェルスクリプト」にコピーして使うことができます。