Bashで文字列をエスケープするのは案外面倒くさい。
そういえばシェル文字列をエスケープするコマンドとかありそうなのに知らないし…。まぁ僕が知らなかっただけで元からあるよって場合は誰か教えて下さい。
**結論を先に書いておくと、今時の新規スクリプトは printf %q "$v"
を使うのが一番確実かなと思う。**Bash 4.3未満で~
がエスケープされないバグとかあったけど未来の保守だけ考えれば良くて過去に持ち帰る必要が無いという要件が殆どだろうしやらないよりは1万倍マシ。
手順
面倒とはいえやらなきゃいかん。
で、とりあえず 'シングルクオート最強! $a "ho ge"'
てな感じでシングルクオートで囲っておけば日本語や空白文字や変数やダブルクオートとかが入ってても何でもそのまま文字列として扱える。これは特定文字にバックスラッシュを付けるとかよりよっぽど簡単だし、簡単ということはミスや漏れもしにくく安全だってことだ。
でも唯一問題なのがシングルクオートをその中に混ぜられないこと。でもこれは一旦シングルクオートのリテラルを閉じてダブルクオートのリテラルで置き換えてやればOK。つまり a'g"e
という文字列を使いたければ 'a'"'"'g"e'
としてやればよい。
ちなみにシングルクオート1つの為に一番外側をダブルクオートにする案は変数や一部文字のエスケープが面倒なので却下。
関数化
固定値なら上記を手で書いても良いが、変数などで渡ってきた不定な値をエスケープするには手作業は出来ない。というわけで関数化してみる。
sh-escape() {
local s a=() q="'" qq='"'
for s in "$@"; do
a+=("'${s//$q/$q$qq$q$qq$q}'")
done
echo "${a[*]}"
}
別に関数じゃなくてシェルスクリプトにしておいても良い。これは好みの問題でどっちも同じように使える。
#!/bin/bash
q="'" qq='"'
for s in "$@"; do
a+=("'${s//$q/$q$qq$q$qq$q}'")
done
echo "${a[*]}"
簡単な補足をしておく。
-
a+=("xxx")
は配列aにxxxという文字列をpushするの意味。-
a[${#a[@]}]=xxx
と書くこともあるが、vimのシンタックスハイライトが変になるので+=
の方をよく使うようになった。
-
-
${s//foo/bar}
で変数sの中のfooを全てbarに置換したものが得られる。- 最初の
//
を/
1個にすると最初に見つけた1個だけ置換という意味になる。 - 今回は
"
を全部'"'"'
に置換したいので/
じゃなく//
を使っている。 - fooやbarに当たる部分に
"
や'
を直接書くとバックスラッシュエスケープが必要で面倒なので一旦変数に入れておいてそれを使ってる。 - ちなみに最初は
a+=("'${s//\'/\'\"\'\"\'}'")
て書いてたんだがBashのバージョンによって挙動が変わってバックスラッシュがそのまま出力されちゃって動作しないケースを発見したのも$qと$qqの変数に置き換えた理由。こういうのがあるからバックスラッシュエスケープは嫌いだ…。
- 最初の
動作確認
使い方はこんな感じだ。
$ sh-escape 'ho"ge' "fu'ga"
'ho"ge' 'fu'"'"'ga'
$ cmd="echo $(sh-escape 'ho"ge' "fu'ga")"
$ echo "$cmd"
echo 'ho"ge' 'fu'"'"'ga'
$ eval "$cmd"
ho"ge fu'ga
これで eval
も怖くない。
活用例
なんでこんな事を始めたかって言うと、tmuxのnew-windowとかnew-sessionとかのオプションがコマンド引数を個別に認識してくれないからだ!
new-window [-adkP] [-c start-directory] [-F format] [-n window-name] [-t target-window] [shell-command]
manから引用すると↑こんな感じ。この [shell-command]
てやつが単体の文字列でしか受け付けない仕様なのでムキーってなるわけです。これが [shell-command] [arg [args...]]
みたいになっててくれれば、引数を配列変数argsに入れておいて tmux new-window prog "${args[@]}"
とか書けるのに、1つの文字列になってるせいで安全に書きにくいのよ。
で結構多くの人が tmux new-window "prog ${args[*]}"
と同値な書き方をしちゃってる例を見る。でもこれだとargsのなかに"ho ge"
ていう値が入ってたら、prog には一つの文字列として渡したいのに、ho と ge ていう二つの引数として渡されちゃうのでそれ駄目じゃん?
ましてや"; rm -rf *"
みたいな文字列が混じってたらとか想像したら怖すぎる。
というわけで上述した関数を作ったわけだ、ここが目的地。あれを使うと↓こう書ける。
tmux new-window "prog $(sh-escape "${args[@]}")"
あースッキリした!
追記:printf で行けた!→いややっぱ駄目ぽ?→バージョン4.3以降ならOK
printf %q "$value"
これでいいんじゃね?
※ただし%qは、coreutilsパッケージとかに含まれてる/usr/bin/printfではなく、bashのbuildin functionのprintfのみで使えるようです。Bashスクリプト書いてる時はこれでいいけど、使えないケースもあるかもしれないのでその点は留意すること。
※更に追記、printf %qだとチルダ~
が文字列としてエスケープされない模様。その仕様だとちょっと違ってきちゃうのでやっぱ自前関数作るしか無いのか…?
※更に追記、チルダがエスケープされないのは仕様じゃなくバグだったってことで4.3で直った模様。