bashとrubyの連携に見るエスケープな悩み
問題
- 突然ではあるが、以下のコード...ruby -pe に続けて、シングルクォートを取り除くワンライナーを完成できるだろうか?
- 出力結果が「私はzariganiです。」となればOK。
- ちなみに、作業環境はOSX 10.9 Mavericksである。
$ echo "私は'zarigani'です。" | ruby -pe
- もし、10秒以内に正しく動くコードを思い描けた方は素晴らしい。尊敬する。
- 自分は思い描いたコードを実行したけど、謎のエラーに悩み、1時間悩んだあげく眠くなり、そのまま夢の中で考えて、翌朝起きて閃いて、ようやく解決できた。
- 自分の場合、なぜか一晩眠ると閃くパターンって多い。
____以下、自分なりの解釈____
最初のコード
- 普通に考えて、最初は以下のコードを試した。
- しかし、結果は以下のとおりのエラーである。
$ echo "私は'zarigani'です。" | ruby -pe '$_.gsub!(/\'/, "")' -bash: syntax error near unexpected token `)'
- たぶんエスケープに絡むエラーだと想像できるのだけど、なぜエラーになるのか?この時点で分からない...。
ダブルクォートに換えてみる
- シングルクォートがダメなら、ダブルクォートでやってみよう、という単純な発想である。
$ echo "私は'zarigani'です。" | ruby -pe "$_.gsub!(/'/, '')"
>
- しかし、returnキー押してもコマンドが実行できない。
- 入力途中を表現するプロンプト>が表示される。
どこまでちゃんと動いているのか?
- どこまでちゃんと動いているのか、ダメな部分はどこなのか、特定してみる。
- シングル、ダブル、どちらのクォートでも、中身がなければ正常に動いている。
$ echo "私は'zarigani'です。" | ruby -pe '' 私は'zarigani'です。 $ echo "私は'zarigani'です。" | ruby -pe "" 私は'zarigani'です。
シングルクォートの場合
- という訳で、問題はクォートの中身である。
- では、クォートの中がどのように解釈されているのか?
- rubyをechoに変更して、実行してみた。
$ echo -pe '$_.gsub!(/\'/, "")' -bash: syntax error near unexpected token `)' $ echo -pe '$_.gsub!(/\q/, "")' -pe $_.gsub!(/\q/, "")
- シングルクォートをqに置き換えることで、ちゃんと表示された。
- つまり、正規表現中のシングルクォートに問題があるのだ。
ダブルクォートの場合
- 一方、ダブルクォートの方は、エラーに!を警告していたので外してみた。
- すると、エラーは消えたが、表示がちょっとおかしい。
- なぜかgsubが二重に続いてしまっている。
$ echo -pe "$_.gsub!(/'/, '')" -bash: !: event not found $ echo -pe "$_.gsub(/'/, '')" -pe $_.gsub!(/\q/, "").gsub(/'/, '')
- よく観察してみると、最初のgsubは直前のシングルクォートの実験で実行したコードである。
- その後のgsubがダブルクォートの実験で試そうとしたコードである。
言語仕様
クォート
ダブルクォートの内側 | 例 | |
---|---|---|
bash | バックスラッシュ記法、変数名などが解釈され、それぞれの意味のテキストデータに置き換えられる。 | "\n" = 0x0A(改行コード) X=testなら、"$X" = test |
ruby | バックスラッシュ記法、変数名などが解釈され、それぞれの意味のテキストデータに置き換えられる。 | "\n" = 0x0A(改行コード) X=testなら、"$X" = test |
シングルクォートの内側 | 例 | |
bash | 無用な解釈をされず、そのままのテキストが表示される。 よって、その内側ではシングルクォートを表現できない。 |
'\'' = \と、対応する閉じがない' '\' = \ |
ruby | 無用な解釈をされず、そのままのテキストが表示される。但し、\'と\\のみ解釈される。 よって、その内側でシングルクォートもちゃんと表現できる。 |
'\'' = ' '\\' = \ |
- ほとんど同じ意味なのだけど、シングルクォートの\'と\\を解釈するか、しないかの違いが大きな違い。
$_
- bashの$_には、直前のコマンドの最後の引数が代入されている。
$ echo a b c a b c $ echo $_ c $ echo x y z x y z $ echo $_ z
- 一方、rubyの$_には、直前のgets()またはreadline()で読み込んだ行の内容が代入されている。
- ちなみに、ruby -pe '何らかのコマンド...'は、以下のように展開される。
- 標準入力からのデータをgets()メソッドで1行ずつ$_に代入して、何らかのコマンド処理をして、$_の内容を出力するのだ。
ruby -e ' while gets() 何らかのコマンド... print $_ end'
!
- bashの!は、それに続く履歴番号のコマンドに置き換えられる。
- あるいは、それに続く文字列で始まる直近のコマンドに置き換えられる。
$ history 10 958 echo '0x27' 959 echo a b c d 960 echo $_ 961 echo x y z 962 echo $_ 963 echo a b c 964 echo $_ 965 echo x y z 966 echo $_ 967 history 10 $ echo !965 echo echo x y z echo x y z $ echo !echo echo echo $_ echo z
- 一方、rubyの!は、メソッドの末尾に付加すると、対象となるテキスト(オブジェクト)そのものを書き換える破壊的メソッドとして機能する。
シングルクォートの内側はどのように解釈されているのか?
- ここで、ターミナルに入力した最初のコードは、どのように解釈されているのかを考えてみる。
$ echo "私は'zarigani'です。" | ruby -pe '$_.gsub!(/\'/, "")'
正しい理解
一旦、bashがすべてを解釈 |
---|
echo "私は'zarigani'です。"|ruby -pe '$_.gsub!(/\'/, "")' |
rubyが解釈 |
---|
ruby -pe '$_.gsub!(/\'/, "")' |
bashはバックスラッシュ記法を解釈しないので ' の終わりと判断する | クォートなしの引数1 | クォートなしの引数2とシングルクォートの始まり(閉じてない) |
---|---|---|
'$_.gsub!(/\' | /, | "")' |
問題箇所
- クォートなしの引数の部分で、いきなり閉じる括弧 )が出てくる。そこでエラーが発生している。
- しかも、カンマの後にスペースがあるので、引数が二つに分かれてしまっている。
- また、最後のシングルクォートも閉じてない。
解決策1 文字コードで指定する
bashはシングルクォートの内側でシングルクォートを表現できない!これは紛れもない事実である。
- じゃあどうするか?というと、前回の日記では文字コードを指定することで切り抜けた。
- bashには、文字コード指定の\0027をそのまま解釈してもらい、
- rubyに\u0027が渡された時に、rubyはそれを ' と解釈するのだ。
$ echo "私は'zarigani'です。" | ruby -pe '$_.gsub!(/\u0027/, "")'
私はzariganiです。
見事、シングルクォートが取り除かれた!
8進数表記 | 16進数表記 | UTF-8表記 | |
---|---|---|---|
bash | $'\047' | $'\x27' | なし |
ruby | \047 | \x27 | \u0027 |
- ちなみに、\u0027の部分は、\047あるいは\x27でもOK。
- また、bashの記法で書くなら、以下のようにシングルクォート直前に$を追記しておく必要がある。
$ echo "私は'zarigani'です。" | ruby -pe $'$_.gsub!(/\x27/, "")'
私はzariganiです。
解決策2 シングルクォートから一度出してしまう
- シングルクォート内では、シングルクォートを表現できないが、
- シングルクォート外なら、シングルクォートを表現できるのだ。
よって...
$ echo "私は'zarigani'です。" | ruby -pe '$_.gsub!(/'\''/, "")'
私はzariganiです。
- \'をシングルクォートで囲ってみた。
- それにどんな意味があるのかというと...
シングルクォート内側 | クォートなし | シングルクォート内側 |
'$_.gsub!(/' | \' | '/, "")' |
- つまり、\'の部分のみ、シングルクォートの外側に追い出したことになるのだ。
ダブルクォートの内側はどのように解釈されているのか?
- それでは、ダブルクォートの内側はどのように解釈されていたのだろうか?
$ echo "私は'zarigani'です。" | ruby -pe "$_.gsub!(/'/, '')"
>
正しい解釈
一旦、bashがすべてを解釈 |
---|
echo "私は'zarigani'です。"|ruby -pe "$_.gsub!(/'/, '')" |
よって...
ruby -pe | 直前のコマンドの最後の引数 | .gsub | (/'/, '')"を履歴検索して、ヒットなし |
---|---|---|---|
ruby -pe " | $_ | .gsub | !(/'/, '')" |
解決策
$ echo "私は'zarigani'です。" | ruby -ne "puts \$_.gsub\!(/'/, '')"
-e:1: syntax error, unexpected $undefined
puts $_.gsub\!(/'/, '')
^
-e:1: syntax error, unexpected ',', expecting ')'
puts $_.gsub\!(/'/, '')
^
-e:1: warning: string literal in condition
- !と( )の表記が続くと、なぜかエラーになってしまう。(謎...どなたか教えて欲しい)
$ echo "私は'zarigani'です。" | ruby -ne "puts \$_.gsub! (/'/, '')"
-e:1: syntax error, unexpected ',', expecting ')'
puts $_.gsub! (/'/, '')
^
そして、たどり着いたコードは...
$ echo "私は'zarigani'です。" | ruby -pe "\$_.gsub! /'/, ''"
私はzariganiです。
- !の後にスペースを空けて、()を取り除いてしまった。
- !にスペースが続く時は、履歴展開はされず、そのままの!と解釈される。
- rubyのコマンドは、()で囲わなくても実行可能なのだ。