【シェル芸人への道】メリーさんスクリプト作った

# **メリーさんスクリプト**

やってきました。第二回、シェル芸人への道。
今回はスクリプトの勉強を兼ねて「 **メリーさんスクリプト** 」なるものを作ってみました。

- **他ユーザへのメッセージの送り方**
- **オプションの使い方**
- **乱数 & デフォルト値の使い方**

が勉強できました。

ご存知かとは思いますが、メリーさんはこれです。

- **[メリーさんの電話](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%AA%E3%83%BC%E3%81%95%E3%82%93%E3%81%AE%E9%9B%BB%E8%A9%B1)**

それでは早速現物をご覧ください。

```
[root@sandbox1 ~]# bash --version
GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu)

[root@sandbox1 ~]# ./mery.sh

```

さて、なんの変哲もないこのスクリプト。
ログインしている他のユーザの画面ではこんなことが起こります。

```
[hoge@sandbox1 ~]$
あたし、メリーさん。今　あなたの /etc にいるの。

あたし、メリーさん。今　あなたの /opt にいるの。

あたし、メリーさん。今　あなたの /srv にいるの。

死ね

(セッションが切断される)
```

メリーさんがビープ音とともに何度か場所を告げたあと、セッションが切断されます。
各メッセージはランダムな間隔で出現しますが、次のメッセージが出るまではメリーさんがいると言ったディレクトリに mery というファイルができています。

```
[hoge@sandbox1 ~]# cat /etc/mery
ふふっ、 ls してたんだ。

(次のメッセージが出たあと)

[hoge@sandbox1 ~]# cat /etc/mery
cat: /root/mery: No such file or directory
```

というように、ユーザが以前行ったコマンドがメリーさんに知られています。
ただし、次のメッセージが出てしまうとこのファイルは忽然と消えています。

# **使い方**

メリーさんスクリプトは引数なしでも動きますが、いくつかオプションを持っています。
`-h` でオプション一覧が出ます。

```
[root@sandbox1 ~]# ./mery.sh -h
Usage: This script must be executed by root.
Usage: ./mery.sh [-i INTERVAL_BASE] [-t MESSAGE_TIMES] [-u TARGET_USERNAME] [-h]
```

後で説明しますが、このスクリプトは root での実行が必須です。
各オプションの効果は下記のようになっています。

|オプション|内容|指定例|
|:---:|:---|:---|
|-i|メッセージが表示される間隔の基準値を指定します。この値を指定すると、メリーさんのメッセージが **1 ～ INTERVAL_BASE 秒の範囲からランダムな間隔で表示** されるようになります。デフォルト値は15です。|-i 10|
|-s|最後のメッセージが **突然の死** になって、雰囲気が少しマイルドになります。|-|
|-t|メッセージが表示される回数を指定します。デフォルト値は3です。|-t 5|
|-u|メリーさんが遊びに行くユーザを指定します。指定したユーザが複数のセッションを持っている場合、メリーさんは全てのセッションに遊びに行きます。また、 **指定がない場合、メリーさんは実行者以外の全ユーザのところへ遊びに行きます** 。|-u hoge|
|-h|usageを表示します。|-|

突然の死オプションの他に、 **cowsay** オプションとかも付けたかったんですが、yum で入らなかったので諦めました。すみません。

```
[hoge@sandbox1 ~]$
あたし、メリーさん。今　あなたの /var にいるの。

あたし、メリーさん。今　あなたの /home にいるの。

あたし、メリーさん。今　あなたの /opt にいるの。

＿人人人人人人＿
＞　突然の死　＜
￣Y^Y^Y^Y^Y^Y^￣
```

# **mery.sh**

中身はこんな感じです。
エラー処理とかオプション解釈は適当です。

```bash:mery.sh
#!/bin/bash

# 実行ユーザチェック
if [ "$(whoami)" != "root" ] ; then
        echo "Exec user must be root."
        exit
fi

# ヘルプ用関数
usage() {
        echo "Usage: This script must be executed by root."
        echo "Usage: $0 [-i INTERVAL_BASE] [-t MESSAGE_TIMES] [-u TARGET_USERNAME] [-h]" 1>&2
        exit
}

# オプション取得
while getopts i:st:u:h OPT
do
        case ${OPT} in
                i)      INTERVAL_BASE=${OPTARG}
                        ;;
                s)      USE_ECHO_SD=1
                        ;;
                t)      MESSAGE_TIMES=${OPTARG}
                        ;;
                u)      TARGET_USERNAME=${OPTARG}
                        # 指定されたユーザの存在確認
                        if [ "$(cat /etc/passwd | grep -e "^${TARGET_USERNAME}:" | wc -l)" == "0" ] ; then
                                echo "User ${TARGET_USERNAME} is not exist."
                                exit
                        fi
                        ;;
                h)      usage
                        ;;
                \?)     usage
                        ;;
        esac
done

# 変数準備
DEFAULT_INTERVAL_BASE=15
DEFAULT_MESSAGE_TIMES=3

EXEC_TTY=$(tty | cut -c6-)
ROOT_DIRNAMES=$(ls -l / | grep -Ev "proc|sys" | grep -E "^d" | awk '{print $9}')
ROOT_DIRS=$(echo ${ROOT_DIRNAMES} | wc -w)
LOGIN_USERS=$(who | cut -d" " -f1)
TARGET_USERS=${TARGET_USERNAME:-${LOGIN_USERS}}

# 対象ユーザごとに処理を実行する
for TARGET_USER in ${TARGET_USERS}
do

        TARGET_TTYS=$(w ${TARGET_USER} | awk 'NR>2{print $2}' | grep -v "${EXEC_TTY}")

        # ユーザが複数セッションを持っていたらその全てを対象にする
        for TARGET_TTY in ${TARGET_TTYS}
        do
                # メッセージは指定回数送る
                for i in $(seq 1 ${MESSAGE_TIMES:-${DEFAULT_MESSAGE_TIMES}})
                do

                        MERY_DIR=$(echo ${ROOT_DIRNAMES} | awk -v column_number=$(( $(( ${RANDOM} % ${ROOT_DIRS} )) + 1 )) '{print $column_number}')
                        LAST_CMD=$(tail -n 1 $(cat /etc/passwd | grep -e "^${TARGET_USER}:" | cut -d: -f6)/.bash_history)

                        # meryファイルの配置
                        echo "ふふっ、 ${LAST_CMD} してたんだ。" > /${MERY_DIR}/mery
                        chmod 777 /${MERY_DIR}/mery

                        # ビープ音とメッセージの送信
                        echo -ne '\a' > /dev/${TARGET_TTY}
                        echo -e "\nあたし、メリーさん。今　あなたの /${MERY_DIR} にいるの。" > /dev/${TARGET_TTY}

                        # INTERVAL_BASEの範囲内でランダム秒待つ
                        sleep $(( $(( ${RANDOM} % ${INTERVAL_BASE:-${DEFAULT_INTERVAL_BASE}} )) + 1 ))

                        rm -f /${MERY_DIR}/mery
                done

                # オプションがあればecho-sdでメッセージを送る
                if [ "${USE_ECHO_SD}" == "1" ] ; then
                        echo > /dev/${TARGET_TTY}
                        echo-sd > /dev/${TARGET_TTY}
                else
                        echo -e "\n死ね" > /dev/${TARGET_TTY}
                fi
                sleep $(( $(( ${RANDOM} % ${INTERVAL_BASE:-${DEFAULT_INTERVAL_BASE}} )) + 1 ))

                # セッションのプロセスを調べてkillする
                TARGET_PID=$(ps ax | grep sshd | grep "${TARGET_TTY}" | cut -d" " -f1)
                kill -9 ${TARGET_PID}
        done
done
```

# **中身の話を少しだけ**

## **他ユーザへのメッセージの送り方**

このメリーさんスクリプトを作ろうと思った元々のアイデアは、この他ユーザへのメッセージの送り方を知ったからでした。

仕事でサーバ作業をしている際、借用の連絡を出しているにも関わらずサーバにログインして何かやっているユーザが数名いました。
それぞれのユーザ名は分かるので、メールアドレスと突き合わせるなりして連絡取ろうかと思っていたんですが、

「**wall** コマンドでメッセージ送ってみよう！」

というメンバがいました。
結局、wall コマンドでメッセージを送ったことにより、そそくさと件のユーザたちはログアウトしてくれたわけなんですが、そんなコマンドの存在を知らなかったので「**へー、面白い**」と思ってメリーさんネタを思いついたのでした。
調べてみたところ、この手のメッセージ送信は

- **write**
- **wall**
- **/dev/pts/***

などがあることがわかりましたが、**write, wall は日本語が送れない** ということがわかったので、今回は pts 経由でやることにしたのでした。
**/dev/pts* にはrootでないとアクセスできない** ため、このスクリプトはrootでしか動かないようになっています。

## **オプションの使い方**

シェルスクリプトは仕事でも書いていたりするのですが、オプションを使ったのは実は初めてでした。
普通に引数を書いて順番に取得していたので、特にオプション化する必要もなかったというわけです。

ただ、使ってみて思いましたが、便利ですねこれ。

## **乱数の使い方 & デフォルト値**

今回乱数を「メリーさんメッセージの出力間隔」に使っているわけなんですが、

`$(( ${RANDOM} % ${INTERVAL_BASE:-${DEFAULT_INTERVAL_BASE}} )) + 1`

というような形で取っています。
**`RANDOM`** が乱数を簡単に返してくれる特殊変数なのですが、桁が大きいので剰余を使って好きな範囲の乱数として使っています。
また、範囲の決定にはオプションで取る `INTERVAL_BASE` を使いますが、必須オプションとはしていないので、デフォルト値を取らせています。

**`${[変数名]:-[デフォルト値]}`**

これも意外と便利なものでした。コロンのあとに `-` がついているのがポイントで、 これが `+` になってくるとまた違う感じになります。

# **おわりに**

作ってる最中はとても楽しかったんですが、今思うと **何の役にも立たない** ですねこれ。
いえ、わかってました。

ではまた来週。