SCL テンプレート

2021/08/17 19:10 OS別::Linuxその他::シェル

シェルスクリプトテンプレート

シェルコマンド言語は、インタープリタの性質上、実行速度の面で不利です。
そのため、大規模なアプリとしての実装には向きません。

しかし、ログ出力といった最低限の機能は作るほうが、メンテナンス等で楽になります。
ここでは、シェルコマンド言語を利用する場合のテンプレートを公開しています。いずれもライセンスは 3条項BSDライセンス としていますので、自由に改変してください。
また、日本語を含むため UTF-8 で保存しています。日本語行を削除した場合は、エディタの実装によって ASCII や Shift JIS として処理されます。

テンプレート A (標準構成)

ひとつのファイルですべてを完結する、標準的なテンプレートです。

そのまま利用する場合は /var/log/script ディレクトリを事前に作成してください。
スクリプトの格納場所は指定しません。
利用時には Timing の Anytime か Cron どちらかを消してください。他にも不都合がある部分は修正してください。

ファイルは こちら からダウンロードできます。
template_a_20210807.zip

もしくは、下記をコピペしてください。文字コードを UTF8 にすることをお勧めします。
template_a.sh
#!/bin/bash
######################################################################
#
# Script name : template_a.sh
# Usage       : template_A.sh [-h] [-v]
# Update      : 2021.8.7   / Kazuya Jimba
#               yyyy.mm.dd / Name
# Timing      : Anytime
# Timing      : Cron (* * * * *)
#
#                 オリジナル配布元 http://ttm.jimba.ddo.jp/
#                 改変は自由ですが、改変者の責任に基づくものとします。
######################################################################

### Parameter and Bariable ###########################################
## Directory
pLogDir="/var/log/script"

## File
pLogFile="$(basename $0 .sh).log"

## Path
pLogPath="${pLogDir}/${pLogFile}"

## etc
pVersion="20210807.01"

## Numbers
nReportDay=0
nRV=0

## Flag

# Script log
# 0 - Script log
# 1 - Standard output
# 2 - Standard error output
# 3 - /var/log/messages
fLogOutput=0

### Function #########################################################
fnLog() {
  case ${fLogOutput} in
     0 ) echo "$(date '+%Y/%m/%d %H:%M:%S') $*" >> ${pLogFile};;
     1 ) echo "$*";;
     2 ) echo "$*" >&2;;
     3 ) /usr/bin/logger -t ${0##*/} "$*";;
  esac
}

fnErr() {
  echo "$*" >&2
  fnLog "Error: $*"
}

fnEnd() {
  case ${1} in
     0 ) fnLog "Normal end.";;
    11 ) fnErr "Read permission not exist (${2})";;
    12 ) fnErr "Write permission not exist (${2})";;
    13 ) fnErr "Exec permission not exist (${2})";;
    14 ) fnErr "Directory not exist (${2})";;
    15 ) fnErr "File not exist (${2})";;
     * ) fnErr "Unknown error (${2})";;
  esac
  exit ${1}
}

### Main #############################################################

# Environment check
fnLog "Script start (${0##*/})"
[ -d "${pLogDir}" ] || fnEnd 14 "${pLogDir}"
[ -f "${pLogPath}" ] || fnEnd 15 "${pLogPath}"

# Argument analyse
while getopts vh tOpt; do
  case ${tOpt} in
    h )
      fnLog "Option: Help message."
      echo "${0##*/} [-h] [-v]"
      echo "  [-v]              Version print."
      echo ""
      fnEnd 0
      ;;
    v )
      fnLog "Option: Version print."
      echo "Script version: ${pVersion}"
      echo ""
      fnEnd 0
      ;;
  esac
done

# xxxxx


### End ##############################################################
# report
nReportDay=$(( $(date -u +'%d' --date="@${SECONDS}") - 1 ))
fnLog "Exec Time: ${nReportDay} day $( date -u +'%H:%M:%S' --date=@${SECONDS} )"

# end
fnEnd ${nRV}

######################################################################

テンプレート B (Source あり)

source コマンド . で変数および関数を外部ファイルにした、中規模~大規模向けのテンプレートです。

デフォルトでは、スクリプトの格納場所を /usr/local/script で想定しています。
そのまま利用する場合は /usr/local/ ディレクトリにファイルを置いて、unzip コマンドで解凍してください。
ファイルは、次の構成になっています。
./script/
./script/template_b.sh
./script/conf/
./script/conf/common.cnf
./script/conf/template_b.cnf
./script/log/
./script/tmp/
展開後は、各ディレクトリ、各ファイルの権限を適切に変更してください。

利用時には Timing の Anytime か Cron どちらかを消してください。他にも不都合がある部分は修正してください。

ファイルは こちら からダウンロードできます。
template_b_20210808.zip

もしくは、下記をコピペしてください。文字コードは UTF8 をお勧めします。
template_b.sh
個別に作るスクリプトのテンプレートです。
コピーして使います。
#!/bin/bash
######################################################################
#
# Script name : template_b.sh
# Usage       : template_b.sh [-h] [-v]
# Update      : 2021.8.8   / Kazuya Jimba
#               yyyy.mm.dd / Name
# Timing      : Anytime
# Timing      : Cron (* * * * *)
#
#                 オリジナル配布元 http://ttm.jimba.ddo.jp/
#                 改変は自由ですが、改変者の責任に基づくものとします。
######################################################################

### Parameter and Bariable ###########################################
# common.cnf
if [ -f conf/common.cnf ];then
  . conf/common.cnf
else
  echo "Not exist conf/common.cnf"
  exit 1
fi

# template_b.cnf
[ -f conf/template_b.cnf ] || fnErrEnd 11 "conf/template_b.cnf"
. conf/template_b.cnf

### Main #############################################################

# Environment check
fnLog "Script start (${0##*/})"
[ -d "${pConfDir}" ] || fnErrEnd 14 "${pConfDir}"
[ -d "${pLogDir}" ]  || fnErrEnd 14 "${pLogDir}"
[ -d "${pTmpDir}" ]  || fnErrEnd 14 "${pTmpDir}"

# Argument analyse
while getopts vh tOpt; do
  case ${tOpt} in
    h )
      fnLog "Option: Help message."
      echo "${0##*/} [-h] [-v]"
      echo "  [-v]              Version print."
      echo ""
      fnEnd ${nRV}
      ;;
    v )
      fnLog "Option: Version print."
      echo "Script version: ${pVersion}"
      echo ""
      fnEnd ${nRV}
      ;;
  esac
done

# xxxxx


### End ##############################################################
# report
nReportDay=$(( $(date -u +'%d' --date="@${SECONDS}") - 1 ))
fnLog "Exec Time: ${nReportDay} day $( date -u +'%H:%M:%S' --date=@${SECONDS} )"

# end
fnEnd ${nRV}

######################################################################

common.cnf
スクリプト共通の設定ファイルです。
作成するスクリプトで追加したい共通機能があれば、こちらに追記します。

利用したい環境に応じて、各設定を変更してください。
######################################################################
#
# Script name : common.cnf
# Usage       : . ./common.sh
# Update      : 2021.8.7   / Kazuya Jimba
#               yyyy.mm.dd / Name
# Timing      : Anytime
#
#                 オリジナル配布元 http://ttm.jimba.ddo.jp/
#                 改変は自由ですが、改変者の責任に基づくものとします。
######################################################################

### Parameter and Bariable ###########################################
## Directory
pBaseDir="/usr/local/script"
pConfDir="${pBaseDir}/conf"
pLogDir="${pBaseDir}/log"
pTmpDir="${pBaseDir}/tmp"

## File
pLogFile="$(basename $0 .sh).log"

## Path
pLogPath="${pLogDir}/${pLogFile}"

## etc

## Numbers
nReportDay=0
nRV=0

## Flag

# Log Severity level
# NAME     Lv   Message content
# EMERG     0   System is unusable
# ALERT     1   Should be corrected immediately
# CRIT      2   Critical conditions
# ERR       3   Error conditions
# WARNING   4   May indicate that an error will occur if action is not taken.
# NOTICE    5   Events that are unusual, but not error conditions.
# INFO      6   Normal operational messages that require no action.
# DEBUG     7   Information useful to developers for debugging the application.
pLogNotifyLv=5

# Log output
# 0 - Script log
# 1 - Standard output
# 2 - Standard error output
# 3 - /var/log/messages
fLogOutput=0

# ログ管理 0=通知する
fEnLog=0
fEnLogEmerg=0
fEnLogAlert=0
fEnLogCrit=0
fEnLogErr=0
fEnLogWarn=0
fEnLogNotice=0
fEnLogInfo=0
fEnLogDebug=0

### Function #########################################################
fnLog() {
  case ${fLogOutput} in
     0 ) echo "$(date '+%Y/%m/%d %H:%M:%S') $*" >> ${pLogPath};;
     1 ) echo "$*";;
     2 ) echo "$*" >&2;;
     3 ) /usr/bin/logger -t ${0##*/} "$*";;
  esac
}

fnEmerg() {
  echo "$*" >&2
  [ "${fEnLogEmerg}" -eq "0" ] && fnLog "[Emergency] $*"
}

fnAlert() {
  if [ "${pLogNotifyLv}" -ge "1" ]; then
    echo "$*" >&2
    [ "${fEnLogAlert}" -eq "0" ] && fnLog "[Alert] $*"
  fi
}

fnCrit() {
  if [ "${pLogNotifyLv}" -ge "2" ]; then
    echo "$*" >&2
    [ "${fEnLogCrit}" -eq "0" ] && fnLog "[Critical] $*"
  fi
}

fnErr() {
  if [ "${pLogNotifyLv}" -ge "3" ]; then
    echo "$*" >&2
    [ "${fEnLogErr}" -eq "0" ] && fnLog "[Err] $*"
  fi
}

fnWarn() {
  if [ "${pLogNotifyLv}" -ge "4" ]; then
    [ "${fEnLogWarn}" -eq "0" ] && fnLog "[Warn] $*"
  fi
}

fnNotice() {
  if [ "${pLogNotifyLv}" -ge "5" ]; then
    [ "${fEnLogNotice}" -eq "0" ] && fnLog "[Notice] $*"
  fi
}

fnInfo() {
  if [ "${pLogNotifyLv}" -ge "6" ]; then
    [ "${fEnLogInfo}" -eq "0" ] && fnLog "[Info] $*"
  fi
}

fnDebug() {
  if [ "${pLogNotifyLv}" -ge "7" ]; then
    [ "${fEnLogDebug}" -eq "0" ] && fnLog "[Debug] $*"
  fi
}

fnParam() {
  fnDebug '[Param] $'"$1 = $2"
}

fnErrEnd() {
  # common.cnf を利用するスクリプト共通のエラー番号
  caseValue=$1

  case ${caseValue} in
     0) fnWarn "Normal number input...";;
     1) fnErr "Read Permission error ($2)";;
     2) fnErr "Write Permission error ($2)";;
     3) fnErr "Exec Permission error ($2)";;
    10) fnErr "Directory not exist error ($2)";;
    11) fnErr "File not exist error ($2)";;
     *) fnErr "Unknown error (${caseValue})";;
  esac
  exit ${caseValue}
}

######################################################################

template_b.sh
スクリプト個別の設定ファイルです。
必要の都度、コピーして使います。
######################################################################
#
# Script name : template.cnf
# Usage       : [template.sh] . conf/template.cnf
# Update      : 2021.8.7   / Kazuya Jimba
#               yyyy.mm.dd / Name
# Timing      : Anytime
#
#                 オリジナル配布元 http://ttm.jimba.ddo.jp/
#                 改変は自由ですが、改変者の責任に基づくものとします。
######################################################################

### Parameter and Bariable ###########################################
## Directory

## File

## Path

## etc
pVersion="20210807.01"

## Numbers

## Flag

# Log Severity level
# NAME     Lv   Message content
# EMERG     0   System is unusable
# ALERT     1   Should be corrected immediately
# CRIT      2   Critical conditions
# ERR       3   Error conditions
# WARNING   4   May indicate that an error will occur if action is not taken.
# NOTICE    5   Events that are unusual, but not error conditions.
# INFO      6   Normal operational messages that require no action.
# DEBUG     7   Information useful to developers for debugging the application.
pLogNotifyLv=5

# Log output
# 0 - Script log
# 1 - Standard output
# 2 - Standard error output
# 3 - /var/log/messages
fLogOutput=0

# ログ管理 0=通知する
fEnLog=0
fEnLogEmerg=0
fEnLogAlert=0
fEnLogCrit=0
fEnLogErr=0
fEnLogWarn=0
fEnLogNotice=0
fEnLogInfo=0
fEnLogDebug=0

### Function #########################################################
fnEnd() {
  # template.sh で利用するエラー番号
  case ${1} in
     0 ) fnLog "Normal end.";;
     * ) fnErr "Unknown error (${2})";;
  esac
  fnLog "Script end (${0##*/})"
  exit ${1}
}

######################################################################

コーディング規約案

2021/07/24 09:06 OS別::Linuxその他::シェル

シェルコマンド言語 コーディング規約案

いろいろな考え方があり、それぞれのプロジェクトで最適解が異なります。
規約のたたき台として、まずは作成しました。
参考の一助になれば幸いです。
ここでは、自分が使いやすい規約にしてみました。
臨機応変に変更してください。

Google Shell guide といったものも存在します。
https://google.github.io/styleguide/shellguide.html


プロジェクトにおけるシェルコマンド言語を利用したスクリプトは、視認性やメンテナンス性の向上を目的として、次の規約に則り作成すること。

汎用性の考慮

UNIX として共通の仕様である POSIX に規定された範囲でのコーディングとし、独自拡張は極力利用しないこと。

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html

また、必要があって利用する場合は、ヘッダに注意事項として記載すること。
[Tips]
たとえば、Linux の for 文で C 言語のような ( i=0 ; i < x ; i++ ) という形式を利用できます。これは便利なのですが bash の独自実装なので汎用性が下がります。

スクリプト

スクリプトは、次の要件を満たすこと。

ファイル名
ファイル名は、英数字 + 拡張子とする。
拡張子
拡張子は、シェルスクリプト単体で実行するものを .sh とする。
また、変数や関数を別途まとめたファイルについては .src とする。
[Tips]
csh / tcsh / fish / bash / dash といった、シェルを複数利用するようなプロジェクトでは、拡張子をそれぞれに合わせた方が良いと思います。
bash しか利用しないことが分かっている環境で、シバンにも bash 指定のみの場合は .sh でも不都合はないと思います。

.src は、内容として bash なのですが source コマンド (ビルトインでは . コマンド) で呼ぶことを意図しています。
実行権限
実行権限は、システムとして特に指定のない限り 610 とする。
[Tips]
権限を 755 や 700 で運用することは多いのですが、セキュリティを考慮すると、権限は最小限にすべきでしょう。
ここでは本番環境を想定していて、作成するユーザと、実行するユーザが異なることを考慮しています。
所有者は、更新できるが実行できない。前提条件を満たさない、不用意な実行を阻止できる。
グループ(実行者)は、更新できないが実行できる。JP1 のような自動実行ユーザのスクリプト改変を阻止できる。

ファイル内容

構成
スクリプトは、次の構成を基本とする。
段落内容備考
ヘッダシバンやスクリプトの目的などを記載する。
変数宣言スクリプトが利用する変数を宣言もしくは指定する。source (.) での指定も可
関数宣言スクリプトが利用する関数を宣言する。source (.) での指定も可
メイン処理スクリプトが目的とする処理を行う。
終了処理一時利用のために展開していたテンポラリファイルの削除などを行う。

文字コード
文字コードは UTF-8 とする。
また、BOM は付与しないこと。
日本語はコメントに限定する。
[Tips]
コメントに日本語を使わない場合は ASCII を指定してもよいです。
むしろ ASCII として認識されるでしょう。
改行コード
改行コードは LF とする。
[Tips]
Windows でスクリプトを書くと、改行コードは考慮から漏れがちです。
Linux や UNIX 上で書くと気にしなくても良いのですが、プロジェクトの進行状況や管理方法次第で、どうしても Windows 端末での開発を行わなければならない状況もあります。
致命的ではない文字コードやインデントも指定するのですから、致命的エラーとなる改行コードは指定しておきましょう。
インデント
インデントは、半角スペース2つとする。
[Tips]
タブ (半角スペース4つもしくは8つ) だと、横に広がりすぎて視認性が悪くなることがあります。
シバン
シバンは、以下の通り bash とする。
#!/bin/bash
[Tips]
シバンとヘッダは、ひとまとめにしてしまっても良いかもしれません。
もしくは、拡張子の指定でシバンも固定することになるでしょう。
ヘッダ
スクリプトの冒頭には、スクリプトの概要を記載する。
項目内容備考
スクリプト名Script nameファイル名称を記載する。
変更Modify変更日・変更者・変更内容を記録する。

以下は、定義する際のサンプル。
項目内容備考
スクリプト名Script nameファイル名称を記載する。
目的Purposeスクリプトの目的を記載する。
引数Option指定可能なオプションを記載する。
変更Modify作成日を記録する。日付と担当者をひとまとめにしてもよい。
作成者Auther作成者を記載する。
更新日Update更新日を記載する。更新内容も併記するとなおよい。
権限Permission実行者や権限を記載する。
サンプル 1
# Script name: example.sh
サンプル 2
# スクリプト名: example.sh
# 目的: スクリプトサンプル
コメント
適宜コメントを記載すること。
コメント内は日本語で記述する。

サンプル
# システム

パラメータ関係

変数名
変数名は、別途定めるプレフィックス/サフィックスを含め、キャメルケースで指定する。
一部の予約された変数名以外は、単語の組み合わせで指定すること。
[Tips]
私がスネークケースよりキャメルケースのほうが読みやすいと感じるタイプなので、キャメルケースを指定しています。
メリット・デメリットを考慮して、規定するのが良いです。

変数名はスクリプト上部にまとめて宣言しておくので、エディタの画面分割機能でコピペすることが前提としてあります。
画面分割は vim でも可能です。
変数名が短くても、何回も手入力していればタイポする可能性は高いです。
プレフィックス
プレフィックスは、変数の用途に応じて次の文字を使用する。
文字用途備考
p汎用パラメータParameter を意図
n数値用パラメータNumber を意図
f0 を真としたフラグ情報Flag を意図
tループで頻繁に書き換える使い捨てパラメータTemporary を意図
iループ用に予約Integer を意図 (while/until での数値ループに使用)
sループ用に予約String を意図(for での文字列ループに使用)
サフィックス
サフィックスは、特定の用途で次の文字列を使用する。
文字用途備考
Path絶対パスでファイル情報を指定する場合に利用
Dirディレクトリ情報の場合に利用文字列の最後は / で終わること。
Nameファイル情報の場合に利用ディレクトリは含めないこと。
Enableフラグ管理など、0 で有効とする場合に利用
サフィックスとしての Disable は、Enable と併用すると混乱を招くことがあるので禁止する。
利用時の指定方法
変数を利用する場合は、ブラケットで括ること。
Exit Status
終了ステータスは、スクリプトの目的が満たされることを正常とし、正常終了した場合に 0 とする。

関数関係

関数は、
関数名
関数名は、別途定めるプレフィックスを含め、キャメルケースで指定する。
一部の予約された関数名以外は、単語の組み合わせで指定すること。
プレフィックス
プレフィックスは、変数の用途に応じて次の文字を使用する。
文字用途備考
fn関数 (ファンクション) であることを示す
テンプレート関数
fnLog
特定のログファイルに、毎回リダイレクトを記述するのは可視性としても悪くなり、ミスも誘発しやすい。
引数としてメッセージをログファイルへ書き込む関数を用意した。

スクリプトの進捗状況を記述する目的で利用すること。

サンプル
fnLog "Sample message"
fnDebug
開発時に利用したデバッグ用メッセージを、メンテナンス時にも再利用できるよう、フラグに応じて出力指定もできる関数を用意した。

デバッグ情報を記述する目的で利用すること。

サンプル
fnDebug "Debug message"
fnErr
シェルスクリプトが標準エラー出力を行うための関数を用意した。

エラー出力を記述する目的で利用すること。

サンプル
fnErr "Standard Error message"
fnErrEnd
存在しなければ即時で終了するための関数を用意した。

引数として、エラー番号と、対象のファイルまたはディレクトリなどを指定すること。

サンプル
[ -d "/etc" ] || fnErrEnd 14 "/etc"
[ -f "/etc/hosts" ] || fnErrEnd 15 "/etc/hosts"
[ -x "/bin/ps" ] || fnErrEnd 13 "/bin/ps"

メイン

オプション解析
getopts を利用した解析を行う。
[Tips]
    • help といったロングオプションを使いたい場合は、getopts ではなく、引数そのものを case で解析するほうがよいと思います。

備考

テンプレート
シェルスクリプト
サンプル
#!/bin/bash
############################################################
# Script name: example.sh
# Modify     : yyyy.mm.dd 名前 初版作成
############################################################

### Parameter and Variable #################################
pCommon=./src/common.src
if [ -f "${pCommon}" ]; then
  . ${pCommon}
else
  echo "${pCommon} not exist."
  exit 1
fi

### Function ###############################################
fnEnd() {
  # スクリプト内で異常終了する際の Exit Status と、メッセージ内容の定義
  nExitStatus=$1
  case "${nExitStatus}" in
     0 ) fnLog "Script ${0##*/} end.";;
     * ) fnErr "Unknown error ($2)"; nExitStatus=1;;
  esac
  exit ${nExitStatus}
}

### Main ###################################################
fnLog "Script ${0##*/} start."

## オプション解析
while getopts h OPT; do
  case ${OPT} in
    h )
      fnDebug "Option h used."
      echo "Usage ${0##*/} [-h]"
      echo " [-h]    help messages."
      fnEnd 0
      ;;
    * )
      fnDebug "Unknown option used."
      fnErrEnd 10 "${OPT}"
      ;;
  esac
done

# xxx 処理


### End ####################################################
fnEnd 0

############################################################
外部ファイル
サンプル
# 共通ファイル (common.src)

### Parameter and Variable #################################

## ディレクトリ
pLogDir="/var/log/script/"

## ファイル
pLogName=$(basename ${0%.*}.log)

## パス
# ログファイル
pLogPath="${pLogDir}${pLogName}"

# ヌルクリア
i=""
s=""

# Exit Status
nExitStatus=0

## フラグ
fDebugEnable=0

### Function ###############################################

# 目的:ログ出力
fnLog() {
  echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >> ${pLogPath}
}

# 目的:デバッグ出力
fnLog() {
  [ "${fDebugEnable}" -eq "0" ] && fnLog "[Debug] $*"
}

# 目的:エラー出力
fnErr() {
  # ログ出力
  fnLog "$*"
  # 標準エラー出力
  echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >&2
}

# 目的: エラー終了
fnErrEnd() {
  nExitStatus=${1}

  # 引数確認
  if [ -z "${nExitStatus}" ]; then
    fnErr "Not argument."
    exit 1
  fi

  case "${nExitStatus}" in
     0 ) fnErr "Function (fnErrEnd) argument error."; nExitStatus=1;;
    10 ) fnErr "Argument error ($2)";;
    11 ) fnErr "Read permission not exist ($2)";;
    12 ) fnErr "Write permission not exist ($2)";;
    13 ) fnErr "Exec permission not exist ($2)";;
    14 ) fnErr "Directory not exist ($2)";;
    15 ) fnErr "File not exist ($2)";;
     * ) fnErr "Unknown error ($2)";;
  esac
  exit ${nExitStatus}
}
||

実行中スクリプトの編集

実行中スクリプトの編集



まず、スクリプトの実行についておさらいしておきます。

スクリプトは、テキストを読んで命令文を逐次解釈し、記述された順番でコマンドを実行するものです。
これはコマンドインタープリタの標準的な挙動です。

そのため、シェルスクリプトも Windows バッチファイルも、実行中にファイルを書き換えたり、消したりするとエラー終了します。

検証方法

検証には、次のようなスクリプトを書くとよいでしょう。
#!/bin/bash
echo "Start"
sleep 30
echo "End"
sleep で待ち時間が発生している間に、End を書き換えたり、追記したり、ファイルを消したりすればよいわけです。

実際にやってみれば、書き換わった後に追記したコードが実行されたり、エラーが出たりと、予想できない挙動を示すでしょう。
そういった理由から誤って実行中にバージョンアップしてしまったり、自分自身を書き換えてしまうバグを作りこんでしまったときには、非常に困るわけです。

不意の修正に強くするためには

では、対策を考えてみます。

そもそもの仕様としてファイルへの読み込みが発生するので、実行中スクリプトの修正に書き換えを行わせないための策は、根本的にありません。

ですが、少しでも強くする方法はあります。
(1) 適切な権限を設定する
「書き込み権限があるので書き込めるのだ」という発想で、書き込み権限を消しておく方法です。

$ sudo chmod -w sample.sh

頻繁に修正の発生しない、もしくは発生しては困る本番環境では、考慮されているものと思われます。
デメリットとしては、修正を行う時は毎回権限を付与する、もしくは sudo を利用して書き換えるといった手間や権限の乱用に繋がる点でしょう。
(2) 書換を検知する
スクリプト自体に、自身の書き換えを検知させる方法としては、ls や file、stat といったコマンドで自身の更新時刻を管理させます。

頻繁に実行する内容になるでしょうから、関数として呼び出すのが自然でしょう。

Linux bash では、関数にすると環境変数としてセットされるようです。
何が環境変数としてセットされているかは set コマンドで確認できます。

環境変数はメモリに展開されるので、ファイル編集の影響をうけません。
ということは、関数はファイル編集だけならば影響をうけません。

環境変数を書き換える行為(関数の再定義)が必要です。

つまり、スクリプト始めにファイルの更新時刻を環境変数として持っておき、以後は適宜差分をみる方法がとれます。

差分自体は test コマンドで比較すれば、そう難しいロジックを組むことなく実装できるでしょう。


かなり荒々しいですが、次のようなサンプルでも差分検出としては要件を満たせるでしょう。

#!/bin/bash
before="$(ls -l $0)"

functionDiff() {
  after="$(ls -l $0)"
  [ "${before}" == "${after}" ] || echo "NG"
}

functionDiff
また、md5sum といった、ファイルの破損や編集を検知するためのチェックサム関係コマンドを利用するのも良いです。

(3) メモリに読み込む 1
関数はメモリに置かれた内容が実行されるので、影響を受けにくいことは前段で記述しました。
ならば、出来るだけ関数にしてしまえば影響を極力排除できるだろう、という発想です。

関数などは別ファイルに記述できるので、究極、制御系(判断分岐や関数実行)しかないスクリプトになります。
関数名に気を遣えば抽象的な構文になるので、かなり読みやすいスクリプトになると思います。

外部ファイル /tmp/example.conf

### example.conf

# Parameter or Variable
example="EXAMPLE"

# Function
exFunction() {
  echo "${example}"
}

実行ファイル /tmp/example.sh

#!/bin/bash
### Script name: example.sh
# Include
. ./example.conf

# Main
exFunction

(4) メモリに読み込む 2
スクリプトに記載されている内容を、一度メモリに読み込んでおくという手法も取れます。

実体としては、一度、実行したいスクリプトを変数に読み込んで、echo してあげて1行ずつ解釈していく方法になるでしょう。


実行したいスクリプト /tmp/example.sh
#!/bin/bash
# script name : example.sh
echo a
echo b
echo c

注意事項として bash の echo では、変数を展開するときダブルクォートがあるかどうかで、出力形式が変わります。
具体的には、次の通りです。

envbar=$(cat /tmp/example.sh)
$ echo ${envbar}
#!/bin/bash # script name : example.sh echo a echo b echo c
$
$
$ echo "${envbar}"
#!/bin/bash
# script name : example.sh
echo a
echo b
echo c

余談ですが、変数名のブラケット と は無くても大丈夫です。付けておくと、変数展開がしやすいのでお勧めです。
なお、シングルクォートでは変数展開が行われません。

サンプルとしては、次の通りです。

example.sh を実行するコマンド

envbar=$(cat /tmp/example.sh)
$ echo "${envbar}" | bash -
a
b
c

まとめ

実行中の書き換えに対する予防策は、アクセス権限だったり検知機構だったり、ファイル内容の取得だったりといろいろ示しましたが、意図して起こした場合には無力です。

複数の防御手段を併用するのが、一番強力です。

感想

変数への読み込みを行うメリットは、メモリに展開される関係で処理速度が段違いに早くなることも考慮できるでしょう。
特に、HDD といった比較的遅い媒体からの読み出しとオンメモリでは、比較になりません。

変数は int (32bit) で定義されていることが多いようなので 4GB までは対応していそう*1です。

変数への取り込みは、スクリプト内で変数へ読み込んでおいて、1行ずつデータを解析していくにも良さそう。

技術の引き出しが多いことに越したことはないので、他にも方法があれば教えていただきたいです。

*1 : メモリに展開する以上、メモリの最大容量にも依存するものと思われる。

シェルコマンド言語


シェルコマンド言語の雑な説明

このページでは、シェルコマンド言語の使い方、ひいてはシェルスクリプトを記述するための前提となる知識を、雑に紹介するところから始めます。
一応、このツイートと関連しますw

シェル

ある程度馴染みがあるであろう Linux で説明します。

Linux は Linus Torvalds さんがメインとなって開発されている中心的プログラムである Kernel(カーネル) で動きます。
Kernel というのは、たとえばファイルの扱い方だったり、ネットワーク通信の仕方だったりを制御してくれます。

指示すれば制御してくれるのですが、思った通りにはやってくれませんので Kernel そのままでは、人が扱うのに不向きです。
そこで、何らかの形で Kernel に仕事をさせるプログラムが必要になります。

それが、キーボードから 特定の文字列(コマンド) を入力して Enter を押すと Kernel にアクセスすることができる、シェルと呼ばれるプログラムです。
Linux Kernel を包み込む 貝殻(シェル) のようなイメージをすると、良いかもしれません。

つまり Linux におけるシェルとは、コマンドラインインタープリタ(指定した文字列を適宜解釈して実行するプログラム)のことです。
bourne shell とか c shell とかいった、いくつかの種類が存在しています。

このシェルには環境変数と呼ばれる変数があり、暗黙的/明示的に様々な形で利用されます。

なお、シェル自体は Linux が参考にした UNIX で実装されている CUI でもあります。
UNIX の歴史とともにあるプログラムですが、ここでは割愛します。

コマンド

コマンドは、シェル自体にビルトインされているものもありますが、基本的には単一の目的のために作られたプログラムになります。
たとえばシェルで ls というコマンドを実行すると、カレントディレクトリ配下にあるファイルが表示されますが、これは /bin/ls というファイルを実行した結果です。

ls コマンド


type というコマンドは、どのパスで実行しているか以外にハッシュされているかどうかを表示してくれます。
ハッシュされている(位置を記憶している)と、環境変数 $PATH を検索せずにコマンドが実行されることを意味しています。

which というコマンドは、パスのどこに存在するのか教えてくれます。

インタプリタ内部では $PATH に定義されたパスの順に、コマンドと同じファイル名のプログラムを探し、最初に合致したファイルを起動します。
この PATH に . が含まれている場合、カレントディレクトリのファイルも実行対象として検索されます。

コマンド用のプログラムは、目的に沿って格納するディレクトリが分けられています。
そのディレクトリは環境変数 PATH に記述され、コマンド実行時に随時*1参照されて呼び出すプログラムが決定されます。

*1 : 場所を予め覚えているコマンドがあるといった一部例外はありますが

シェルコマンド言語

ここでは、シェルコマンド言語についてあれこれ記載します。
UNIX のシェル (CUI) で使うコマンドの仕様のことです。

シェルスクリプト

日本における IT 業界では、シェルコマンド言語で書かれたスクリプトをシェルスクリプトと呼んでいます。
むしろ、シェルコマンド言語というと「コマンドは言語じゃないだろ」と言う人もいるかもしれません。

なんでこんな書き方をしてるかというと POSIX が Shell Command Language と書いているからです。

プログラム言語は、基本的に英語圏で開発されたものなので、英語をもとにしています。
言語の和訳は、つぎのように行われています。

programming language → プログラミング言語
C Language → C 言語

なら Shell Command Language は シェルコマンド言語でしょう。
シェルコマンド言語で書いたスクリプトは、シェルスクリプトで良いと思います。

POSIX

では、POSIX というのは何か。

現在の POSIX とは UNIX を名乗る OS に共通する仕様を決めたものです。

正式名称を Portable Operating System Interface for UNIX *2 と言います。
2020 年8月現在、UNIX という商標をもち POSIX という仕様を策定している The Open Group という業界団体が仕様を公開しています。

https://publications.opengroup.org/

なお、規格としては IEEE になっているので IEEE Std 1003.1-2017 という表記になっています。

UNIX と名乗る OS は、POSIX 仕様に準拠したうえで、ライセンス料を支払う必要があります。

ちなみに Linux は POSIX に準拠していますが、ライセンス料は支払っていないので UNIX ではない、という立ち位置です。
また、Linux は Linus Torvalds が商標を持っています。

最新は 2018 Edition ですね。
何年か毎に更新されるので、参考までに。

シェルコマンド言語

前置きが少し長くなりましたが、シェルコマンド言語仕様へのアクセスは次の URL になります。

https://pubs.opengroup.org/onlinepubs/9699919799/
main
3ペインの左上から Shell & Utilities をクリックします。
すると左下ペインに、対応するメニューが表示されます。
Shell and Utilities
メニュー内容
1. Introductionどんなコマンドとユーティリティを提供するのか、その前置き
2. Shell Command Languageシェルコマンド言語の定義
3. Batch Environment Servicesバッチジョブへ提供する機能の定義
4. UtilitiesUNIX が提供する機能やコマンドの説明

*2 : なお for UNIX の部分は後付けと聞いています。

シェルスクリプト

シェルスクリプトは、シェルコマンド言語で書かれたスクリプトファイルです。

シェルスクリプトというと長いので、省略してシェルと呼ばれることもあります。
文脈で判断してください。

シェルスクリプトの書き方

シェルスクリプトはテキストなので、テキストエディタで記述します。
ファイルの形式
基本的には、コマンドを書いてファイルに保存し、bash に引数として読み込ませれば実行されます。

ファイル単体で実行するためには、最低限、以下の書き方をする必要があります。
shebang
ファイルの1行目に、つぎの文字列を書きます。
#!/bin/bash
これを shebang (シバン等と読む)と言い、UNIX の処理系ではインタープリタ(実行プログラム)を指定するものとして利用されています。
#!/bin/perl
であれば perl スクリプトになります。
bash を指定するので、bash スクリプトです。

sh や ksh 等ありますが、現在の POSIX に準拠したシェルは bash くらいなので、ここでは bash を利用します。
文字コード
文字コードは、基本的に ASCII であれば問題なく動きます。

UTF-8 を指定する場合でも、BOM (Byte Order Mark) やマルチバイト文字がなければ、実質上は ASCII と同じになるので問題なく動きます。

BOM を指定すると、スクリプトとしてはファイルの先頭に余計なバイナリが差し込まれることになります。
Kernel 側で実行を制御できなくなり、エラーで返るので注意してください。

また、マルチバイト文字も一部のコードが別な制御コードと混同されて、エラーを返すことがあります。
コメントであっても、できれば使わないに越したことはありません。

その点を認識して、注意してコメントを書くという運用を行うのであれば、問題にはならないと思います。
改行コード
文字コードには関係なく (ASCII でも UTF-8 でも EUC でも)、改行コードは LF である必要があります。

シェルスクリプトとして CR+LF を利用した場合は、実行エラーが発生します。

通常、実行するシステムでコーディングを行うことが多く、滅多に問題にはなりません。
外注した場合に改行コードの指定をうっかり忘れると、ほとんどの場合で Windows 端末によるコーディングが行われます。
動かないコードが納品される原因の上位に来るので、細かい点ですが注意しましょう。

FTP での ASCII 送信で、スクリプト単体を送信すれば改行コードが変換されて問題ないのですが、Zip にまとめて送るなどで取り切れないこともあります。
記述方法
テキストファイルなので、テキストエディタで記述します。

Linux なら vi / vim や nano, emacs, vscode 等で書けます。
Windows ではないので拡張子は何でも大丈夫、なんなら拡張子が無くても大丈夫ですが、慣例的に .sh や .bsh 等が使われています。

以下、最も簡単なサンプルとして xxxxx.sh を記述します。
#!/bin/bash
echo 'Hello World!'

実行方法

シェルスクリプトは、実行権限を与えて、コマンドとして利用します。

実行権限は、次のコマンドで与えることができます。
$ chmod +x xxxxx.sh
権限が付与できたら、次のように実行します。
$ ./xxxxx.sh
Hello World!
$ 
以下、実際に実行したところです。
サンプル スクリプト(実行権限あり)


また、実行権限を付与しなくても、シェルスクリプトとして実行する方法はあります。
サンプル スクリプト(実行権限なし)


これは、bash にスクリプトファイルを引数として読み込ませて、実行させているためです。

サンプルでは ./ を付けるものと付けないものがあり、なぜ分けているのか疑問に思われたでしょうか?

Linux / UNIX では、カレントディレクトリを示すのに . が利用されます。
カレントディレクトリのファイルを示すためには、ファイル名と区別する意味で / による階層構造を示す必要があります。

現在のディストリビューションでは $PATH にカレントディレクトリを示す . が含まれていません。
そのためカレントディレクトリにあるスクリプトは ./ がないとコマンドとして認識されません。

サンプル スクリプト(PATH指定なし)


これは、カレントディレクトリに ls 等のよく使うコマンド名でシェルスクリプトをホームディレクトリに配置されてしまうと、悪意あるコードを実行させることが容易になるからです。

シェルスクリプトの入門としては、以上のことに気を付ければ良いと思います。