繰り返し文字生成の実行時間

2024/08/31 19:08 OS別::Linuxその他::シェル
bash で Separator のような、任意の文字を繰り返し出力する方法を探して、見つけたこちら。
https://genzouw.com/entry/2020/10/29/211143/2094/

選択肢が割とあって「お、おう……」となったので「実行速度」で選ぶことにした記事。
シェルスクリプトなので、普段は実行時間なんて気にしないのだけど。

ここで結論を先に書いておくと、ビルトインコマンドである for 文(for *1; do echo -n '='; done)を利用するのが最強のようです。
次点で、print (printf "=%0.s" 0..9)でした。

*1 : i=0; i<10; i++

テスト環境

テスト環境は、以下のような状況です。
$ uname -moi
armv7l unknown GNU/Linux
$
$ bash -version | head -n 1
GNU bash, バージョン 5.1.4(1)-release (arm-unknown-linux-gnueabihf)
$
$ . /etc/os-release
$ echo ${PRETTY_NAME}
Raspbian GNU/Linux 11 (bullseye)

テスト方法

スクリプトにしておいて、シンプルに time で計測します。

作成したスクリプトおよび内容は、以下の通りです。
$ ls -1
test-A.sh
test-B.sh
test-C.sh
test-D.sh
test-E.sh
test-F.sh
test-G.sh
work.sh
$
$ for i in *.sh
do
  echo "### ${i}"
  echo "----------------------------------------------------"
  cat ${i}
  echo "----------------------------------------------------"
  echo ""
done
### test-A.sh
----------------------------------------------------
#!/bin/bash
printf "=%0.s" {0..9}
----------------------------------------------------

### test-B.sh
----------------------------------------------------
#!/bin/bash
yes = | head -n 10 | tr -d '\n'
----------------------------------------------------

### test-C.sh
----------------------------------------------------
#!/bin/bash
seq -s '=' 0 10 | tr -d '[0-9]'
----------------------------------------------------

### test-D.sh
----------------------------------------------------
#!/bin/bash
seq -s '=' 0 10 | sed "s/[0-9]//g"
----------------------------------------------------

### test-E.sh
----------------------------------------------------
#!/bin/bash
for ((i=0; i<10; i++)); do echo -n '='; done
----------------------------------------------------

### test-F.sh
----------------------------------------------------
#!/bin/bash
seq 10 | xargs -i echo -n =
----------------------------------------------------

### test-G.sh
----------------------------------------------------
#!/bin/bash
seq 10 | xargs -i printf "="
----------------------------------------------------

### work.sh
----------------------------------------------------
#!/bin/bash

fnChecker() {
  echo "### ${1}"
  time ./${1}
}

fnChecker test-A.sh
fnChecker test-B.sh
fnChecker test-C.sh
fnChecker test-D.sh
fnChecker test-E.sh
fnChecker test-F.sh
fnChecker test-G.sh
----------------------------------------------------

$

計測

傾向が見えればよいと考えたため、とりあえず1回実行したところが、以下の数値です。
$ ./work.sh
### test-A.sh
==========
real    0m0.016s
user    0m0.013s
sys     0m0.003s
### test-B.sh
==========
real    0m0.028s
user    0m0.020s
sys     0m0.030s
### test-C.sh
==========

real    0m0.023s
user    0m0.024s
sys     0m0.007s
### test-D.sh
==========

real    0m0.028s
user    0m0.009s
sys     0m0.028s
### test-E.sh
==========
real    0m0.014s
user    0m0.001s
sys     0m0.013s
### test-F.sh
==========
real    0m0.082s
user    0m0.026s
sys     0m0.067s
### test-G.sh
==========
real    0m0.080s
user    0m0.036s
sys     0m0.051s
$

結論

for コマンドを使います。

感想

printf コマンド(test-A.sh)もなかなか良い数字なんだけど、ビルトインコマンドの for によるループ(test-E.sh)が強いですね。

その他、普通は bash の予約語なので利用されない time コマンドを使ってみての計測なんかもしてみましたが、メモリ使用率なんかは変化が少ないようです。

wait

time コマンドで計測できる wait の内容は、次の通り(man bash の引用)。
wait の回数、つまりそのプログラムが自発的にコンテキストスイッチされた回数。
例えば、I/O 操作の完了を待っている間などが該当する。
$ type time
time はシェルの予約語です
$
$ which time
/usr/bin/time
$
$ for i in test-*.sh
> do
>    /usr/bin/time -f "\nwait: %w" ./${i}
> done
==========
wait: 1
==========
wait: 8
==========

wait: 7
==========

wait: 5
==========
wait: 1
==========
wait: 31
==========
wait: 36
$
計測した結果は上記の通りですが、printf と for でのループ、待ちなんてほとんど発生してませんね。

その他

あと、こんな感じで他にもいろいろ計測できたりします。

これは、メモリ(プロセス生存中のそのプロセスの resident set size の最大値)。
$ for i in test-*.sh
do
   echo "### ${i}"
   /usr/bin/time -f "\n Memory: %MKB" ./${i}
done
### test-A.sh
==========
 Memory: 2752KB
### test-B.sh
==========
 Memory: 2820KB
### test-C.sh
==========

 Memory: 2820KB
### test-D.sh
==========

 Memory: 2816KB
### test-E.sh
==========
 Memory: 2692KB
### test-F.sh
==========
 Memory: 2868KB
### test-G.sh
==========
 Memory: 2868KB
$
うん。だいたい 3MB 程度ですね。

メジャーページフォルトの回数

外部コマンドでは、メジャーページフォルトの回数を計測も出来たりします。
$ for i in test-*.sh
do
   echo "### ${i}"
   /usr/bin/time -f "\n Page: %F" ./${i}
done
有意な差がないので結果は載せないですが、test-C.sh (seq -s '=' 0 10 | tr -d '[0-9]') はちょこちょこページフォルトが発生している模様。
他のも、合ったりなかったり。
$ for i in test-*.sh
do
   echo "### ${i}"
   /usr/bin/time -f "\n Memory: %MKB" ./${i}
done

pwsh update

2024/03/27 06:43 OS別::Windowsその他::シェル
Windows PowerShell を Windows Terminal でアップデートする手順を記載します。

環境

この手順は、以下の Windows バージョンで確認しています。
OS Version
OS Version

バージョンアップ

Windows Terminal の起動

筆者の環境では PowerShell を起動すると、Windows Terminal が開きます。
Windows Terminal
Windows Terminal

バージョン確認

PowerShell のバージョンを確認しておきます。
PS C:\Users\test> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      22621  2506


PS C:\Users\test>

インストール

次のコマンドで、Microsoft Store から PowerShell の最新版を確認し、インストールします。

なお、winget コマンドは Microsoft の パッケージ管理ツール になります。
PS> winget search Microsoft.PowerShell
PS> winget install --id Microsoft.Powershell --source winget
PS> winget install --id Microsoft.Powershell.Preview --source winget
もしユーザアカウント制御 (UAC) が表示された場合は、「はい」を選択してください。
PowerShell 7.4 install
PowerShell 7.4 をインストール中の画面です。
PowerShell 7.4 install
PowerShell 7.4 install
PowerShell 7.5 install
PowerShell 7.5 Preview をインストール中の画面です。
PowerShell 7.5 install
PowerShell 7.5 install

インストール完了画面

どちらもインストールを完了すると、下の画面のようになります。
PowerShell インストール完了
PowerShell インストール完了

Windows Terminal 設定

インストールしただけでは Windows Terminal に反映されないようです。
Windows Terminal を再起動するか、新しいプロファイルとして設定します。

なお、インストールした PowerShell は、別なディレクトリに格納されています。
C:\Program Files\PowerShell\7\pwsh.exe
新しいプロファイルとして設定する場合は、参考にしてみてください。
そのまま設定すると、自身のプロファイルを設定・保存した際に新しい PowerShell が定義されたので、Windows Terminal を再起動すれば設定されているのかもしれません(確認不能状態になってしまいました)

Windows Terminal 設定を開く

必要であれば Windows Terminal 設定画面を開きます。
Windows Terminal 設定
Windows Terminal 設定

新しいプロファイルを開く

「新しいプロファイルを開く」を選択します。
新しいプロファイルを追加します
新しいプロファイルを追加します

新しい空のプロファイルを開く

「新しい空のプロファイルを開く」を選択します。
新しい空のプロファイル
新しい空のプロファイル

プロファイルを設定する

必要事項を入力して、「保存」を選択します。
プロファイル設定
プロファイル設定

プロファイル設定後

筆者の環境では、設定してプロファイルを保存すると、自動的に 7.4 および 7.5 が適切に設定されました。
せっかく設定しましたが、自分で設定したプロファイルは「プロファイルの削除」を選択して削除しました。
PowerShell 7.4 プロファイル設定後
PowerShell 7.4 プロファイル設定後
PowerShell 7.4 プロファイル設定後
PowerShell 7.5 プロファイル設定後
PowerShell 7.5 プロファイル設定後
PowerShell 7.5 プロファイル設定後

PowerShell 7.4

新しく開く

新しく開く
新しく開く

バージョン確認

PowerShell 7.4 バージョン確認
PowerShell 7.4 バージョン確認

参考

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 : メモリに展開する以上、メモリの最大容量にも依存するものと思われる。