シェルスクリプトからコマンド実行（っぽいもの）を抜き出すワンライナー

こちらは、[ハンズラボ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/handslab) 4日目の記事になります！

# はじめに

最近うまいもの食べすぎで太ってきた[yktakaha4](https://twitter.com/yktakaha4)です...🐖

[Amazon Linuxのセキュリティサポート終了まであと半年](https://aws.amazon.com/jp/amazon-linux-2/faqs/#Support_for_Amazon_Linux_2_LTS_Candidates_and_Amazon_Linux_AMI)となりましたが、みなさま対応は進んでいますでしょうか？
私の所属するCRMチームでは、主にバッチサーバとしての用途でAmazon LinuxのEC2がいくつか稼働しています
以前から[バッチ処理のPython化&ECSタスク化](https://www.hands-lab.com/tech/entry/4674.html)も進められていますが、全てのコードを置き換えるには時間がかかるので、並行してAL2のEC2へのプログラム移行に着手しています

弊社は[ユニケージ開発手法](https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%8B%E3%82%B1%E3%83%BC%E3%82%B8%E9%96%8B%E7%99%BA%E6%89%8B%E6%B3%95)という、シェルスクリプトでシステム開発をおこなう手法を広く採用していた経緯があり、
CRMチームでも、特にバッチ領域についてはbashで書かれた業務ロジックがまだまだ残っています
ユニケージを使用しているサーバには、`self`や`join1`といった独自コマンドがいろいろインストールされているのに加え、
`nkf`をはじめとした標準で入ってないコマンドも適宜追加されており、移行の障壁となりそうです...

構築当初（何年も前のことです...）のドキュメントを漁ったり、`yum`から追うのもよいですが、
実際に使われるコマンドは大抵限られており、また移行の過程で使わなくなったものもあるはずなので、
インストール&動作検証対象を減らすためにも、ソースコード上で利用されているコマンド一覧（とできれば件数）が分かると良さそうです

# 結論

なんかできました👹

```terminal:ターミナル（Macの場合）
$ find . -name "*.bash" | xargs ggrep -oP '(^|(?<=;)|(?<=do )|(?<=then )|(?<=`)|(?<=&&)|(?<=\|\|)|(?<=\|)|(?<=\$\())(\s*\w+=\w+\s)?\s*\w+(?=$|[^=])' | awk -F: '{print $NF}' | awk '{print $NF}' | sort | uniq -c | sort -nrk1 | head -5
 500 error_exit <- ※こいつはfunction。引っかかってしまうがしゃーなし
 400 source
 300 cat
 200 basename
 100 plus <- ※こいつはユニケージコマンド
```

上述の通り、functionなども拾ってしまうため目検は必要ですが、
今回だと `1 lha` [（こいつ）](http://www.k-tanaka.net/unix/lha.php)みたいな結果も得られたのでそれなりに満足です

目で見るのが面倒な方は、移行元のAmazon Linuxでコマンド（っぽい文字列たち）を`whereis`にかけるとよいでしょう
更にその結果から抽出したコマンドを移行先のAmazon Linux2で`whereis`して、出てこなかったものが要インストール...という考え方でもよいかもしれません

```terminal:ターミナル
$ cat commands.txt | head -5
000
awk
basename
cat
cd

$ whereis $(cat commands.txt) | head -5
/usr/bin/awk
/usr/bin/basename
/bin/cat
/usr/bin/cd
/bin/cp
```

# 解説

ワンライナーだと見づらいので、改行&コメントをつけてみました

```bash:整理する
# 現在ディレクトリ配下のbashファイルを抽出
find . -name "*.bash" |
# ファイルごとに、正規表現にてコマンドっぽい文字列を抽出（後述）
xargs ggrep -oP '(^|(?<=;)|(?<=do )|(?<=then )|(?<=`)|(?<=&&)|(?<=\|\|)|(?<=\|)|(?<=\$\())(\s*\w+=\w+\s)?\s*\w+(?=$|\W)' |
# ファイルパス:コマンドっぽい文字列 の形式で出力されるので、コロン区切りで最終フィールドを抽出
awk -F: '{print $NF}' |
# スペース区切りで最終フィールドを抽出（理由は後述）
awk '{print $NF}' |
# カウントのために並び替え
sort |
# 件数カウント
uniq -c |
# 件数順に並び替え
sort -nrk1 |
# 上位5件を表示
head -5
```

途中、`ggrep`という馴染みのないコマンドがありますが、こちらはMac環境で上記ワンライナーを実行する際に必要になるものです
今回grepコマンドの正規表現にて[肯定先読みと肯定後読み](https://qiita.com/tohta/items/2ba7ecde5636b38ef1f6)を使用していますが、こちらを利用するために必要な`-P`オプション（[PCRE](https://ja.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions)準拠の正規表現を使用）が、Macにデフォルトで入っているBSD版のgrepコマンドだと使えないため、追加でインストールしています
Amazon Linux上で実行する場合は、単純に`grep`に変更すれば動作するはずです

```terminal:ターミナル
# インストール
$ brew install grep

# BSD grepはだめ　Macにはデフォルトではこちらが入っている
$ grep -V | head -1
grep (BSD grep) 2.5.1-FreeBSD

# GNU grepなら🙆‍♀️
$ ggrep -V | head -1
ggrep (GNU grep) 3.3
```

本記事のキモとなる正規表現の部分ですが、[Debuggex](https://www.debuggex.com/r/mrXJEWqDGZJ0HTBW
)というサイトで可視化してみました

![regex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/d2dab909-1427-4932-92f8-c0a5b3d46c39.png)

仕様としては、大体以下のような感じになるはずです
[こちら](https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html)なども参照しつつ、思いつく限りのコマンド実行のパターンを列挙したつもりですが、抜けやバグがあるかも...
ただ、主だったパターンを抽出できれば十分なので、とりあえず納得して使っています

|検出|内容|例|備考|
|---|---|---|---|
|◯|普通のやつ| cat example.txt |
|◯|1行に複数コマンド| cat example.txt; echo $? |
|◯|パイプして複数コマンド| cat example.txt &#124; grep -c "hoge" |
|◯|制御構文| for f in *; do echo $f; done <br> if [ -f example.txt ]; then echo "found!"; fi |
|◯|制御演算子| [ -f example.txt ] && echo "found!" <br> [ -f example.txt ] &#124;&#124; echo "not found..." |
|◯|コマンド置換| echo "時刻は \`date` です" <br>echo "時刻は $(date) です"|
|◯|変数定義&コマンド| LANG=C sort -u |ユニケージで頻出。意味は[こちら](https://qiita.com/bass_ikeda/items/e62f8514330baa35d843)|
|×|コマンドの引数にコマンド|timeout 10 ping<br>eval 'echo "hello"'|`timeout` `eval`しか検出できない|
|×|変数展開された結果コマンド|cmd="whoami"; $cmd|🤗|

なお、上記とは別に`ヒアドキュメントや複数行中の文字列`や`function`、`alias`などについても誤検出されます

# おわりに

現在は、コマンド調査の結果を元に作成したAL2のDockerイメージ上でスクリプトを実行し、ディレクトリの権限やユーザなどの設定のうち必須そうなものを洗い出しています
ECSになるかDocker on EC2になるかEC2になるかはまだ決めかねていますが、年度末に控えたRI購入イベントまでに決着するよう頑張りたいと思います...！

[ハンズラボ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/handslab) 明日は、[yuka_jyotei](https://twitter.com/yuka_jyotei)さんです！