【LInux】実際に作りながら学ぶシェルスクリプト

## はじめに
今回は、シェルスクリプトについて学んだことをまとめていきたいと思います。シェルスクリプトの概要や基本的な構文を押さえた後、指定したディレクトリ配下のファイルの内容を確認しながら、リネームを行うシェルスクリプトを作成していきます！

## シェルスクリプトとは？
シェルの一連のコマンドを記載したファイルのこと
### シェルスクリプトの特徴
- if文による条件分岐、for文による繰り返し処理なども記載することができ、プログラミング言語のように扱える
- ファイルに記載することで、都度コマンドを打たなくても、記載した一連のコマンドを実行することができる
- ファイルを他の人に配布することで簡単に共有ができる

### シェルスクリプトの基礎
### 1.ファイルの拡張子
`.sh`にするのが慣習
例:`hoge.sh`

```hoge.sh
#!/bin/bash
この下に様々なコマンド、処理をかいていく
echo 'hoge'
```

### 2.`#!`(シバン)について
ファイルの先頭に記載された`#!`のことを**シバン**と呼ぶ
シェルから実行命令を受けたLinuxカーネルはまず、ファイルの先頭行を見に行く
そのとき`#!`があれば、その後に記載されたコマンドを実行する仕様になっている

そのため、上記のhoge.shを実行した場合
```bash
./hoge.sh
```
Linuxカーネルは#!の後を見て`hoge.sh`を`/bin/bash`で実行する

明示的にどのシェルでスクリプトを実行するか指定することもできる。
例えば、以下のように`bash`シェルでスクリプトを実行する場合

```bash
/bin/bash ./hoge.sh
```

以下のように処理される

1. **`/bin/bash`が先に実行され、Bashシェルが起動される**。
2. **Bashシェルが`./hoge.sh`を読み込み、スクリプトを実行する**。
3. スクリプト内の**シバン（`#!`）はコメントと同様に扱われ、無視される**。これは、シェルがすでに起動しているため
4. そのため、Bashシェルがそのままスクリプトの次のコマンドを順次実行する

シェルスクリプトが`bash`で実行される際、**シバン行（`#!`）**はシェルを指定するためのものだが、明示的にシェルを指定してスクリプトを実行した場合にはシバン行は無視され、次のコマンドが実行される

### 3.`source`コマンドによる実行
`source`コマンドでもシェルスクリプトを実行することができる
```bash
source <ファイルパス>
```
#### sourceコマンドによる実行とファイル名指定での実行の違い
- sourceコマンドで実行した場合、ジバンはいらない
  - 何故なら、sourceコマンドはカレントシェル(現在開いているシェル)の設定を引き継いで実行するため。
- ファイル名指定の場合はカレントシェルからサブシェルを開いて実行する
- sourceコマンドで実行した場合、カレントシェルの設定を引き継ぐため、エイリアスや設定の変更などスクリプト実行後も引き継ぎたい場合に使う
- 基本的には、副作用などさけるためファイル名指定で実行する

### 4.サーチパスの設定
ファイル名など、毎回明示的に指定せず使いたい場合、自分のシェルスクリプト置き場となるディレクトリを作成し、それをサーチパスに含めることで通常のコマンドのように使うことができる

#### サーチパスとは？
シェルがコマンドを実行した時に、コマンドを探す対象のパスを格納しているシェル変数のこと
```bash
$PATH
```
という名前で定義されている
ここには`:`区切りでサーチ対象のパスが格納されている
**例**
```bash
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
```
この末尾にディレクトリのパスを追加してあげれば良い
```bash
PATH="$PATH: ~/bin"
```
これでシェルを再起動すると
```
<コマンド名>
```
と打つだけで、シェルがサーチパスに定義された`~/bin`内から指定したコマンドを探し実行してくれるようになる

## シェルスクリプトの基本構文

https://qiita.com/rgj0830/items/2491ea89acd8859bf113

こちらにまとめたのでよければ参照してください

## 実際にシェルスクリプトを作ってみる
学んだことをもとに、指定されたファイルやディレクトリ内のファイルを対話的にリネームするシェルスクリプトを作ってみたいと思います！

### スクリプトの使用用途
なぜこのスクリプトを作ることにしたかというと、いつも本を読むときにマークダウンでメモを取っているのですが、ファイル名を適当に付けてしまい、後から見返すときに何が書かれているのかファイル名だけでは判断できないことが多いからです！笑
1冊の本を読み終わる頃には20ファイル以上になることもあったり、毎回GUIでファイルを開いて確認するのが面倒でした。なので、ファイルの中身を確認しながら、対話的にファイル名を変更できる機能を勉強がてら作ろうと思いました！(あんまり実用性ないかもですが、勉強目的なのでその辺は気にしないことにします）

基本的な機能は以下の通りです
### 1. オプション解析
- `-d`オプションが指定されると、**ディレクトリモード**が有効になり、ディレクトリ内の全てのファイルを処理する
- `-d`オプションが指定されない場合は、**デフォルトモード**として、1つのファイルを処理する

### 2. デフォルトモード（ファイルリネーム）
- 最初に、引数に指定されたファイル（`$1`）が存在するかどうかを確認する。
- ファイルが見つかった場合、その内容を表示し、ユーザーにリネームするかを確認する
  - 「y」を入力すると新しいファイル名を入力し、ファイルをリネームする
  - 「n」と入力するとファイルはリネームされない

### 3. ディレクトリモード（複数ファイルのリネーム）
- 指定されたディレクトリ内のファイルをリストアップし、各ファイルに対して以下の操作を繰り返す
  - ファイルの先頭10行を表示し、リネームするかを確認する
  - 「y」と入力すると新しいファイル名を入力し、ファイルをリネームする。
  - 「n」と入力するとファイルはそのまま。
  - 「q」と入力するとクリプトを終了する

### 作成したシェルスクリプト
```bash
#!/bin/bash

mode='default_mode'
target=$1

while getopts "d" opt; do
    case $opt in
        d)
            echo "Directory mode enabled."
            mode=directory_mode
            shift $((OPTIND-1))
            target=$@
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            ;;
    esac
done

# Function to rename the file
function file_renamer() {
if [ ! -f "$target" ]; then
echo "File not found"
exit 1
fi

cat "$target"
echo "Do you want to rename the file? (y/n)"
read answer
if [ "$answer" = "y" ]; then
    echo "Enter the new name for the file"
    read new_name
    mv "$target" "$new_name"
    echo "File renamed"
else
    echo "File not renamed"
fi
}

function interactive_rename_all_files
{
    if [ ! -d "$target" ]; then
        echo "Directory not found"
        exit 1
    fi
    target_files=$(ls -l $target | awk '$1~/^-/{print $9}')
    for file in $target_files
    do
      while true; do
        head -n 10 "$target$file"
        echo "Do you want to rename the file $file? (y/n/q)"
        read answer
        case $answer in
            y)
                echo "Enter the new name for the file"
                read new_name
                mv "$target$file" "$target/$new_name"
                echo "File renamed"
                break
                ;;
            n)
                echo "File not renamed"
                break
                ;;
            q)
                echo "Exiting"
                exit 1
                ;;
            *)
                echo "Invalid input. Please enter y/n/q"
                ;;
        esac
     done
    done
}

case $mode in
    directory_mode)
    echo "$target"
        interactive_rename_all_files
        ;;
    default_mode)
        file_renamer
        ;;
esac
```

## 解説
### 1.シェル変数にmodeとtargetを設定
```bash
mode='default_mode'
target=$1
```
`mode`のデフォルト値にdefault_mode
`target`に引数で受け取ったファイルパスをセットする

### 2.オプションが指定されているか確認
```bash
while getopts "d" opt; do
    case $opt in
        d)
            echo "Directory mode enabled."
            mode=directory_mode
            shift $((OPTIND-1))
            target=$@
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            ;;
    esac
done
```
`getopts`コマンドを使って引数にオプションが含まれているか確認する。
`getopts`コマンドは第1引数に指定された文字列のオプションが含まれているか確認し含まれていれば第2引数の`opt`に格納します。
ここでは`d`オプションが含まれていればoptに格納されます。
そしてcase文でopt変数の中身を確認し、`d`ならディレクトリモードに変更しています。
`shift $((OPTIND-1))`はオプション部分だけを削除するコマンドです。
オプション部分を取り除いた後`target`に`$@`で残りの全ての引数を入れることで、適切に指定されたパスが入るようにしています。
`\?`はそれ以外の場合の条件ですね。

### 3.ファイル指定時のリネーム関数
```bash
# Function to rename the file
function file_renamer() {
if [ ! -f "$target" ]; then
echo "File not found"
exit 1
fi

cat "$target"
echo "Do you want to rename the file? (y/n)"
read answer
if [ "$answer" = "y" ]; then
    echo "Enter the new name for the file"
    read new_name
    mv "$target" "$new_name"
    echo "File renamed"
else
    echo "File not renamed"
fi
}
```
これはファイルを指定されたときのrenameを行う処理です。
`if [ ! -f "$target" ]; then`
でtargetがファイルじゃなかった場合処理を終了するようにしています。
そのあと`cat`で指定されたファイルの内容を表示し
renameするか確認をします。
`read`コマンドはユーザからの入力を待ち受けるコマンドです。
入力された内容は引数の変数に格納されます。
` mv "$target_file" "$new_name"`
そして入力された内容でmvコマンドを実行してファイル名をかえています。



### 4.ディレクトリ指定時のリネーム処理
```bash
function interactive_rename_all_files
{
    if [ ! -d "$target" ]; then
        echo "Directory not found"
        exit 1
    fi
    target_files=$(ls -l $target | awk '$1~/^-/{print $9}')
    for file in $target_files
    do
      while true; do
        head -n 10 "$target$file"
        echo "Do you want to rename the file $file? (y/n/q)"
        read answer
        case $answer in
            y)
                echo "Enter the new name for the file"
                read new_name
                mv "$target$file" "$target/$new_name"
                echo "File renamed"
                break
                ;;
            n)
                echo "File not renamed"
                break
                ;;
            q)
                echo "Exiting"
                exit 1
                ;;
            *)
                echo "Invalid input. Please enter y/n/q"
                ;;
        esac
     done
    done
}
```

` if [ ! -d "$target" ]; then`
ここで指定したtargetがディレクトリじゃない場合、処理を終了しています。
`target_files=$(ls -l $target | awk '$1~/^-/{print $9}')`
上記はコマンド置換でコマンドの結果を`target_files`に入れています。
ls -lで指定したディレクトリのファイルを標準出力にして`awk`コマンドに渡しています。
awkコマンドでは正規表現で`$1`(1列目)の`~-`先頭が`-`つまり属性がファイルのファイル名だけ出力するようにしています。
`for file in $target_files`
あとはforで回して、全てのファイルに対して処理を行えるようにしています。
ファイル数が多いのに全ての内容表示しているとターミナルが流れてしまうので、ディレクトリを指定した時は、先頭の10行だけ表示してファイルの内容を確認できるようにしています。
`head -n 10 "$target$file"`

ここでtargetを入れているのはディレクトリのパスも含めるためです。
`mv "$target$file" "$target/$new_name"`

### 5.modeによる条件分岐
```
case $mode in
    directory_mode)
    echo "$target"
        interactive_rename_all_files
        ;;
    default_mode)
        file_renamer
        ;;
esac
```
modeをcase文で確認し、modeに応じて作成した関数を呼び出すようにしています。

## 実際に使ってみる
### 1.ファイルを指定して実行
以前書いたDockerの学習メモのファイルを変更してみます！

```bash
$ ./file_renamer.sh docker-No1.md
```
**出力結果**
```
## コンテナとは？

コマンドを実行するための独立した領域
コンテナはイメージから作られる
コンテナ A とコンテナ B は干渉しない。
それぞれが PID1(親プロセス)を持つ
そのため、異なるコンテナ同士で同名のファイルやプロセスがあっても問題ない

## イメージとは

tar アーカイブファイル(レイヤとも呼ぶ)を重ね合わせたもの。
例えば Ubuntu の tar ファイル
PHP の tar ファイルそれらを重ね合わせることで、１つのファイルシステムを実現する

## イメージの詳細の確認
.......etc
Do you want to rename the file? (y/n)
```
ファイルの内容が表示され、名前を変更するか聞かれます。

**yを入力**
```
y
Enter the new name for the file
```

**新しいファイル名を入力してエンター**
```
Enter the new name for the file
docker-container-memo.md
File renamed
```

```bash
$ ls
docker-container-memo.md	
```
名前の変更が確認できました！

### 2.ディレクトリを指定して実行
今回のLinuxの学習メモを置いているディレクト理を指定してみます！

```bash
./file_renamer.sh -d ../memos/linux/
```
```
Directory mode enabled.
../memos/linux/
## Linuxのディレクトリ構成と各ディレクトリの役割

Linuxでは、すべてのファイルやディレクトリはルートディレクトリ（`/`）を頂点とする1つのディレクトリツリーに配置されます。複数のディスクがあっても、このディレクトリツリーは1つだけです。各ディスクは、ディレクトリとしてマウント（結合）されます。

### 主要なディレクトリとその役割

| ディレクトリ | 役割 |
|:---|:---|
| **/** | ルートディレクトリ。全てのディレクトリとファイルの頂点となる。 |
| **/bin** | システム動作に必要な基本的なコマンドの実行ファイルを格納。 |
Do you want to rename the file Linuxのディレクトリ構成.md? (y/n/q)
```

**yを選択して名前を変更**
```
y
Enter the new name for the file
Linux-dir-memo.md
File renamed

# Linuxとは？
Linux = OS
OS = コンピュータを動かす基本的なソフトウェア

## Linuxカーネル
OSの中核、ハードウェアの制御を行うソフトウェア
狭議のLinux

## Linuxディストリビューション
ユーザが使用できるように、Linuxカーネル加えて、基本的なコマンド群やアプリケーションをまとめてパッケージングしたもの
Do you want to rename the file Linuxの概要.md? (y/n/q)
```
次のファイルが読み出されています。
**nを選択**
```
n
File not renamed
# Linuxコマンドまとめ

## 1. コマンド一覧

| コマンド | 説明 | 使用例 | オプション |
|:--|:--|:--|:--|
| `mkdir` | 新しいディレクトリを作成する | `mkdir <ディレクトリ名>` | `-p`: 存在しない親ディレクトリも含めて一気に作成 |
| `touch` | ファイルを作成する | `touch <ファイル名>` | 複数ファイル指定可能 |
| `rm` | ファイルを削除する | `rm <ファイル名>` | `-r`: ディレクトリを再帰的に削除<br>`-i`: 削除確認を行う |
| `rmdir` | 空のディレクトリを削除する | `rmdir <ディレクトリ名>` | 空でないディレクトリを削除しない |
Do you want to rename the file Linuxコマンド1.md? (y/n/q)
```
名前を変更せず次のファイルを読み出せました！

**qを選択して、処理を終える**
```
q
Exiting
```
抜けることができました！

## まとめ
初めて、シェルスクリプトを作成しましたが思っている以上にできることが多くびっくりしました。
今後、業務や個人開発、学習の中で繰り返し使う処理をスクリプトにしたり積極的に活用していきたいです！