はじめに

SSHを通してリモート先でコマンド実行するには、ssh $remote "$command $option"というようにsshの最後にコマンドを書く。コマンド部分をくくるクォーテーションと、コマンド内部でクォーテーションを使用した時のエスケープについて書く。

クォーテーションが必要ない場合

ls /var/tmpのような簡単なコマンドであれば、実行コマンドをシングルクォーテーションやダブルクォーテーションでくくる必要はない。

$ remote=192.168.0.3
$ ssh $remote ls /var/tmp
test1
test2

クォーテーションが必要な場合

複数のコマンドをつなぐ時

&&;で複数のコマンドを実行したりする場合は、クォーテーションでくくらなくてはいけない。

$ ssh $remote 'hostname && hostname'
server2
server2

# クォーテーションでくくらないと、後のコマンドは自分のいるサーバで実行されてしまう。
$ ssh $remote hostname && hostname
server2
server1

コマンドのオプション部分でクォーテーションが使われている場合

他にはsedで置換するときにexpression部分をシングルクォーテーションで囲むとき等はsedコマンド自体をダブルクォーテーションでくくらなければいけない。簡単な置換であればexpression部分をシングルクォーテーションで囲まなくてもいいが、後方参照をするために拡張正規表現のグルーピング()を使った場合などは、expressionをクォーテーションで囲む必要が出てくるので、sedをSSHを通して実行する場合はsedコマンド自体をクォーテーションでくくらなければいけないことが多いだろう。

# 簡単な置換であればクォーテーションなしでOK
$ ssh $remote sed -n s/access_log/Access_Log/p /etc/httpd/conf/httpd.conf
  CustomLog logs/Access_Log combined
 
 
# 後方参照で()を使うときなどはシェルに()を解釈されないように、クォーテーションで囲む必要が出てくる
# SSH越しに実行しなくても、クォーテーションは必要
$ ssh $remote
$ sed -n -r s/a(ccess_)l(og)/A\1L\2/p /etc/httpd/conf/httpd.conf
-bash: syntax error near unexpected token `('
# ↑ 'がないので、(がシェルに解釈されてしまっている
$ sed -n -r 's/a(ccess_)l(og)/A\1L\2/p' /etc/httpd/conf/httpd.conf
  CustomLog logs/Access_Log combined
$ exit
 
# SSH越しに実行する場合、sed自体をクォーテーションでさらに囲む必要がある
$ ssh $remote sed -n -r 's/a(ccess_)l(og)/A\1L\2/p' /etc/httpd/conf/httpd.conf
bash: -c: line 0: syntax error near unexpected token `('
bash: -c: line 0: `sed -n -r s/a(ccess_)l(og)/A\1L\2/p /etc/httpd/conf/httpd.conf'
$ ssh $remote "sed -n -r 's/a(ccess_)l(og)/A\1L\2/p' /etc/httpd/conf/httpd.conf"
  CustomLog logs/Access_Log combined

シングルクォーテーションかダブルクォーテーションか

SSH越しに実行するコマンドにコマンド置換や変数が含まれていた場合、手元のサーバかリモート先かどちらのサーバで解釈させたいかによって、コマンドにシングルクォーテーションをつけるかダブルクォーテーションをつけるかが変わる。シングルクォーテーションをつけないと、手元のサーバでコマンドの解釈がされた上でSSH先にコマンドが送られる。シングルクォーテーションをつけると、コマンド置換や変数の展開が行われないので、コマンドで書いた字面そのままがリモート先に送られ、実行される。

$ ssh $remote echo `hostname`
server1
$ ssh $remote "echo `hostname`"
server1
$ ssh $remote 'echo `hostname`'
server2

ここまでが、SSHを通してリモート先でコマンドを実行する基本的な文法の説明で、今回の一番書きたいことであるクォーテーションを気にしないで実行する方法について以降で考えてみたい。

クォーテーションのエスケープ

題材として、ApacheのLog出力の最後に%Dを追加してレスポンスタイムをマイクロ秒でログ出力するためのconf編集をsedを使って行う。

sedの置換式の中にダブルクォーテーションがあったとき、置換式のダブルクォーテーションと置換式をくくるシングルクォーテーション、そしてSSHを通して実行するのでsedコマンド自体をくくるクォーテーションが必要になる。クォーテーションだらけで訳が分からなくなってしまう。
今回の題材を実現するためには置換式の中にダブルクォーテーションが出てくる。

# 元のLogFormat
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\"" combined
# 期待するLogFormat
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\" %D" combined

sedコマンド

sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf

これをSSH越しで実行するためにはsedをダブルクォーテーションで囲み、置換式の中に出てくるダブルクォーテーションを\でエスケープする必要がある。

$ ssh $remote "sed -n -r 's/^(LogFormat.*)(\" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf"
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\" %D" combined

もしエスケープ箇所が増えたらものすごく読みづらい置換式になってしまう。

クォーテーションのエスケープを書かなくても済む方法

これを解決する方法としては、エスケープしていない通常のsedコマンドをファイルに記載してスクリプトファイルをSSHを通して実行する方法がある。ssh $remote sh < ファイルでリモート先でスクリプトファイルを配布せずに実行できる。

$ cat sed_log.sh
sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf
$ ssh $remote sh < sed_log.sh
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\" %D" combined
 
# shの部分は以下のように書いてもOK
$ ssh $remote bash < sed_log.sh  # スクリプトファイルの内容次第ではbashでないと動かないことも
$ ssh $remote 'sh' < sed_log.sh  # 当然、shをクォーテーションでくくっても問題ない
$ ssh $remote sh -x < sed_log.sh # shのオプションもつけられる

スタンダードなやり方だが、スクリプトファイル、つまりここでは中間ファイルができてしまうのが好みでないので、中間ファイルを作らずに実行する方法を考える。

クォーテーションのエスケープを書かなくても済むスマートな方法

ターミナルに入力したクォーテーションを標準出力にそのまま出す方法として考えられるのは、ヒアドキュメントを利用する方法がある。
echoでスクリプトファイルに書かれているものと同じコマンドを出力しようとしても、シングルクォーテーションが削られて出力される。

# "は'でくくられているので正しく出力されるが、'自体はechoの引数を囲むクォーテーションと解釈されて出力されない
$ echo sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf
sed -n -r s/^(LogFormat.*)(" combined)/\1 %D\2/p /etc/httpd/conf/httpd.conf

# echoで正しい値をとろうとすると、全体をダブルクォーテーションでくくったうえで、
# 先ほどと同じように中のダブルクォーテーションをエスケープしなければならない。
$ echo "sed -n -r 's/^(LogFormat.*)(\" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf"
sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf

ヒアドキュメントを使うと、エスケープを書かずに出力できる。

$ cat <<EOS
sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf
EOS
 
sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf

ヒアドキュメントを使うとき、大抵cat <<EOS > output_fileのようにファイルに書き出すが、今回は出力先をファイルではなく、パイプでつないだssh $remote shにする。技術的にはスクリプトファイルをリダイレクトするものとほぼ同じになる。

$ cat <<EOS | ssh $remote sh
sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/p' /etc/httpd/conf/httpd.conf
EOS
 
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\" %D" combined

server1からserver100の複数のサーバに実行したい場合、for loopと組み合わせて次のように書ける。

$ for i in server{1..100}
do
  cat <<EOS | ssh $i sh
sed -n -r 's/^(LogFormat.*)(" combined)/\1 %D\2/' /etc/httpd/conf/httpd.conf
EOS
done