Bash: わかるとほんのちょっとうれしくなること５選

Bash の勉強を始めた頃、頭がごちゃごちゃになって調べたこと、わかるとほんのちょっとうれしくなったことを５個ピックアップしてみました。
## - 目次 -
1. alias sudo='sudo ' と alias sudo='sudo -E '
1. bash -c
1. /dev/null
1. grep -rl
1. xargs で任意の位置に引数を展開する
## - 説明 -
## 1. alias sudo='sudo '
alias コマンドを使うと、長いコマンドを別名で登録できて、とても便利です。
でもそのまま `sudo` と組み合わせると、エラーになります。
例えば、`la` という登録名で、`ls -a` を実行させると、
```bash
$ alias la='ls -a'
$ la
$ sudo la      #エラー
```
`sudo la` でエラーになります。
bash のマニュアルには、以下のように書かれています。
>alias [-p] [name[=value] ...]
> value の末尾に空白があると、エイリアスが展開されたときに、 空白の次の単語についてエイリアス置換があるかどうか調べられます。

つまり` alias sudo='sudo '` のように、`sudo` の末尾に空白があるときだけ、`sudo` の次のコマンドが `alias` の登録コマンドかどうかチェックするらしいです。それ以外はチェックせずにエラーになってしまうようです。まあ、そういう仕組みだから覚えるしかなさそうです。

それともう一つ。例えば、`vim` でコメントが見づらいので、色変更し、その設定を `~/.vimrc` に書いたとします。でも管理者権限が必要なファイルを `sudo vim` で開くと、色の変更が反映されていません。
それは設定ファイル（通常 `/etc/sudoers` ）に環境変数をリセットする記述があるからです。
```bash
$ sudo cat /etc/sudoers | grep 'env'
Defaults        env_reset
```
そこで `sudo` に `-E` オプションをつけると、その**環境変数のリセットを無効**にしてくれるようです。なので一般ユーザで設定した環境変数がそのまま反映されるようになります。`alias` で登録すると、以下のようになります。
```bash
$ alias sudo='sudo -E '
```
設定を永続化したい場合は、`~/.bashrc` に書いたあと、`source ~/.bashrc` で反映されるようになります。
`sudo` に `-E` オプションをつけたくない場合は、個人の PC なら、シンボリックリンクを使う方法もあります。
`vim` の例だと、
```bash
$ sudo ln -s ~/.vimrc /root/.vimrc
```
**参考サイト**
https://penpen-dev.com/blog/sudo-ll-zikkou/
https://yudoufu.hatenablog.jp/entry/20110326/1301129885

## 2. bash -c
シェルスクリプト（bash）を書くとき、よく使うのが `-eu` オプション
```bash
#!/bin/bash
set -eu
```
または、
```bash
#!/bin/bash -eu
```
> `set -e` : エラーがあったらシェルスクリプト終了

> `set -u` : 設定していない変数があったらエラー

では `bash -c` は、使ったことがあるでしょうか？ これが割と便利なオプションなのです。`-c` オプションをつけると、文字列からコマンドを読み取ることができます。その文字列の中に複数のコマンドがあっても `;` や `|` などで区切ってあれば、その複数のコマンドが実行できるのです。例えばこんな感じです。
```bash
$ bash -c "mkdir Test && touch Test/test.txt"
```
`man bash` でマニュアルを見ると、
>オプション
 -c string -c オプションが指定されると、コマンドが string から読み込まれます。 string  の後に引き数があれば、これらは 位置パラメータ (positional parameter: $0 から始まるパラメータ) に代入されます。

引数があるときは、`$0 $1 $2...` で値が取れて、引数がないときは、`$0` は実行コマンド名になるようです。
```bash
$ bash -c 'echo hello $0' world 
hello world
$ bash -c 'echo $0'
bash
```

あと、プログラムからプログラムを起動するときなどにも利用できます。
では例えばということで、PowerShell から WSL2 起動時にコマンドを渡して実行したい場合、
```bash
> wsl --exec bash -c "echo hoge | sed 's/hoge/fuga/'"
fuga
```

Windows では、上記のようなコマンドをバッチファイルにしてタスクスケジューラで自動化させたりできます。

**参考サイト**
https://qiita.com/ukinau/items/410f56b6d777ad1e4e90

## 3. /dev/null
### ・/dev/null とは
`/dev/null` は特殊なファイルです。`/dev/null` への出力は、すべて廃棄されます。ゴミ箱をイメージするといいかもしれません。

### ・コマンド >/dev/null 2>&1 の意味
いっぱい情報があるのでざっくりと説明します。
以下のように **1** と **2** の数字の意味とリダイレクト（>）の意味を覚えます。

> 1 標準出力
> 2 標準エラー出力

> リダイレクト（>）を使うと、出力先を変更できる。
> リダイレクト元 > リダイレクト先

次に書き方です。

標準出力を標準エラー出力にリダイレクトにさせる場合、
```bash
1>&2          # リダイレクト先の 2 の前に & をつける
```
標準エラー出力を標準出力にリダイレクトさせる場合、
```bash
2>&1          # リダイレクト先の 1 の前に & をつける
```
標準出力をすべて破棄させる場合、
```bash
1>/dev/null
```
以上のことがわかれば、`コマンド >/dev/null 2>&1` の意味がわかると思います。
`コマンド >/dev/null` は、`コマンド 1>/dev/null` と同じ意味です。結果は、標準出力も標準エラー出力も両方とも破棄されます。図にすると、こんな感じです。

![qiita.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1958320/79446118-250b-5576-25db-8c89c618cf17.png)


あと、`/dev/null` には次のような使い方もあります。標準出力を破棄して、実行したコマンドの終了ステータスで条件分岐しています。
```bash
#!/bin/bash
if grep 'hoge' test.txt >/dev/null; then
    echo 'hoge が含まれています'
else
    echo 'hoge が含まれていません'
fi
```
**参考サイト**
https://qiita.com/ritukiii/items/b3d91e97b71ecd41d4ea

## 4. grep -rl
`grep -rl` は使っていますか？ こちらも便利なので覚えておくといいかもしれません。まずは、`-r` と `-l` のそれぞれのオプションの意味です。

> `grep -r` : 指定ディレクトリを再帰的に検索

`cp -r` とか `rm -r` のあの `-r` です。
> `grep -l` : パターンを含むファイル名だけを標準出力で表示

したがって、`grep -rl` で、特定の文字列（パターン）を指定ディレクトリ以下で再帰的に検索し、該当したファイル名だけを出力します。
> `grep -rl [検索する文字列（パターン）]  [検索対象のパス]`

ちなみに `find ～～ | xargs grep ～～` でも似たようなことができます。

次の `xargs` の説明でも使うので、以下のようなツリー構造のディレクトリ・ファイルを作成しました。

![qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1958320/8ea21e6d-5ca5-3cca-2006-765e96854b6b.png)


```bash
$ cat Test/file1
hogefugapiyo
$ cat Test/dir1/file2
fugapiyohoge
$ cat Test/dir1/dir2/file3
piyofugahoge
```
では確認してみます。
```bash
$ grep -r 'hoge' Test
Test/file1:hogefugapiyo
Test/dir1/file2:fugapiyohoge
Test/dir1/dir2/file3:piyofugahoge
```

```bash
$ grep -rl 'hoge' Test
Test/file1
Test/dir1/file2
Test/dir1/dir2/file3
```

## 5. xargs で任意の位置に引数を展開する
`xargs` はオプションなしで実行すると、最後に引数が追加されます。
でもまとめて処理されるので、1 行ずつ処理したい場合は、`-L1` か `-l` （注意、小文字のエル）オプションをつけます。
```bash
$ grep -rl 'hoge' Test | xargs echo
Test/file1 Test/dir1/file2 Test/dir1/dir2/file3
$ grep -rl 'hoge' Test | xargs -L1 echo
Test/file1
Test/dir1/file2
Test/dir1/dir2/file3
```

`-I<置換文字列>` オプション（注意、大文字のアイ）をつけると、任意の位置に引数を展開できるようになります（1 行ずつ処理）。例えば、`cp` コマンドや `mv` コマンドを実行する場合は引数が 2 つ必要ですが、そのようなときに便利です。あと、`echo` で文字列の間に挿入したい場合とか。**置換文字列**には、`{}` がよく使われますが、`@` とか `XXX` とかでもオッケーです。覚えておくとこれまた便利なオプションです。
```bash
$ cat test.txt
hoge
fuga
piyo
$ cat test.txt | xargs -I{} echo AAA{}BBB
AAAhogeBBB
AAAfugaBBB
AAApiyoBBB
$ find . -name "*.log" | xargs -I{} cp {} ./mylog

```
上記で説明した `grep -rl` と `bash -c` を使って、ちょっと複雑なコードを書いてみました。

```bash
$ mkdir Test/Bkup
$ grep -rl 'hoge' Test | xargs -I{} bash -c "sed -i.bak 's/hoge/****/' {} && mv {}.bak Test/Bkup"
```
コードの内容を説明します。ファイル内の検索語を置換する前に `mkdir Test/Bkup` で、あらかじめバックアップ用のディレクトリを作っておきます。`grep -rl` で `hoge` の文字列があるファイルを検索して見つけ、その該当ファイル名を `xargs` の次のコマンドに 1 つずつ展開していきます。
展開位置は、`-I` で指定した `{}` です。ところが、`xargs` の次のコマンドが、`bash -c` なので、そのあとの `""` 内の複数のコマンドで展開位置を指定できるようになっています。あとは、`sed -i.<指定拡張子>` のオプションの意味がわかれば理解できると思います。これは**指定拡張子でのバックアップと上書き保存**が同時に指定できるオプションです。では、結果を確認してみます。
```bash
$ cat Test/dir1/file2
fugapiyo****
$ ls Test/Bkup
file1.bak  file2.bak  file3.bak
$ cat Test/Bkup/file2.bak
fugapiyohoge
```
うまくいっているようです。
以上です。お付き合いいただきありがとうございました。