hkoba blog

プログラマーです。プログラミング言語ミーハーです。ツッコミ歓迎です。よろしくどうぞ(能代口調)

ssh 先で sudo 実行するコマンドに渡すワンライナーを zsh に quote させる

いつもの通り、ツッコミ歓迎です。

その quote, zsh に任せると幸せ…かも??

笹田さんのこのツイを見て、おっと zsh 宣伝チャンス、と。

zsh には変数展開時に結果を自動で quote してくれる (q), (qq), (q-) ... という機能があります。

この機能を使って、 ssh 先の sudo に渡すスクリプトの quote を試みます。

(ローカル側は zsh ですが、 リモート側は /bin/sh で OK なのが味噌です)

まず最初の echo の例

% cmds=(
  "echo a b"
  "echo c d"
)

% ssh -t localhost sh -x -c ${(qqj/&&/)cmds}
+ echo a b
a b
+ echo c d
c d
Connection to localhost closed.
%
  • ssh に渡す ${(qqj/&&/)cmds} は以下の2つの組み合わせ
    • (qq) による自動 quote
    • (j/&&/)join("&&")

ssh に渡される実際のコマンドラインを知りたい場合は以下のようにすると良いです。

% print -R ssh -t localhost sh -x -c "${(qqj/&&/)cmds}"
ssh -t localhost sh -x -c 'echo a b&&echo c d'
% print -l ssh -t localhost sh -x -c "${(qqj/&&/)cmds}"
ssh
-t
localhost
sh
-x
-c
'echo a b&&echo c d'
%
  • -R は escape の抑制
  • -l は引数を一行ずつ出力する

ssh 先の sudo に渡すワンライナーzsh に quote させる

次は以下の二行を一度の ssh に渡すよう、quote してみます。

sudo ruby -e 'puts "Hello uid=#{Process.uid}"'

sudo ruby -e 'puts "Again uid=#{Process.uid}"'

上記の2つのワンライナーを配列に格納するには、 各々を '...' で quote する必要が有るので、

cmds=(

  'sudo ruby -e '\''puts "Hello uid=#{Process.uid}"'\'''

  'sudo ruby -e '\''puts "Again uid=#{Process.uid}"'\'''

)

これを手で入力すると大変ですが、幸い zsh には現在の入力を丸々 一本の文字列へと quote するコマンド M-' quote-line が用意されているので、これを使いましょう。 範囲指定版の M-" quote-region もあります。

出来た配列 $cmds を使って ssh してみます。

% ssh -t localhost sh -x -c "${(qqj/&&/)cmds}"
+ sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
[sudo] hkoba のパスワード:
Hello uid=0
+ sudo ruby -e 'puts "Again uid=#{Process.uid}"'
Again uid=0
Connection to localhost closed.

quote-line を zsh の関数として書いてみる

使い捨ての作業なら quote-line で一行ずつ quote しても良いのですが、 スクリプト化したい時には不便です。引数それぞれを quote して繋げた文字列を 返す関数を定義してみましょう。

% function zquote { print -R "${(@qq)argv}" }

% zquote sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
'sudo' 'ruby' '-e' 'puts "Hello uid=#{Process.uid}"'
%

(q-) の方が、読みやすい出力になって良いですね。

% function zquote { print -R "${(@q-)argv}" }

% zquote sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
sudo ruby -e 'puts "Hello uid=#{Process.uid}"'
%

これを使うと、先の例はこう書き直すことが出来ます。

cmds=(

 "$(zquote sudo ruby -e 'puts "Hello uid=#{Process.uid}"')"

 "$(zquote sudo ruby -e 'puts "Again uid=#{Process.uid}"')"

)

zquote の出力を一本の文字列にする必要が有るので、 "$(zquote ...)" のように呼び出し全体を ".." で囲っています。

…面倒臭くなってきたので、この辺で!あとは皆さん、ご自由に!

…なお、もっと複雑なケースの時は私は tcl + sshcomm を使います。 が、その話はまたいつか…

参考