bashとrubyの連携に見るエスケープな悩み

問題

  • 突然ではあるが、以下のコード...ruby -pe に続けて、シングルクォートを取り除くワンライナーを完成できるだろうか?
    • 出力結果が「私はzariganiです。」となればOK。
  • ちなみに、作業環境はOSX 10.9 Mavericksである。
    • GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
    • ruby 2.0.0p247 (2013-06-27 revision 41674) [universal.x86_64-darwin13]
 $ 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の仕様、rubyの仕様をちゃんと調べてみると、その動きが見えてきた。
ダブルクォートの内側
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が解釈 パイプ rubyが解釈
echo "私は'zarigani'です。" ruby -pe '$_.gsub!(/\'/, "")'
正しい理解
一旦、bashがすべてを解釈
echo "私は'zarigani'です。"|ruby -pe '$_.gsub!(/\'/, "")'
  • bashが解釈した内容を元に、各コマンドを実行して、rubyコマンドの部分をrubyが処理する。
  • 但し、その引数はbashが解釈した内容に置き換えられている。
rubyが解釈
ruby -pe '$_.gsub!(/\'/, "")'
  • ruby -pe に続くシングルクォート内は、bashによって以下のように解釈されているはず。
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!(/' \' '/, "")'
  • つまり、\'の部分のみ、シングルクォートの外側に追い出したことになるのだ。
解決策3 バックスラッシュエスケープのみ可能な$'文字列'

ここまで書いて、もっと単純な方法を閃いた!

  • 解決策1のbash$'文字列'記法は、バックスラッシュエスケープのみ許可されたシングルクォートと考えることができるので、わざわざ文字コードに置き換える必要さえないのだ。
 $ echo "私は'zarigani'です。" | ruby -pe $'$_.gsub!(/\'/, "")'
 私はzariganiです。

これでOK!

ダブルクォートの内側はどのように解釈されているのか?

  • それでは、ダブルクォートの内側はどのように解釈されていたのだろうか?
 $ echo "私は'zarigani'です。" | ruby -pe "$_.gsub!(/'/, '')"
>
正しい解釈
一旦、bashがすべてを解釈
echo "私は'zarigani'です。"|ruby -pe "$_.gsub!(/'/, '')"
  • bashは、$_を直前に実行したコマンドの最後の引数に置き換える。
  • bashは、!に続く履歴番号を探そうとする。しかし、検索不能な(/'/, '')"が続いている。

よって...

  • ruby -pe に続くシングルクォート内は、bashによって以下のように解釈されているはず。
ruby -pe 直前のコマンドの最後の引数 .gsub (/'/, '')"を履歴検索して、ヒットなし
ruby -pe " $_ .gsub !(/'/, '')"
解決策
  • bashからダブルクォートを利用して、rubyとパイプで繋げるのは厄介に感じた。
  • エスケープした\$は$と解釈されるが、エスケープした\!は\!になってしまう。(バックスラッシュが残る)
 $ 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のコマンドは、()で囲わなくても実行可能なのだ。

結論

bashからrubyワンライナーで繋げるなら...
ruby -pe "ダブルクォート" で囲うのは避けた方が良さそう。
ruby -pe $'シングルクォート' で囲うのがおすすめ。