sudo rm -rf /*した残骸でpsコマンドを作り直す

## 1 はじめに

[前の記事](https://qiita.com/minnsou/items/60a306510f613a666fec)で、`sudo rm -rf /*`した後に**選択ソートを実装**して、ユーザーが入力した数値列をソートしてファイルに書き出すということをやりました。

`ps`コマンドが使えなかったこともあり、プロセス関連のことはほとんど触れられませんでした。というわけで今回は消え去ってしまった`ps`コマンドを**bash組み込みコマンドのみから実装したい**と思います。ちゃんと今回も最初から`sudo rm -rf /*`します。
<img width="509" alt="スクリーンショット 2020-03-23 17.57.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/c3361952-f35e-1670-fdd1-5ea9b8c1d1fc.png">
基本的に出てくるコマンドや構文は紹介をしますが、関数や変数の定義、配列、if文、for文など前回の記事で紹介したものは詳しく扱いません。[前回の記事](https://qiita.com/minnsou/items/60a306510f613a666fec)を参考にしてください。それではいきましょう。

・参考文献
オライリー・ジャパン [入門bash](https://www.oreilly.co.jp/books/4873112540/)

・実行環境
Raspberry Pi 4 Model B+
Raspbian Buster Lite [2020-02-13-raspbian-buster-lite.img](https://www.raspberrypi.org/downloads/raspbian/)

## 2 下準備

`sudo rm -rf /*`により消えてしまったコマンドはたくさんありますが、`ls`と`cat`はよく使うので再定義しておきます。また補完も変になっているので直しておきます。

### 2.1 lsコマンド

`echo *`を`ls`のエイリアスとする（つまり`alias ls="echo *"`とする）のでも良いですが、せっかくなのでディレクトリは青色、それ以外は白色で表示する`ls`コマンドを作ります。

そのためには`test`コマンド(`[`コマンドでも同じ)を使います。このコマンドでは**ファイル属性演算子**を用いて条件判定を行います。例えば、`-a`というファイル属性演算子は「そのファイルが存在するかどうか」を表します。具体例を見てみましょう。
<img width="269" alt="スクリーンショット 2020-03-23 18.28.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/a55b9b30-a70e-a839-2950-75ecd7baf61f.png">
`/`において、`proc`ディレクトリは存在するので終了ステータスとして0を返しますが、`bin`ディレクトリはもう存在しないので1を返しています。

これを使って`ls`コマンドを作ります。`-d`はディレクトリが存在するかどうかを表すファイル属性演算子です。

```.bash
function ls 
{ 
for i in *
do
  if [ -d $i ]
  then 
    echo -ne "\e[34m$i\e[m\t" # ディレクトリは青色
  else
    echo -ne "$i\t" # それ以外は白色
  fi
done
echo # 改行のためのecho
}
```

`echo`で色をつけるのに[こちら](https://qiita.com/ko1nksm/items/095bdb8f0eca6d327233)の記事を参考にしました。また、ファイル名の間にはtab文字(`\t`)を入れています。この`ls`を適当なディレクトリで実際に使ってみましょう。
<img width="718" alt="スクリーンショット 2020-03-23 20.56.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/2bf63ea8-0be9-56a1-ce0a-a9fc99e7cafb.png">
普通の`ls`と違って改行が少し変ですが、今回はよしとしましょう。ターミナルの表示列数を表すシェル変数`COLUMNS`を使って場合分けすればもう少しきれいに出力できると思います。


### 2.2 catコマンド

これにはwhile文を使います。構文は以下です。

```
while < 条件式 >
do
  < 一連の文 >
done
```
`< 条件式 >`にはif文と同じように、コマンドの終了ステータスを用います。例えば以下です。

```.bash
a=0
while [ $a -ne 3 ]
do
  echo $a
  let a++
done
```
`-ne`は等しくない(not equal)を意味します。シェル変数`a`が3以外のときは`[ $a -ne 3 ]`は終了ステータス0を返すのでループし、`a`が3になると1を返すのでループから抜けます。組み込みコマンド`let`は算術演算子を評価して、その結果を変数に代入します。上の例では`a`をインクリメントするだけです。

実際にやってみると以下のようになります。
<img width="310" alt="スクリーンショット 2020-03-23 23.32.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/f52d07bf-6ee9-383e-2df7-0f43f82acf7b.png">
`a`が3になったときに、ちゃんとループから抜けてコマンドが終了していることがわかります。

それではwhile文を使って`cat`コマンドを実装します。簡単のため、引数を1つ取ってその中身を表示する機能のみを実装します。

```.bash
function cat 
{
while read val
do
  echo $val
done
} < $1
```

`read`コマンドは、シェル変数に値を代入するコマンドです。今回の例ではシェル変数`val`にファイルの中身を1行ずつ代入して、`echo`で表示します。`$1`で指定したファイルの中身が空になった時、`read`コマンドは終了ステータスとして1を返すので、ちゃんとループから抜けることができます。

### 2.3 補完について

また、`sudo rm -rf /*`するとtabでうまく補完できなくなることがあります。
<img width="508" alt="スクリーンショット 2020-03-23 18.01.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/5f2afc10-ca49-6c20-1d8b-22af4a40c397.png">
↓ tab入力
<img width="508" alt="スクリーンショット 2020-03-23 18.01.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/8b6a207b-08c9-0ef5-a63b-69bdc0d28452.png">
見たことないエラーが出ています。`cat`コマンドにおいてtabの補完がどうなっているかは、組み込みコマンド`complete`を使用することで見れます。
<img width="297" alt="スクリーンショット 2020-03-24 13.30.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/20e57ce6-bfd1-c6f2-1087-15385e44ef04.png">
これは`cat`コマンドの補完が`_longopt`という関数で決められているという意味です。この関数の詳細は`declare -f _longopt`で見れますが、ここでは深入りしないでおいておきます。要はこの関数が`sudo rm -rf /*`した影響で**うまく動かなくなっている**ということです。

今回はシンプルに**補完をファイル名で行う**ようにしましょう。具体的には普通のファイル名から補完する`-f`オプションを使って以下のように書きます。

```
complete -f cat
```

[前の記事](https://qiita.com/minnsou/items/60a306510f613a666fec)でも述べたとおり、ファイル名補完は`ESC+/`でもできるのですが、これでtabでもできるようになりました。ついでに`cd`コマンドもtabが使えるようにしておきましょう。`-d`オプションなら補完は**ディレクトリ名**から行われます。

```
complete -d cd
```

## 3 psコマンドの作成

それでは実際に`ps`コマンドを作っていきます。まずはプロセスの情報を含む`/proc`ディレクトリの説明から入ります。

### 3.1 /procディレクトリ
Linuxにおいては、プロセスの情報を含む`/proc`ディレクトリというものが存在します。これは通常のディレクトリとは異なる擬似的なディレクトリで、`sudo rm -rf /*`しても消えません。

実際に見てみると、こうなります。
<img width="718" alt="スクリーンショット 2020-03-23 20.57.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/60aeb753-a325-f702-0d2b-373b764d127c.png">
上の方の数字のみからなるディレクトリには、そのプロセスID(PID)に対応したプロセスの情報が入っています。

ちなみにこの`/proc`ディレクトリはLinuxでは存在しますが、純粋なUNIX(例えばBSD系のFreeBSDやMac OS、System V系のSolarisなど)では**存在しない**、もしくは**存在しても中身が異なる**ことがあるため注意です。

それではこのログインシェル(つまりbash)のプロセスIDのディレクトリを見てみましょう。現在のシェルのプロセスIDは`echo $$`で調べることができます。
<img width="718" alt="スクリーンショット 2020-03-23 21.00.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/a4c0e28f-f0c6-07db-50c1-5f3dc653e7af.png">
この中で`stat`というファイルがあり、これにそのプロセスに関する情報が記述されています。
<img width="718" alt="スクリーンショット 2020-03-23 21.01.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/cadd0eb9-741b-58bf-cf9e-3e391bc35595.png">
最初から見ていきましょう。

- 1つ目はプロセスIDで、値は`768`です。
- 2つ目がカッコで括られた実行形式のファイル名で、`(bash)`です。
- 3つ目はプロセスの状態で`R`です。`R`は実行中(Running)を表しています。

全部は紹介しきれないので、詳細は`man proc`で見てください。

これらの値を取得して、画面に表示するのが`ps`コマンドとなります。ただ`ps`コマンドのデフォルトでは表示するプロセスが少なすぎるので、`a`オプションをつけた`ps a`コマンド、つまり端末を持つ全てのプロセスを表示する`ps`コマンドを作りたいと思います。

まずはこの`stat`ファイルを引数にとり、シェル変数に代入する`read_stat`関数を作ります。

```.bash
function read_stat
{
read -a values
pid=${values[0]}
comm=${values[1]}
state=${values[2]}
tty_nr=${values[6]}
time=$(( ${values[13]} / 100 ))
} < $1
```

まずは`read`コマンドの`-a`オプションを使って、`values`という配列に`stat`の各値を入れていきます。シェル変数の解説は以下です。

- `pid` プロセスID
- `comm` 実行形式のファイル名
- `state` プロセスの状態
- `tty_nr` プロセスの接続している端末名
- `time` 実行時間（単位は秒）

実際に実行してみます。
<img width="356" alt="スクリーンショット 2020-03-23 21.29.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/d7deac8a-9e9e-3cae-595b-52c4ff9d3c58.png">
ちゃんと値が入っているのが確認できます。

これで基本的な情報は表示できるのですが、端末名はうまく表示できません。それは`tty_nr`に入っている数値(上の例では34816)を`tty1`や`pts/0`のような文字列に変換しないといけないためです。ここで**デバイスファイル**について少し説明しましょう。

### 3.2 デバイスファイル

UNIX系のOSでは、HDDやUSBメモリ、端末といったハードウェアもファイルとして扱うことができます。このようなファイルを**デバイスファイル**と言います。出力を捨てるための`/dev/null`やランダムな文字列を生成する`/dev/random`もデバイスファイルで、皆さんも使ったことがあるかもしれません。デバイスファイルには**ブロック型**と**文字型**の2種類があります。前者は**ディスク装置を操作する**ためのファイルで、後者はそれ以外を扱うためのファイルです。

デバイスファイルは**メジャー番号**と**マイナー番号**を使って管理されています。実際のLinuxのドキュメントで確認してみましょう。

https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/devices.txt

<img width="707" alt="スクリーンショット 2020-03-23 22.59.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/8ddaa5f2-1c59-0f57-9741-d3ac899dcc52.png">


例えば「ブロック型デバイスファイルのメジャー番号1」は「RAMディスク」に割り当てられています。マイナー番号ではRAMディスクの何番目かを表します。

`ps`コマンドで表示される端末は基本的にメジャー番号4の**TTYデバイス**か、メジャー番号136の**Unix98 PTY スレーブ**です。TTYデバイスは**ラズパイに直接繋がっている画面**、Unix98 PTY スレーブは擬似端末(pseudo terminal)の一種で、要は**SSHで接続した時の画面**です。

ではここで、`tty_nr`に戻りましょう。この数値列の意味は`man proc`に書いてあるので、引用してみます。
>(7) tty_nr  %d
> The controlling terminal of the process.  (The minor device number  is contained  in
>the combination of bits 31 to 20 and 7 to 0; the major device number is in bits 15 to 8.)

`tty_nr`の数値列の31から20ビットと7から0ビットの部分がマイナー番号、15ビットから8ビットの部分がメジャー番号であるということです。

これらを使って、シェル変数`tty`に端末名を表示する関数を作成します。

```.bash
function get_tty
{
major_num=$((tty_nr>>8))
minor_num=$((tty_nr&255))
if [ $major_num -eq 4 ]
then
  tty=tty${minor_num}
elif [ $major_num -eq 136 ]
then
  tty=pts/${minor_num}
else
  tty=???
fi
}
```

メジャー番号は8ビット左シフト(`tty_nr>>8`)を使うことで取り出し、マイナー番号は2進数で11111111である255との論理積(`tty_nr&255`)から取り出しています。本当は31から20ビットも使わないと正確ではないですが、今回はこれでも問題ないのでこの形にしています。

メジャー番号が4なら普通の端末（`tty1`とか）、136なら擬似端末（`pts/0`とか）、それ以外は不明(`???`)として、それにあったものをシェル変数`tty`に代入しています。

実際に使ってみましょう。
<img width="310" alt="スクリーンショット 2020-03-23 23.13.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/c9501419-68b7-a735-31de-3eb37cef3239.png">
この実験はmacからSSHで接続して行っているので、ちゃんと擬似端末が表示されました。

### 3.3 psコマンドの作成

ここまでに作った`read_stat`関数と`get_tty`関数を用いて、`ps`コマンドを作ります。

```.bash
function ps { 
echo -e "PID\tTTY\tSTATE\tTIME\tCMD"
for stat in /proc/[1-9]*/stat # 数字で始まりstatがあるディレクトリを考える
do 
  read_stat $stat
  if [ $tty_nr -ne 0 ] # 端末を持つプロセスのみ表示
  then
    get_tty # ttyを取得
    echo -e "${pid}\t${tty}\t${state}\t${time}\t${comm:1:-1}" # commの値の()は外す
  fi
done
}
```
ここでシェル変数`comm`は文字列がかっこ付きなので、それを取り外して出力(`${comm:1:-1}`)しています。
なお、古いbashでは-1が使えないこともあるので、その場合は同じ意味である`${comm:1:${#comm}-1}`を使うと良いと思います。

実際に使ってみましょう。
<img width="333" alt="スクリーンショット 2020-03-23 23.11.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395943/1967a1ca-c7c1-979f-8dd6-b3e9b7ed66fe.png">
ちゃんと出力されました。今回はラズパイに直接つなげているモニターでもbashが起動しており、そのPIDが614ということもわかりました。


## 終わりに

bashの組み込みコマンドだけで、意外となんでもできるってことが実感できたと思います。時間があったら、次はプロセスの管理（ジョブの概念や`kill`コマンド、シグナルなど）の話もやりたいと思います。