Shell Scriptで検索コマンドを作ってみました

この記事は、 CAMエンジニア [Advent Calendar 2019](https://qiita.com/advent-calendar/2019/cam-inc) 22日目の記事です。
昨日は @gucciNa さんの[Lottieで少し遊ぶ](https://qiita.com/gucciNa/items/1a106434a3e00c631a94) でした。


# はじめに
こんにちは。cotsupaです。
この記事では、
**Shell Script**で複数ファイルから文字列を検索するコマンド（findgrep.sh）を作る事を通して、
簡単なShell Scriptの構文を学ぶ事が出来たので、出来るだけ詳細な説明をいれて、まとめました。


## とりあえず完成形はこちら


``` findgrep.sh
#!/bin/bash

pattern=$1
directory=$2
name=$3

#第２引数（起点ディレクトリ）が空文字列ならデフォルト値として.（カレントディレクト）を設定//②
if [ -z "$directory" ]; then
  directory='.'
fi

#第３引数（検索ファイルパターン）が空文字列ならデフォルト値として'*'を設定//②
if [ -z "$name" ]; then
  name='*'
fi

#引数がない場合はエラーメッセージを表示//③
if [ "$#" -eq 0 ]; then
  echo "Usage: `basename $0` PATTERN [PATH] [NAME_PATTERN]" 1>&2
  exit 1
fi

#検索ディレクトリが存在しない場合はエラーメッセージを表示//③
if [ ! -d "$directory" ]; then
  echo "`basename "$0"`: ${directory}: No such directory" 1>&2
  exit 2
fi

#対象ファイルから文字列を検索//①
find "$directory" -type f -name "$name" | xargs grep -nH "$pattern"
```

##使用方法
「検索パターン」、「対象始点ディレクトリ」、「対象ファイル」を引数に指定する事で、
始点ディレクトリに含まれるファイル全てを対象に文字列の検索を行います。


```
// ./配下の'*.text'に該当するファイル内でhogeを検索
$ findgrep.sh hoge . '*.text'
./doc/example.text:15:hogehogehoge
./doc/example.text:121:hoge
./README.text:185:(hoge)
```

# Scriptの解説
上記のScript内で番号を振った順に解説していきます。
前提として、Shell Scriptでは特殊パラメータが存在し、今回は下記を使用して変数を定義しています。


|特殊パラメータ |意味 |
|---|---|
|\$0 |実行しているScriptのファイルパス |
|\$1〜\$9 |引数（\$1は第一引数、$2は第二引数...） |
|\$# |  Scriptに与えた引数の数|

##1,　引数で指定した対象のファイルから文字列を検索


``` .sh
#対象ファイルから文字列を検索//①
find "$directory" -type f -name "$name" | xargs grep -nH "$pattern"
```

findコマンドとxargsコマンドをパイプライン演算子（|）で繋げて実行しています。

- **パイプライン演算子**
 - コマンドの標準出力を別のコマンドの標準入力に繋ぐことでコマンドの連携をさせる演算子

- **findコマンド**
 - 指定ディレクトリからファイルを検索し、出力するコマンド
 - `-type f`でファイルのみを指定、`-name "$name"`で第三引数のファイル名を指定しています。
 
- **xargsコマンド**
 - `xargs [実行したいコマンド]`という形式で実行し、[実行したいコマンド]が標準入力から受け取ったリストを引数として実行します。
 - 今回はfindコマンドの出力結果をgrepコマンドが引数として実行しています。

- **grepコマンド**
 - 引数のファイルから文字列を含む行を出力します（出力結果を絞って表示する際によく使います）
 - `-n`で検索結果の行数を出力し、`-H`で引数にファイルが一つしか指定されなくてもも必ずファイル名を出力します。
 
また、`"$directory"`など変数をダブルクォートで囲うことで変数展開しています。
（詳しくはCAMエンジニア Advent Calendar 2019 18日目[shellの基礎構文](https://qiita.com/mori_keisuke/items/c2a5e04199b45619d904)）

## 2, 引数が指定されなかった場合の条件分岐
```.sh
#第２引数（起点ディレクトリ）が空文字列ならデフォルト値として.（カレントディレクト）を設定//②
if [ -z "$directory" ]; then
  directory='.'
fi

#第３引数（検索ファイルパターン）が空文字列ならデファルト値として'*'を設定//②
if [ -z "$name" ]; then
  name='*'
fi
```
「対象始点ディレクトリ」、「対象ファイル」は必要な場合のみ指定できるように制御構文を使用してそれぞれデフォルトの値を設定しておきます。

- **制御構文**
 - `if`と`then`を使用して条件分岐を記述
 - `-z`は文字列長が0なら真となります。
 
（制御構文、シングルクォートによる変数展開の詳細はCAMエンジニア Advent Calendar 2019 18日目[shellの基礎構文](https://qiita.com/mori_keisuke/items/c2a5e04199b45619d904)）

##3, エラーメッセージの表示
``` .sh
#引数がない場合はエラーメッセージを表示//③
if [ "$#" -eq 0 ]; then
  echo "Usage: `basename $0` PATTERN [PATH] [NAME_PATTERN]" 1>&2
  exit 1
fi

#検索ディレクトリが存在しない場合はエラーメッセージを表示//③
if [ ! -d "$directory" ]; then
  echo "`basename "$0"`: ${directory}: No such directory" 1>&2
  exit 2
fi
```
制御構文を使用して、引数がない場合と存在しないディレクトリ名が指定された場合にエラーメッセージを出力させます。

- **制御構文**
 - `-eq`は等しければ真となります。
 - `-d`はディレクトリなら真になります。
 - `!`はNOT条件です。

`1>&2`は出力リダイレクトの記法で、標準出力（１番）を標準エラー出力（２番）と同じにする指定をしています。
一般的なLinuxコマンドはエラーメッセージを標準エラー出力に出力するため、このコマンドもそれに従います。そうする事で、パイプラインなどで別のコマンドに接続する際に支障がなくなります。
（他にも標準出力をファイルにリダイレクトした際でも標準エラー出力は画面に出力されるため見逃さないなどの利点があります。）


また、exitコマンドで終了ステータスを指定しています。一般的に終了ステータスは0が正常終了で、それ以外の整数がエラー終了を示します。ここではエラー終了なので、1と2を終了ステータスとしています。（エラーの種類がわかるように別の整数にします。）

- **exitコマンド**
 - シェルを終了させるコマンド。
 - 引数にシェルの終了ステータスを指定できる。

さらに、basenameコマンドを使って、ファイル名を出力しています。

- **basenameコマンド**
 - 引数のパス部分を取り除いて、ファイル名を取り出すコマンド

バッククォートによる変数展開についてはCAMエンジニア Advent Calendar 2019 18日目[shellの基礎構文](https://qiita.com/mori_keisuke/items/c2a5e04199b45619d904)をご覧下さい。

# まとめ
これまで、ほとんどShell Scriptに触れてこなかったので、
実際に作って試してみると単純なロジックでも引っかかる部分が多かったです。
最後に、Shell Scriptを書く際に`$ bash -x [コマンド]`でデバックモードになるので効率よくScriptを作成できるみたいです。もっと早く知りたかったです。

**参考資料**
[新しいLinuxの教科書　[著]三宅英明・大角祐介](https://www.amazon.co.jp/%E6%96%B0%E3%81%97%E3%81%84Linux%E3%81%AE%E6%95%99%E7%A7%91%E6%9B%B8-%E4%B8%89%E5%AE%85-%E8%8B%B1%E6%98%8E/dp/4797380942)
[shellの基礎構文](https://qiita.com/mori_keisuke/items/c2a5e04199b45619d904)