シェルスクリプトでオブジェクト指向? -mydns-ip-update_v2.06-

オブジェクト指向化(独自解釈&独自実装)

v2.06の使い方

GitHub - smileygames/mydns-ip-update at v2.06

MyDNS.JPにipを自動でupdateするシェルスクリプト. Contribute to smileygames/mydns-ip-update development by creating an account on GitHub.

ダウンロードはこちら

Release mydns-ip-update_v2.06 · smileygames/mydns-ip-update · GitHub

MyDNS IDが無い場合、エラー終了とする #68

 

オブジェクト指向の解説

10何年か前にプログラマーをしていた時にC言語からC++への移行作業があったのですが、
その際にオブジェクト指向化してくれと言われて、色々調べて結局つかめなかったオブジェクト指向に
もう一度チャレンジしてみることに。

現在ではRuby言語とか使ったほうが早いかもしれませんが、
そういったものは取っかかりづらいので、
今回たまたま使ったシェルスクリプトで作ってみようと思いました。

シェルスクリプトはなかなかいいですね。

  • 日本語の情報がたくさんある。
  • C言語っぽく書ける。
  • 引数の使い方が楽。
  • Linuxマシンで標準で動く。
  • 変数を型やプライベート・パブリックをあまり意識せず扱える。

…とまぁ思い立ったんですが、
オブジェクト指向の情報がたくさん増えた現在でもやはり調べてみたらよくわからない。
といったところで下記参考をみて、
言っていることは他と同じようなことなんですが、とても分かりやすかった。

オブジェクト指向の参考
「オブジェクト指向」とは?プログラミングに必要不可欠な要素を解説! | ロボ団ブログ (robo-done.com)

そして、やはりオブジェクト指向が現在でも理解しづらいという事も判ったので、
もうこれは自分なりの解釈で、
オブジェクト指向のメリットを享受できるように、スクリプトを書いてみることにした。
そもそも、小さいプロジェクトだと意味はないだろうけど、
オブジェクト指向の話は急にスケールがでかくなるから分かりづらいので
まずは小さいもので作って、運用してみようと。

そして、全てをクラス化(もどき)して積み木のように作ってやろうかと!
(Ruby言語はすべてクラスで書かれると見つけて、思いついた)
ちゃんとしたやり方(知らない)ではなく、分かりやすさ重視で!
そもそも、オブジェクト指向って、プログラムを人間に分かりやすくするためのものだろうに。

 

クラス

こいつがまず理解ができない。
だって概念なんだもん。。。

というわけで、独自解釈としてはクラスは一つの機能とすることにした(それはオブジェクトか?)。
そして実態はソースファイルとする。
つまり1つのソースファイル=1つのクラスにする。

というのもシェルスクリプトはソースファイルを関数のように使うことができる。
引数まで使えるので今回この方法を思い立った。

本当はC言語でもこのやり方を思いついたんだけど。
まぁ実装方法が思いつかなかったし、
ソースファイルを全部やり直すことになりそうだったので、無理だった。

こうすることでソースファイル名に意味が生まれるし、可読性もそこまで落ちなくなる気がする。
 

メソッド

そして、メソッドについてはもう関数でいいことにする。
というのもシェルスクリプトは関数は定義するだけのものなので都合がよかったから
実行する処理は関数ではないので。

それと検証したわけではないけど、別のソースファイルなら、
同じ名前の関数が使えるかもしれないという期待もある。

 

インスタンス

これは上記で述べた、実際にスクリプトファイルの中で実行する処理をインスタンスとすることにした。

 

継承

理解するのが難しいが、独自解釈としては、
コール元とコール先(もしくはさらにその先)の間だけ変数や関数をパブリックとして扱えるという認識。

これをシェルスクリプトでやろうとすると継承が必要なもの=>同プロセス
継承が必要ないもの=>別プロセスとすることで対応できるのかな?

つまり、同一プロセスなら、スクリプトファイルを読み込んだコール元が、
コール先の変数を直接使えるというもの。
(シェルスクリプトの場合は順番に気を使う必要がありそうだが)

ちなみに、同一プロセスで実行したい場合は、

. ./ip_update.sh  # 同一プロセスで実行 パブリックとして扱える?
./ip_update.sh    # 別プロセスで実行 プライベートとして扱える?

となる。別プロセスになると変数や定義した関数は扱えないはず。

今回一つだけ継承を使っている。
これは、ループの中なので多少の最適化を考えて、
インクルードファイルをいちいちループのたびに読まなくて済むため。
まぁ説明のために継承もどきを使ってみたかったというだけ。

現在では継承はあまり使わずに、合成を使うようになっているらしいですが。
そんな機能はシェルスクリプトにはないのでね。
分かりやすい継承として使ったつもり。 

 

ポリモーフィズム

クラスに多様性を持たせるものという解釈。
呼び出すクラス(機能)は一緒なのに、行う処理が微妙に異なるようにした。

 

カプセル化

これについてはそもそも呼び出し元と別ファイルになるので、隠蔽できているんじゃないかという解釈

 

プロパティ(属性?)

そもそも、今回のスクリプトはデータをほぼ扱わないので
とりあえず、属性=引数として扱った。

 

こんな解釈で実際に実装してみた。

 

オブジェクト指向化前のスクリプト

こちらを参考

mydns-ip-update_v1.13

今度こそ安定版!のはず。。。Release mydns-ip-update_v1.13 · smileygames/mydns-ip-update (github.com) ▼使い方はこちらsmileygames/myd […]

オブジェクト指向化した後にみると、
やっぱりシンプルだよなぁ。
ただメンテナンスを考えると、どうなるかってところ。

例えば、mydns-ip-update.shの処理は修正したけど、
mydns-ip-change.shのほうの似たような処理の修正を忘れていたとか。
(よくやるやつ)

オブジェクト指向化した後も、やっていることはほぼ同じです。
まぁ上記の状態でもオブジェクト指向は意識はしていたんですけどね。

 

シェルスクリプトの解説

シェルスクリプト構成図

ただのメモなのでUMLとかは無視しています。
言わばクラスフローチャート図のようなものです。

 

ソースファイル

説明の為、元のソースファイルに少しだけ変更を加えています。
処理は全く変わりません。

コンフィグファイル(config/user.conf)
#!/bin/bash
# マルチドメインの場合、例のように [ ] の数字をそろえて追加登録してください
# 例はコメントアウトされているので、先頭の # を外してID等を変更して使用してください

# MyDNS

MYDNS_ID[1]="mydnsxxxx1"
MYDNS_PASS[1]="Password1"
MYDNS_DOMAIN[1]="example.com"

# MyDNS Login URL
MYDNS_IPV4_URL="https://ipv4.mydns.jp/login.html"
MYDNS_IPV6_URL="https://ipv6.mydns.jp/login.html"


# IPV4 アドレス default = on
# IPV4_DDNS 動的IPアドレス mydns-ip-change.serviceが無効の場合は動作しない
# (default) IPV4_DDNS = on
IPV4=on
IPV4_DDNS=on

# IPV6 アドレス default = off
# IPV6_DDNS 動的IPアドレス mydns-ip-change.serviceが無効の場合は動作しない
# default = on
IPV6=off
IPV6_DDNS=on

#   s:	秒(seconds)
#   m:	分(minutes)
#   h:	時間(hours)
#   d:	日(days)
# UPDate Timer 6h (default) 
UPDATE_TIME=6h

# DDNS UPDate Timer 3m (default) 
DDNS_TIME=3m

ちなみに、大文字のみの変数=パブリック変数のつもり
大文字小文字が混ざった変数=プライベート変数のつもり
で、コーディングしています。
シェルスクリプトではすべてパブリック変数の為(同一プロセスであれば)
明示的にしておかないと訳分からなくなりそうだったので。
 

ip-update timerかip-check timerを起動する(ip_update.sh)

では実際にソースコードで解説してみる。

ip-update.sh(クラス名)
まぁメイン処理なのでこの名前にしているだけで、
実際はtimer_select.shという名前のほうがあっているんだけど、
まぁ最初の処理というのを明示的にしたかっただけ。

#!/bin/bash

# include file
File_dir="/usr/local/mydns-ip-update/"
source "${File_dir}config/default.conf"
User_File="${File_dir}config/user.conf"
if [ -e ${User_File} ]; then
    source "${User_File}"
fi

# 関数(メソッド)
timer_select() {
    if [[ ${!MYDNS_ID[@]} != "" ]]; then
        if [ "$IPV4" = on ] || [ "$IPV6" = on ]; then
            ./ddns_timer.sh "update" &
        fi
        if [  "$IPV4" = on ] && [ "$IPV4_DDNS" = on ]; then
            ./ddns_timer.sh "check" &

        elif [ "$IPV6" = on ] && [ "$IPV6_DDNS" = on ]; then
            ./ddns_timer.sh "check" &
        fi
    fi
}

# 実行スクリプト(インスタンス)
timer_select
# ip_update.shから実行されるプロセスを監視する処理
while true;do
    wait -n
    End_code=$?
    if [ $End_code != 0 ]; then
        ./err_message.sh "process" ${FUNCNAME[0]} "endcode=$End_code  プロセスのどれかが異常終了した為、強制終了しました。"
        exit 1
    fi
done

コンフィグに書かれた条件により、2種類のタイマーを起動する処理。
以前は2つのサービスで実装していたが、1つのサービスで実行できるよう新たに作ったクラス。
まぁクラスというより、ただのメイン処理だが。

2種類のタイマーはバックグラウンドで実行される。
バックグラウンドプロセスが実行されている間は、waitコマンドでプロセスを監視している。

 

タイマー及びupdate処理かcheck処理か判断(ddns_timer.sh)

#!/bin/bash

# include file
File_dir="/usr/local/mydns-ip-update/"
source "${File_dir}config/default.conf"
User_File="${File_dir}config/user.conf"
if [ -e ${User_File} ]; then
    source "${User_File}"
fi

Mode=$1

# [. ]をファイル名の最初につけることで同一プロセスで実行している(継承のつもり)
ip_update() {
    if [ "$IPV4" = on ]; then
        . ./ddns_timer/multi_domain.sh "update" "$MYDNS_IPV4_URL"
    fi
    if [ "$IPV6" = on ]; then
        . ./ddns_timer/multi_domain.sh "update" "$MYDNS_IPV6_URL"
    fi
}

ip_check() {
    if [ "$IPV4" = on ] && [ "$IPV4_DDNS" = on ]; then
        . ./ddns_timer/multi_domain.sh "check" "$MYDNS_IPV4_URL" "4" "A" 
    fi
    if [ "$IPV6" = on ] && [ "$IPV6_DDNS" = on ]; then
        . ./ddns_timer/multi_domain.sh "check" "$MYDNS_IPV6_URL" "6" "AAAA"
    fi
}

# 実行スクリプト(インスタンス)
# 引数Modeによってポリフォーリズムを実装しているつもり
case ${Mode} in
   "update")
        sleep 5m;ip_update
        while true;do
            sleep $UPDATE_TIME;ip_update
        done
        ;;
   "check") 
        while true;do
            sleep $DDNS_TIME;ip_check
        done
        ;;
    * )
        echo "[${Mode}] <- 引数エラーです"
    ;; 
esac

見てもらえれば分かる通り関数は似たような処理しかない。
このソースファイルはIPv4かIPv6か、
それとも、IPv4の動的アドレスかIPv6の動的アドレスかしか判断しない処理。

第1引数(ポリモーフィズム用のプロパティ)によってそれぞれの関数をコールするよう分けている。
この関数はこれ以上共通化できないもので作っている。

でインスタンスでは無限ループ処理が入っている。
というのは、関数を2回呼びたいため、こうなった。
本来であればこのコール元でループ処理をしたいが、
このソースファイルがプロセスの一番上の処理の為、こうしたほうが無難と判断している。
この辺の柔軟性をインスタンスには持たせるのかな?と解釈しながら作った。

でないと、インスタンスって意味なくね?ってなるので。

 

マルチドメインアクセス処理(ddns_timer/multi_domain.sh)
#!/bin/bash

Mode=$1
Login_URL=$2
IP_Version=$3
DNS_Record=$4

multi_domain_ip_update() {
    for i in ${!MYDNS_ID[@]}; do
        if [[ ${MYDNS_ID[$i]} = "" ]] || [[ ${MYDNS_PASS[$i]} = "" ]]; then
            ./err_message.sh "no_value" ${FUNCNAME[0]} "MYDNS_ID[$i] or MYDNS_PASS[$i]"
            continue
        fi
        ./dns_access.sh "mydns" $i "${MYDNS_ID[$i]}:${MYDNS_PASS[$i]} $Login_URL"
    done
}

multi_domain_mydns_check() {
    for i in ${!MYDNS_ID[@]}; do
        if [[ ${MYDNS_ID[$i]} = "" ]] || [[ ${MYDNS_PASS[$i]} = "" ]] || [[ ${MYDNS_DOMAIN[$i]} = "" ]]; then
            ./err_message.sh "no_value" ${FUNCNAME[0]} "MYDNS_ID[$i] or MYDNS_PASS[$i] or MYDNS_DOMAIN[$i]"
            continue
        fi 
        IP_old=$(dig "${MYDNS_DOMAIN[i]}" $DNS_Record +short)

        if [[ $IP_New != $IP_old ]]; then
            ./dns_access.sh "mydns" $i "${MYDNS_ID[$i]}:${MYDNS_PASS[$i]} $Login_URL"
        fi
    done
}

# 実行スクリプト
case ${Mode} in
   "update")
        multi_domain_ip_update
        ;;
   "check") 
        IP_New=$(curl -s ifconfig.io -"$IP_Version")
        if [[ $IP_New = "" ]]; then
            ./err_message.sh "no_value" ${FUNCNAME[0]} "自分のIPアドレスを取得できなかった"
            return 1
        fi
        multi_domain_mydns_check
        ;;
    * )
        echo "[${Mode}] <- 引数エラーです"
    ;; 
esac

この処理はちょっと複雑に見えるが、実はエラーチェック処理を外すと、かなりシンプルな処理になる。
実際に外してみよう。

#!/bin/bash
#
# ./ddns_timer/multi_domain.sh
#
# MyDNS

Mode=$1
Login_URL=$2
IP_Version=$3
DNS_Record=$4

multi_domain_ip_update() {
    for i in ${!MYDNS_ID[@]}; do
        ./dns_access.sh "mydns" $i "${MYDNS_ID[$i]}:${MYDNS_PASS[$i]} $Login_URL"
    done
}

multi_domain_mydns_check() {
    for i in ${!MYDNS_ID[@]}; do
        IP_old=$(dig "${MYDNS_DOMAIN[i]}" $DNS_Record +short)

        if [[ $IP_New != $IP_old ]]; then
            ./dns_access.sh "mydns" $i "${MYDNS_ID[$i]}:${MYDNS_PASS[$i]} $Login_URL"
        fi
    done
}

# 実行スクリプト
case ${Mode} in
   "update")
        multi_domain_ip_update
        ;;
   "check") 
        IP_New=$(curl -s ifconfig.io -"$IP_Version")
        multi_domain_mydns_check
        ;;
    * )
        echo "[${Mode}] <- 引数エラーです"
    ;; 
esac

こうすると何をしているのかまる分かりだし、もし、IPチェック処理に不具合があればそれだけ修正。
ループ文にバグがあれば、もう一つも同じ修正が必要な可能性があり、
メンテナンス性が上がると解釈。

後、このクラスは実はサブディレクトリに配置している。
継承のつもりなのだが、誰の子クラスなのか明示的にするための苦肉の策。

まぁ現在の小さいスクリプトだと全く意味はないんですがね。。。
実際に運用してみないと、どうなるのかも未知数だけども。

 

MyDNSへのアドレス通知処理(dns_access.sh)
#!/bin/bash

Mode=$1
Array_Num=$2
Access_URL=$3

mydns_accsse() {
    Out_Time=25s
    Max_Time=21

    timeout ${Out_Time} curl --max-time ${Max_Time} -sSu $Access_URL
    if [ $? != 0 ]; then 
        ./err_message.sh "timeout" ${FUNCNAME[0]} "${Out_Time}: ログイン情報 MYDNS_ID[$Array_Num]:MYDNS_PASS[$Array_Num]"
    fi
}

# 実行スクリプト
case ${Mode} in
   "mydns")
        mydns_accsse
        ;;
    * )
        echo "[${Mode}] <- 引数エラーです"
    ;; 
esac

これは現在、完全に共通化して1つしか関数がないんだけども、
別なDDNSサービスでも対応出来るようにしたいとなったときに、
追加しやすいように一応ケース文を入れている、(ポリモーフィズム)

 

エラーメッセージ処理(err_message.sh)

これは、ログにエラーメッセージを書き込む処理ですね。
別に「logger」コマンドを使うだけでいいんですけどね。
デバッグの時に便利なので実装しました。

#!/bin/bash

Mode=$1
Caller=$2
Message=$3

timeout_err_message() {
    Error_Message="${Caller}: Failed Timeout: ${Message}"
    logger -ip authpriv.err -t "${Caller}" "${Error_Message}"
}

no_value_err_message() {
    Error_Message="${Caller}: no value: ${Message}"
    logger -ip authpriv.err -t "${Caller}" "${Error_Message}"
}

process_err_message() {
    Error_Message="${Caller}: abend error : ${Message}"
    logger -ip daemon.err -t "${Caller}" "${Error_Message}"
}

# 実行スクリプト
case ${Mode} in
   "timeout")
        timeout_err_message
        ;;
   "no_value") 
        no_value_err_message
        ;;
   "process")
        process_err_message
        ;;
    * )
        echo "[${Mode}] <- 引数エラーです"
    ;; 
esac

終わりに

実際に実装してみることで、少し理解できた気がしますが、
これはあくまで独自解釈なので、他のオブジェクト指向言語には生かせないでしょう。

しかも、正しいわけでもないですからね。
ただ、自分的にはかなり分かりやすいので、これをベースに使っていこうと思います。

まぁ使う予定は今のとこ、このスクリプトだけですけどね。( ´艸`)
その内、別なDDNSサービス向けの機能でも追加するかもしれませんし、
しないかもしれません。

予定は未定です。が、
まぁメンテナンス性は上がったと信じたいです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です