ssh先で実行するコマンドのエスケープが面倒なときはbase64でencode/decodeするという選択肢



2019-06-04 追記

いくつかコメントいただいたので、いろいろ試してみた内容を追記します

私がShellの仕様をきちんと理解せずに投稿していた部分もあり、勉強になりました

結論からいうと
1. `base64`なんて使わなくてもssh先のサーバにコマンドは流せます
2. `cat encoded.txt | ssh HOSTNAME "$(base64 -d)"` の`"$(base64 -d)"`の部分はssh先ではなくローカルでdecodeされます(リモートでされると勘違いしてました)

ただ、実際に私が開発している過程で`base64`を使って、上記のShellのテクニックや複雑なエスケープ処理を正しく動作させるまでの手間を回避していたことも事実なので、コードの実例を交えて紹介しておきます

問題は、たんにssh先のホストにコマンドが流せればよいわけではなく***Node.jsの文字列をsshコマンドを通してリモートホストで流す***必要があったということです

#### Node.jsからsshを経由して、リモートサーバ上で複雑なShellSctiptを実行するいくつかの方法

以下のようなコマンドをNode.jsからsshを経由してリモートのサーバ上で実行したいとします

```bash
cat /etc/hosts \
|grep "^192" \
|awk '{print $2}' \
|sed -e "s/^target\([1-3]\)$/updated\1/g" \
> ~/tmp/results.hosts
```

`/etc/hosts`の中から`192`で始まる行を抜き出し、更にそのホスト名だけを抜き出します。そしてホスト名に`'target'`という文字列が含まれる場合は`'updated'`に置き換え、`~/tmp/results.hosts`に結果を出力します。出力先のファイルパスはリモートサーバ上のものです

そんな処理が必要になるような状況は想定しづらいですが開発しているソフトウェアの特性上、Shell上で正しく動作するコマンドは全てリモートサーバでも動作するようサポートしなければならないので、あえて複雑な例でソフトウェアの動作を確認しておいて損はないでしょう

##### 案1: 普通に`ssh HOSTNAME COMMAND`

```Node.js
const exec=require('child_process').exec;


const cmd=String.raw`
  cat /etc/hosts \
  |grep "^192" \
  |awk '{print $2}' \
  |sed -e "s/^target\([1-3]\)$/updated\1/g" \
  > ~/tmp/results.hosts
`;

const ssh=`
  ssh target1 ${cmd}
`;

console.log(ssh);
exec(ssh, console.log);
```

`` ` ``で囲まれている部分はテンプレート構文で、文字列中に変数や定数を埋め込むことができます。`"`や`'`が含まれる文字列もこの構文を使えばエスケープなしで使えます

`String.raw`というのは文字列をそのまま扱ってくれるものです。これがないとテンプレート構文中の`\1`がうまく解釈されずに、SyntaxErrorとなります(Javascriptの細かい仕様は割愛)

そして実際このコードは終了しません。cmd定数の1行目が改行なので`ssh target1`のあとに改行が入ってしまい、ただtarget1にログインするだけで、コマンドは実行されません

`\n`を削除すればよいのですが、そうすると今度はあえて改行を入れているコマンドに対応できなくなります

##### 案2: `echo COMMAND | ssh HOSTNAME sh`

このパターンだと今度は、コマンド内の`'`ないしは`"`をエスケープしなければなりません

```Node.js
const exec=require('child_process').exec;


const cmd=String.raw`
  cat /etc/hosts \
  |grep "^192" \
  |awk '\''{print $2}'\'' \
  |sed -e "s/^target\([1-3]\)$/updated\\1/g" \
  > ~/tmp/results.hosts
`;

const ssh=`
  echo '${cmd}' | ssh target1 sh
`;

console.log(ssh);
exec(ssh, console.log);
```

テンプレート構文とShellの文字列が混在しているので、どうしてもどこかでエスケープが必要になります


##### 案3: `cat EOS ~ EOS` を使う

この方法は問題なく動作しました

```Node.js
const exec=require('child_process').exec;


const cmd=String.raw`
  cat /etc/hosts \
  |grep "^192" \
  |awk '{print $2}' \
  |sed -e "s/^target\([1-3]\)$/updated\1/g" \
  > ~/tmp/results.hosts
`;

const ssh=`
  cat << 'EOS' | ssh target1 sh
    ${cmd} 
EOS
`;

console.log(ssh);
exec(ssh, console.log);
```

ただし終わりの`EOS`はインデントすることができません。`EOS`の前に空白を入れると、コマンドとして認識されエラーになります。見栄えがよくないので、このことを忘れていて何かの拍子にインデントしてしまい「あれ?動かなくなった?」ということもあるかもしれません

##### 案4: `base64`を使う

私なりに考えた結果、コマンドを`base64`でエンコードしてしまえば、そもそもエスケープから解放されるのでは?との結論にいたり、ソフトウェア上でもShellコマンドは全て`base64`でエンコードし、実行時に`base64 -d`でデコードするよう実装することにしました

今のところ自分で利用している分には、問題なく動作しています

```Node.js
const exec=require('child_process').exec;


const cmd=String.raw`
  cat /etc/hosts \
  |grep "^192" \
  |awk '{print $2}' \
  |sed -e "s/^target\([1-3]\)$/updated\1/g" \
  > ~/tmp/results.hosts
`;

const enc=Buffer.from(cmd)
  .toString('base64');

const ssh=`
  echo ${enc} | ssh target1 "$(base64 -d)"
`;

console.log(ssh);
exec(ssh, console.log);
```


以上です。何か良い方法があれば、ぜひ教えていただければと思います

ありがとうございました


※以降、追記前の記事

---



記事: https://qiita.com/mjusui/items/bce4566ffdb54ba7039a
ツール: Submarine - https://gitlab.com/mjusui/submarine

↑で紹介しているSubmarineというツールを作成しているときに、ひらめいた!

通常ssh先でコマンドを実行したい場合は以下のように書くと思います

```
ssh HOSTNAME COMMAND
```

ところが、実際にコマンドを実行してみるとコマンドの一部が、ssh元の方で評価されたり `¥` エスケープがうまくいかずにエラーになることがあります

そんなときにはssh先で実行したいコマンドをパイプで渡してやると、うまく行くことがあります

```
echo COMMAND | ssh HOSTNAME "$(cat -)"
```

しかし、それでも何らかの理由で失敗する場合はbase64でエンコードして、ssh先でデコードしてあげると、エスケープを気にせず、生のコマンドをssh先で実行できます

```
### エンコード
echo command.txt | base64 > encoded.txt

###　実行
cat encoded.txt | ssh HOSTNAME "$(base64 -d)"
```