指定したユーザのホームディレクトリを作成し、グループも自動的に設定するスクリプトを書いてみる

# 経緯
題名の通りのスクリプトですが、書きました。
休日に、しかもGW中にやるような事ではないとは思うのですが、自身の勉強にもなるかなあと思ったので・・・。
しかしホント、一体何をしているんでしょうか。

最初は「超簡単に書けるだろ」と思っていたのですが、状況に応じてグループを変更する必要があったのでオプションを付けて、引数の指定がおかしくてもちゃんとした動作をするようにして、ついでにエラーメッセージも普通のコマンドみたく出力されるようにして、ついでに使用法とかバージョンも書いちまえ・・・とやっていたら結構時間が掛かってしまいました。
何しろちゃんとしたシェルスクリプトを書くのは初めてなもので・・・。
想定としては、LDAP連携をしているサーバに、特定のユーザがちゃんとログインできるようにホームディレクトリを作成する。といったものになっています。

色々なWebサイトさんにお世話になりました。
全部は書ききれないので、タメになった構文を記載しているWebサイト様のみ最後に参考として記載させていただきます。

では、とりあえずスクリプトを全部載っけます。
解説はその後に。

# 指定したユーザのホームディレクトリを作成し、グループも自動的に設定するスクリプト
```bash:createhomedir.sh
#!/bin/bash
# 引数のユーザのホームディレクトリを作成する

cmdname=createhomedir
ver=1.0

# 使用法
usage() {
    cat <<EOF
使用法: ${cmdname} [OPTION]... DIRECTORY...
Create the DIRECTORY and change GROUP script
指定したユーザのホームディレクトリを作成するスクリプトです。
オプションを指定しない場合、hogeグループで作成されます。

Options:
    -f, --foo      グループに"foo"を指定しディレクトリを作成する
    -b, --bar      グループに"bar"を指定しディレクトリを作成する
    -h, --help     使い方を表示して終了する
    -v, --version  バージョン情報の表示

Not Option:
    グループに"hoge"を指定しディレクトリを作成する
EOF

    exit 0
}

# バージョン情報
version() {
    echo -e "version. ${ver}\n2015/05/08 ine1127"
            exit 0
}

# "help参照" を意味するメッセージ
try_help() {
    echo "Try '${cmdname} --help' for more information."
            exit 1
}

# 引数不足時のエラー
error() {
    echo "${cmdname}: オプションには引数が必要です -- '$(echo \ ${param} | sed 's/^ -*//' )'"
            try_help
}

# 複数オプション指定時のエラー
multi_opt() {
    if [ ${a} -ge 2 ]; then
        echo "${cmdname}: オプションは複数指定できません"
        try_help
    fi
}

# オプション解析
i=0
a=0
g=hoge

for opt in "$@"
do
    param=${opt}

    case "$opt" in
        -h|--help)
            usage
        ;;

        -v|--version)
            version
        ;;

        -f|--foo)
            if [ ! -z "$2" ]; then
                ((++a))
                g=foo
                multi_opt
            else
                error
            fi
        ;;

        -b|--bar)
            if [ ! -z "$2" ]; then
                ((++a))
                g=bar
                multi_opt
            else
                error
            fi
        ;;

        -|--)
            echo "${cmdname}: オペランドがありません"
            try_help
        ;;

        --*)
            echo "${cmdname}: オプション '${param}' を認識できません"
            try_help
        ;;

        -*)
            echo "${cmdname}: 無効なオプション -- '$(echo ${param} | sed 's/^-*//' | sed 's/-*$//' )'"
            try_help
        ;;

        *)
            ((++i))
            if [ ${i} -ge 2 ]; then
                echo "${cmdname}: 引数は複数指定できません"
                try_help
            fi

            u=${opt}
            d=/home/${u}
        ;;

    esac

done

# ホームディレクトリをチェックし、ディレクトリがなければ作成する。
if [ ! -d ${d} ]; then
    sudo cp -iRp /etc/skel ${d}
    sudo chmod 700 ${d}

    sudo chown -R ${u}:${g} ${d} 2> /dev/null

    if [ $? -ne 0 ]; then
        echo "${cmdname}: ユーザが存在しません: 再度確認して下さい"
        sudo rm -rf ${d}

        exit 1

    else
        echo "ホームディレクトリ ${u} は グループ ${g} で作成されました。"
        sudo ls -ld ${d}
        sudo ls -la ${d}

    fi

else
    echo "${cmdname}: ホームディレクトリ \`${u}' を作成できません: ファイルが存在します"
    exit 1
fi

exit 0
```


# 解説
とりあえず頭からいきます。

## お決まり的なものと変数宣言
```bash:createhomedir.sh
#!/bin/bash
# 引数のユーザのホームディレクトリを作成する

cmdname=createhomedir
ver=1.0
```
最初は使うシェルの宣言ですね。
今回はbashで実行します。
その後は簡単なコメントですが、何をするスクリプトなのかって事ですね。
次に変数の宣言が来ていますが、これはコマンド名とバージョンの指定をしています。
自作なのでここら辺はまあ書いても書かなくてもどうでもいいでしょう(特にバージョンはどうでもいいですね)。
今回のスクリプトではこの後 `cmdname` をまぁまぁ使うので、早めに宣言しておきます。
<br />

## 関数宣言
```bash:createhomedir.sh
# 使用法
usage() {
    cat <<EOF
使用法: ${cmdname} [OPTION]... DIRECTORY...
Create the DIRECTORY and change GROUP script
指定したユーザのホームディレクトリを作成するスクリプトです。
オプションを指定しない場合、hogeグループで作成されます。

Options:
    -f, --foo      グループに"foo"を指定しディレクトリを作成する
    -b, --bar      グループに"bar"を指定しディレクトリを作成する
    -h, --help     使い方を表示して終了する
    -v, --version  バージョン情報の表示

Not Option:
    グループに"hoge"を指定しディレクトリを作成する
EOF

    exit 0
}
```
次にやっているのは、`usage` 関数の宣言です。
十数行書いたので、 `cat` コマンドで全行まとめて標準出力されるようにしています。
その後は `exit 0` で処理を終了しています。
処理に関わるものではないので書かなくてもいいのですが、書いておいたほうがより親切でしょう。
<br />

```bash:createhomedir.sh
# バージョン情報
version() {
    echo -e "version. ${ver}\n2015/05/08 ine1127"
}
```
次に `version` 関数を宣言しています。
これは本当に不要です。
`usage` 関数の中に入れてしまえばいいんですからね。
今回はなんとなく入れました。
<br /> 

```bash:createhomedir.sh
# "help参照" を意味するメッセージ
try_help() {
    echo "Try '${cmdname} --help' for more information."
            exit 1
}
```
次に `try_help` 関数を宣言しています。
これはエラー処理の時に「コマンドの詳細はhelpオプションを試してみてね」というメッセージを出力する為の関数です。
エラーメッセージ出力と一緒くたにしてしまおうとも思ったのですが、何度も書くのがなんだかなーという感じになってしまったので、関数にしてしまいました。
<br />

```bash:createhomedir.sh
# 引数不足時のエラー
error() {
    echo "${cmdname}: オプションには引数が必要です -- '$(echo \ ${param} | sed 's/^ -*//' )'"
            try_help
}
```
次に `error` 関数を宣言しています。
引数がない場合のエラーメッセージをまとめただけの関数です。
メッセージの後に `try_help` 関数を実行しています。
<br />

```bash:createhomedir.sh
# 複数オプション指定時のエラー
multi_opt() {
    if [ ${a} -ge 2 ]; then
        echo "${cmdname}: オプションは複数指定できません"
        try_help
    fi
}
```
次に `multi_opt` 関数を宣言しています。
これは(後から出てきますが)変数 `a` が2になった際に実行されます。
グループ指定のオプションを引数として2つ指定された時に、実行されるものです。
メッセージの後にはやはり `try_help` 関数を実行しています。

## オプション解析
一番しんどかったところです。
やたら長くなってしまったのですが、もう少し簡潔に出来ないものかなあとも思います。

```bash:createhomedir.sh
i=0
a=0
g=hoge

for opt in "$@"
do
    param=${opt}

    case "$opt" in
        -h|--help)
            usage
        ;;

        -v|--version)
            version
        ;;

        -f|--foo)
            if [ ! -z "$2" ]; then
                ((++a))
                g=foo
                multi_opt
            else
                error
            fi
        ;;

        -b|--bar)
            if [ ! -z "$2" ]; then
                ((++a))
                g=bar
                multi_opt
            else
                error
            fi
        ;;

        -|--)
            echo "${cmdname}: オペランドがありません"
            try_help
        ;;

        --*)
            echo "${cmdname}: オプション '${param}' を認識できません"
            try_help
        ;;

        -*)
            echo "${cmdname}: 無効なオプション -- '$(echo ${param} | sed 's/^-*//' | sed 's/-*$//' )'"
            try_help
        ;;

        *)
            ((++i))
            if [ ${i} -ge 2 ]; then
                echo "${cmdname}: 引数は複数指定できません"
                try_help
            fi

            u=${opt}
            d=/home/${u}
        ;;

    esac

done
```

まぁまぁ長いので、これも分割して解説していきます。
<br />

```bash:createhomedir.sh
i=0
a=0
g=hoge
```
まずは変数の宣言です。
変数 `i` 及び `a` は後々引数の制御で利用します。
変数 `g` はグループですが、基本的にはhogeグループを指定する予定なので、最初に宣言してしまいます。
オプションを指定すると、gの変数が書き換わるような形になります。
<br />

```bash:createhomedir.sh
for opt in "$@"
do
    param=${opt}
```
次に `for` 文でループが始まります。
`$@` は引数を最初から1つずつ読み込みます。
引数の分だけforを回します。
その読み込んだ引数を変数 `opt` に代入しています。
ループに入ると、まず現在の変数 `opt` の中身を変数 `param` に代入しています。
この変数 `param` はエラーメッセージ出力時に利用します。
<br />

```bash:createhomedir.sh
    case "$opt" in
        -h|--help)
            usage
        ;;

        -v|--version)
            version
        ;;
```
次に `case` 文が始まります。
これがオプションマッチングの本体です。
引数 `-h` , `--help` にマッチした場合 `usage` 関数を実行します。
引数 `-v` , `--version` にマッチした場合 `version` 関数を実行します。
<br />

```bash:createhomedir.sh
        -f|--foo)
            if [ ! -z "$2" ]; then
                ((++a))
                g=foo
                multi_opt
            else
                error
            fi
        ;;

        -b|--bar)
            if [ ! -z "$2" ]; then
                ((++a))
                g=bar
                multi_opt
            else
                error
            fi
        ;;
```
引数 `-f` , `--foo` にマッチした場合、まず `if` 文が実行されます。
`-z` は通常、文字列がなければ真を返します。
今回は `-z` の前に `!` を付けてnotにしています。
その為、この `if` 文は2つ目の引数 `$2` が存在すれば、真を返します。
真の場合、変数 `a` に1を足し、変数 `g`にfooを代入します。
その後 `multi_opt` 関数を実行していますが、これは変数 `a` が2になった際に実行されます。
グループ指定オプションでは変数 `a` に1を足していく処理をしている為、2度この `case` 文の処理が実行されると、変数 `a` は2になる為、 `multi_opt` 関数が実行されます。
理由はこのスクリプトの性質上、グループ指定オプションを2つ指定する必要性がない為です。

引数 `-b` , `--bar` にマッチした場合も同様の処理が実行されます。
<br />

```bash:createhomedir.sh
        -|--)
            echo "${cmdname}: オペランドがありません"
            try_help
        ;;

        --*)
            echo "${cmdname}: オプション '${param}' を認識できません"
            try_help
        ;;

        -*)
            echo "${cmdname}: 無効なオプション -- '$(echo ${param} | sed 's/^-*//' | sed 's/-*$//' )'"
            try_help
        ;;
```
引数が `-` , `--` だった場合、エラーメッセージを返します。
その後に `try_help` 関数を実行しています。

引数が `--*` だった場合もエラーメッセージを返し、 `try_help` 関数を実行します。
引数は `-*` だった場合も同様です。
<br />

```bash:createhomedir.sh
        *)
            ((++i))
            if [ ${i} -ge 2 ]; then
                echo "${cmdname}: 引数は複数指定できません"
                try_help
            fi

            u=${opt}
            d=/home/${u}
        ;;

    esac

done
```

上記に当てはまらなかった引数の場合は、ディレクトリ名として認識されます。
まず、最初に宣言した変数 `i` に +1 をします。
その後 `if` 文が開始されます。
ここでは変数 `i` が2以上ならば、エラーメッセージを返すようになっています。
理由は、このスクリプトの性質上、ディレクトリ名を2つ指定する必要がない為です。
その為、頭に `-(ハイフン)` がない通常文字列の引数が2つ以上あると、2度この条件式が実行され、変数 `i` の値が2になり、 `if` 文が真を返し、エラーメッセージが返されます。
通常ならばif文が無視され、現在変数 `opt` に代入されている文字列が、変数 `u` に代入されます。
その後ディレクトリパスの変数 `d` も設定されます。
これで `case` 文が終わり、引数がある場合は `for` 文が再度実行されます。
<br />

## 処理の本体
ここからが処理の本体です。
簡単なコマンドと多少の `if` 文の処理が入っています。

```bash:createhomedir.sh
# ホームディレクトリをチェックし、ディレクトリがなければ作成する。
if [ ! -d ${d} ]; then
    sudo cp -iRp /etc/skel ${d}
    sudo chmod 700 ${d}

    sudo chown -R ${u}:${g} ${d} 2> /dev/null

    if [ $? -ne 0 ]; then
        echo "${cmdname}: ユーザが存在しません: 再度確認して下さい"
        sudo rm -rf ${d}

        exit 1

    else
        echo "ホームディレクトリ ${u} は グループ ${g} で作成されました。"
        sudo ls -ld ${d}
        sudo ls -la ${d}

    fi

else
    echo "${cmdname}: ホームディレクトリ \`${u}' を作成できません: ファイルが存在します"

fi

exit 0
```
これも少し分割しましょう。

```bash:createhomedir.sh
# ホームディレクトリをチェックし、ディレクトリがなければ作成する。
if [ ! -d ${d} ];then
    sudo cp -iRp /etc/skel ${d}
    sudo chmod 700 ${d}

    sudo chown -R ${u}:${g} ${d} 2> /dev/null

```

まずは `if` 文の `-d` を利用し、ディレクトリが存在するか確認しています。
またも `!` でnotにしている為、ディレクトリがなければ `if` 文は真を返します。
真を返した場合、`/etc/skel` ディレクトリとその配下のファイルを、変数 `d` のフルパス名でコピーします。
次に変数 `d` のパーミッションを 700(drwx------) に変更します。
次に `chown` でオーナーとグループの変更をします。
しかし、ユーザ情報がない場合、ここでエラーが出てしまいます。
その場合、エラーメッセージでプロンプト画面が汚れるので、エラー出力は　`/dev/null` に渡しています。
<br />

```bash:createhomedir.sh
    if [ $? -ne 0 ]; then
        echo "${cmdname}: ユーザが存在しません: 再度確認して下さい"
        sudo rm -rf ${d}

        exit 1

    else
        echo "ホームディレクトリ ${u} は グループ ${g} で作成されました。"
        sudo ls -ld ${d}
        sudo ls -la ${d}

    fi
```
その後 `if` 文で返り値を判定します。
判定している返り値は `chown` のものです。
返り値が真の場合はエラーなので、エラーメッセージの出力と、作成したディレクトリとその配下のファイルを全て削除し、 `exit 1` で処理を終了します。
返り値が偽の場合、処理が上手くいった旨のメッセージを出力し、 `ls` コマンドで、作成したディレクトリと配下のファイルの詳細を出力します。
<br />

```bash:createhomedir.sh
else
    echo "${cmdname}: ホームディレクトリ \`${u}' を作成できません: ファイルが存在します"

    exit 1

fi

exit 0
```
この処理は、目当てのディレクトリが存在していた場合に実行されます。
エラーメッセージを出力して `exit 1` で処理を終了します。
処理が滞りなく完了すれば、 `exit 0` で処理を終了します。
<br />

# このスクリプトで出来る事出来ない事
このスクリプトは、オプションの順序は関係ありません。

```bash
$ ./createhomedir.sh --foo user
```
でも

```bash
$ ./createhomedir.sh user --foo
```
でも、どちらでも正常にスクリプトが実行されます。

また、出来ない事としては、オプションの複数指定です。

```bash
$ ./createhomedir.sh -v -h

$ ./createhomedir.sh --foo user -h
```
上記のような形だと、 `-v` か `-h` の、先に指定された方の結果を返します。
また、オプションの頭の `-(ハイフン)` を省略した形には出来ません。

```bash
$ ./createhomedir.sh -vh
```
上記の形ではエラーメッセージが返ってきてしまいます。
方法はあるようなのですが、今回は別段必要ないので、このままの形にしておきます。

# 最後に
結構大変ですね。
特にテストが・・・。
色々な方法で試して、変な処理にならない事を確認して、おかしな処理が起きたらデバッグして・・・と、まぁ時間が掛かりました。
でも書いてしまえばもうテンプレ化もできるので、やっぱり書いてよかったなあと思います。
あとはもう少しスマートな方法がないか・・・。
それはこれから書いていって発見したいと思います。

最後まで読んでいただきありがとうございました。

# 参考にさせていただいたWebサイト
http://doi-t.hatenablog.com/entry/2013/12/08/161929
http://qiita.com/b4b4r07/items/dcd6be0bb9c9185475bb
http://shellscript.sunone.me/parameter.html
http://merry.whitesnow.jp/SEMICMD/SECTION6/section6_7.html
http://homepage2.nifty.com/jr-kun/hidemaru_qa/4_regulr.html