f

2017-07-30

How to name a commands group in POSIX shell script

シェルスクリプトで複数コマンド群に名前を付けてグループ化して可読性を向上させる方法について検討した。

命名グループ化の検討の概要
  • コマンドのグループ化には変数,関数,aliasの3種類の方法が存在。
  • aliasは関数と複合文での利用に注意が必要で,過去のPOSIXでオプションのため利用は非推奨
  • 実行速度の計測結果から,命名グループ化には変数(単独)と関数(波括弧)の利用を推奨

なお,今回の記事で利用したコード類は以下に格納している。

POSIXism/daily/2017/20170730_macro at master · lamsh/POSIXism

Introduction

シェルスクリプトでは,パイプを使うことで複数のコマンドを繋いで処理できる。しかし,パイプをつないで記述されたコードは,全体として何をやっているのか意味がわかりにくくなる。また,複数のオプションを組み合わせたコマンドの記述や,自分がよく知らないコマンドが使われた場合,どういう処理をやっているのかわかりにくくなる。

処理内容をわかりやすくするために,複数の処理を変数や関数にまとめ,適切な名前を与える方法がある。例として,以下にpwdコマンドが存在するかどうかを判定するコードを掲載する。

command -v pwd >/dev/null && echo found || echo not found

このコードでは,command -vの処理内容を理解していないと,見ただけでは意味がわからない。しかし,このコードを以下のように関数化して,人がわかるような名前を与えることで,command -vの意味を知らなくても全体の処理内容を理解しやすくなる。

is_exe_enabled(){
command -v "$1" /dev/null
}

is_exe_enabled pwd && echo found || echo not found

また,関数にまとめるだけでなく,変数にまとめることでも可読性の向上を実現できる。例えば,以下のコードはbashのバージョンが4.3以上であればbindコマンドを実行している。

if bash --version | grep -q -e ' 4\.[3-9]*' -e ' [5-9]\.'"; then
 bind 'TAB: menu-complete'
fi

このコードは以下のように,IS_BASH_VER_GE_43変数に一度代入すれば,どのバージョンを対象としているのかがよりわかりやすくなる。

IS_BASH_VER_GE_43="eval bash --version | grep -q -e ' 4\.[3-9]*' -e ' [5-9]\.'"
if $IS_BASH_VER_GE_43; then
 bind 'TAB: menu-complete'
fi

このように,関数や変数などシェルスクリプトには複数の処理に対して別の名前を付ける方法がいくつかある。ここで疑問が生じる。わかりやすくなったのはいいが,それで実行速度が遅くなっていないだろうか?複数の実現方法があるのならば,最も簡単で最も速度の速い方法を採用したい。

そこで,シェルスクリプトで処理をグループ化して可読性をあげるための方法を紹介し,実行速度を計測することで,どの方法がベストであるかを検討した。

なお,コードの可読性の向上に関しては,コメントを書くことで補うこともできる。しかし,コメントで説明した場合,実際のコードとコメントの内容を合わせて読む必要があり,読み手に負担がかかる。コメントを書く前に,コメントが不要になるようにコード自体をわかりやすくすべきだろう。

Method

処理のグループ化には以下の3種類の方法がある。

  1. Variable(変数)
  2. Function(関数)
  3. alias
Variable(変数)

1点目は変数に処理を代入する方法だ。以下のように処理内容を(マクロ)変数に代入し,変数展開により代入された内容を実行する。

## Simple command (without eval)
EXE=pwd
$EXE

## Compound command (with eval)
IS_BASH_VER_GE_43="eval bash --version | grep -q -e ' 4\.[3-9]*' -e ' [5-9]\.'" if $IS_BASH_VER_GE_43; then bind 'TAB: menu-complete' fi

シンプルで記述量が少ないが,いくつか注意点がある。

変数の注意点
  1. 予約語(for, while, case, until, (), {}, &&,||,;,&など)利用時は先頭にevalが必要。
  2. evalを使わなければ複数コマンドの実行はできない。
  3. 引数は末尾にしか使えない。
  4. evalを使わなければ,変数展開は一度しか起きないため,再帰処理は不可能。

変数に代入して展開する場合,通常のコマンドラインの解釈が異なり,予約語が通常の文字列とみなされてしまう。これを回避する場合,先頭にevalを記述することで残りの部分をevalで解釈してやる。

evalを先頭に使う場合,本来よりもコマンドを余計に1個実行するため,速度が遅くなる懸念がある。

>

使い方としては,条件判定結果を格納して,条件判定に使うのが良い例だろう。

## 現在のシェルのオプションとしてset -eが有効になっているかを判定
IS_ENABLED_SET_E=$( case "$-" in (*e*) echo true;; (*) echo false;; esac ) if $IS_ENABLE_SET_E; then
echo "set -e is enabled"
fi

条件判定に変数を使う場合,判定結果に応じて0と1の整数を代入して,testコマンドで[ $VAR = 1 ]のように記述する方法もある。しかし,個人的にはこの方法は好ましくないと考えている。理由は以下2点だ。

  1. 人によって条件判定値とその意味が異なる(0と1のどちらが真か不明)。
  2. testコマンドによる記述が冗長になる。

変数に直接truefalseを代入して使ったほうが,意図がわかりやすく,また共に組み込みコマンドとしてPOSIXで定義されているので(速度も)安定している。

Function(関数)

2点目は関数を使う方法だ。以下のように複数のコマンド群をグループ化できる。

is_exe_enabled(){
command -v ${1+"$@"} >/dev/null
}

is_opt_enabled()( EXE="$1"; OPT="$2" "$EXE" --help 2>&1 | grep -q -- "$OPT"'[[:blank:],=[]' )

関数を記述する場合,本体部分に波括弧{}か丸括弧()を使うことができ,それぞれで挙動が異なる。

関数定義時の括弧の違い
括弧の種類説明
波括弧{}現在シェルで実行する。関数内で定義した変数の値,関数定義,set,exportなどの特殊組み込みコマンドの実行結果が,現在シェルにも作用する。
丸括弧()サブシェルで実行する。関数内で定義した変数の値,関数定義,set,exportなどの特殊組み込みコマンドの実行結果が,現在シェルには作用しない。

丸括弧()で定義した場合,その関数は別プロセスとなり現在シェルへの影響を抑制できる。そのため,丸括弧で関数を定義すれば,意図しない変数の変更などを防ぐことができ,保守性が上がる。代わりに,新しくプロセスを作成するので,実行速度が遅くなる懸念がある。

関数を使えば,引数の扱いも柔軟にでき,複雑な処理も可能なので,汎用性が高いと考えられる。

alias

3点目はaliasを使う方法だ。

alias IS_ENABLED_SET_E='case "$-" in (*e*) echo true;; (*) echo false;; esac'
if IS_ENABLE_SET_E; then
 echo "set -e is enabled"
fi

aliasには以下の特徴がある。

aliasの特徴
  1. 再帰処理が可能
  2. 予約語が利用可能
  3. 複数行のコマンドが可能
  4. 名前に文字!%,@が使え,より柔軟な命名が可能

変数を使うより柔軟な対応ができ,aliasの定義は一度だけでよいので,alias実行はeval変数を使う場合よりも速度が速い可能性がある。

なお,ここでいうaliasの定義とは以下のようにaliasコマンドでaliasを作成することを指している。

alias ll='ls -l'

変数や関数と異なり,名前に!%,@を使えるので,工夫すればオブジェクト指向型言語のような,メンバー関数(メソッド)やメンバー変数(プロパティ)を実現できる可能性がある。

ただし,以下の注意点がある。

aliasの注意点
  1. 引数は末尾にしか取れない
  2. bashではexpand_aliasesオプションを有効にしないと使えない
  3. 関数や複合文など定義しても使えない場所がある

1点目の引数については,変数と同じであり,エイリアス名の後に渡した内容をそのまま引数と解釈する。

bash expand_aliases

bashにはexpand_aliasesというシェルのオプションが存在する。このオプションはbashでalias展開を有効化するためのオプションとなっており,以下のどちらかのコマンドでalias展開を有効化できる。

bash -O expand_aliases   ## enable with startup
shopt -s expand_aliases ## enable after startup

bashは標準では対話シェル以外では,expand_aliasesがオフになっており,aliasが展開されない

ただし,bashがPOSIXモード(6.11 Bash POSIX Mode - Bash Reference Manual)で起動された場合はaliasが展開される。bashがPOSIXモードで起動される場合は以下となる。

  • シンボリックリンクなどでbashをコマンド名shで実行した場合
  • 起動オプションに--posixを付けてbashを起動(bash --posix
  • setコマンドで有効化(set -o posix

シェルスクリプトでbashのaliasが展開されないパターンとしては,以下2通りが存在する。

  1. シバンにbashを明示的に指定して実行(#!/bin/bash
    cat <<-EOT >alias-shebang.sh
    #!/bin/bash
    alias c=:
    c
    EOT
    bash ./alias-shebang.sh
    ./alias-shebang.sh: line 3: c: command not found 
  2. シバンのないシェルスクリプトをbashの対話シェルからファイル名で実行
    cat <<-EOT >alias-colon.sh
    :
    alias c=:
    c
    EOT
    chmod +x alias-colon.sh
    bash -c './alias-colon.sh'
    ./alias-colon.sh: line 3: c: command not found

ここでは詳細を述べないが,シバンがない方がシェルスクリプトの互換性が高まることが調査の結果わかっている。そのため,aliasを使う場合は,bashの対話シェルからでも起動できるように以下のコードで冒頭に記述する必要がある。

command -v shopt && shopt -s expand_aliases
aliasが使えない場所

aliasはコマンドを実行しても,aliasが定義できなかったり展開されない記述箇所が存在する。

  1. 関数内で定義した場合は,(たとえ他の関数内でも)関数内では展開できない(関数外なら展開できる)。
    sh <<-EOT
    func1(){
    alias c=:
    }
    func2(){
    c
    }
    func1
    func2
    c
    EOT
    sh: line 5: c: comand not found
  2. 複合文({}, (), if, for, while, until)やリスト(;, &&, ||, &)で定義した場合は,その内部では展開できない(外部では展開できる)。
    sh <<-EOT
    { alias c=:; c; }
    c
    EOT
    sh: 1 c: not found

このことについて,理由を考察する。

まず1点目の関数内で定義した場合に関数内で使えない理由だ。これは関数は一つの独立した環境であるためだ。

コマンドの実行環境は以下の通りに定義されている。

Utilities other than the special built-ins (see Special Built-In Utilities) shall be invoked in a separate environment that consists of the following. The initial value of these objects shall be the same as that for the parent shell, except as noted below.
Open files inherited on invocation of the shell, open files controlled by the exec special built-in plus any modifications, and additions specified by any redirections to the utility
  • Current working directory
  • File creation mask
  • If the utility is a shell script, traps caught by the shell shall be set to the default values and traps ignored by the shell shall be set to be ignored by the utility; if the utility is not a shell script, the trap actions (default or ignore) shall be mapped into the appropriate signal handling actions for the utility
  • Variables with the export attribute, along with those explicitly exported for the duration of the command, shall be passed to the utility environment variables
The environment of the shell process shall not be changed by the utility unless explicitly specified by the utility description (for example, cd and umask).
2.12 Shell Execution Environment - Shell Command Language - POSIX 2016

すぐ上のシェル実行環境(shell execution environment)ではaliasが定義されているが,コマンド(utilityl)に関してはaliasについて定義されていない。上記引用部分の末尾で強調した通り,コマンド内で明確に変更されることが記述されていれば,変更は可能である。しかし,aliasの説明を確認しても,関数内でaliasを定義できるとの文言はない。そのため,関数内でaliasの定義と実行を同時にはできないのだと考えられる。

DESCRIPTION
An alias definition shall affect the current shell execution environment and the execution environments of the subshells of the current shell. When used as specified by this volume of POSIX.1-2008, the alias definition shall not affect the parent process of the current shell nor any utility environment invoked by the shell; see Shell Execution Environment.
alias - XCU - POSIX 2016

なお,aliasの説明で書かれている通り,現在シェルでaliasを定義した場合は,現在の実行環境とサブシェルに影響を与えるため,関数内でもaliasを展開できる。

2点目の複合文内でのaliasの定義と展開の同時実行について。これは,複合文内はUtilityと同じように別の環境であるためだと考えられる。POSIXでのUtiltyは以下に記載されている通り,Special Built-in utilityを除く名前が付いているものと定義されている。

A program, excluding special built-in utilities provided as part of the Shell Command Language, that can be called by name from a shell to perform a specific task, or related set of tasks.
3.439 Utility - Definitions - XBD - POSIX 2016

複合文は名前は付いていないので,Utilityではないと思うのだが,シェル実行環境とは異なるためこのような挙動になっているのだと思われる。

また,実行後に現在シェルでaliasが有効になっている件については,関数も複合文も同一プロセスであるため,実行後の現在シェルにはaliasの定義が残るのだと考えられる。

複合文でのaliasの定義・展開については一つ懸念がある。それは,複合文内でaliasの定義・展開が行われている外部のシェルスクリプトをdot.コマンドで読み込んだ場合,どうなるかだ。

シェルスクリプトが外部でどのように利用されるかは利用者次第だ。コマンドとして実行される他に,dotコマンドでライブラリーとして読み込んで実行されてもおかしくはない。その場合,複合文内でdotコマンドで読み込んだ場合,この挙動だと確実にalias展開が失敗することになる。しかし,dotコマンドはSpecial Built-In Utilityであるため,また別の挙動を示す可能性がある。

そこで,これらのaliasがどのような挙動を示すか調査した。実際にいろんなシェルでaliasの定義と実行結果について調査し,表にまとめた。検証コードは以下となる。

:
################################################################################
## \file      alias.sh
## \author    SENOO, Ken
## \copyright CC0
################################################################################

: <<-EOT
 aliasが使えないことがあるのでその検証。
 複合コマンド内でaliasを定義と実行を同時にできない。
 リストもダメなようだ
EOT

is_exe_enabled(){
 IS_ENABLED_SET_E=$( case "$-" in (*e*) echo true;; (*) echo false;; esac )
 is_command_enabled(){ command -v : >/dev/null 2>&1; }

 $IS_ENABLED_SET_E && set +e
 if is_command_enabled; then
  $IS_ENABLED_SET_E && set -e
  command -v ${1+"$@"} >/dev/null 2>&1 && return || return
 fi
 $IS_ENABLED_SET_E && set -e

 IFS=:
 for path in $PATH; do
  unset IFS
  command -p [ -x "$path/"${1+"$@"} ] && return
 done
}


echo_alias_result(){
 EXIT_STATUS=$?
 STR=${1+"$@"}
 if [ $EXIT_STATUS = 0 ]; then
  echo "Alias run success: $STR"
 else
  echo "Alias run failure: $STR"
 fi
}

EXE=':'
SHS='sh ksh ksh93 pdksh oksh lksh mksh posh ash dash bash zsh yash busybox'

TMP_ALIAS_FILE1='tmp-alias1.sh'
cat <<-EOT >$TMP_ALIAS_FILE1
 alias c=:
 c 2>&-
EOT

TMP_ALIAS_FILE2='tmp-alias2.sh'
cat <<-EOT >$TMP_ALIAS_FILE2
 { alias c=:; }
 c 2>&-
EOT

for sh in $SHS; do
 is_exe_enabled $sh || continue
 case $sh in busybox) sh='busybox ash'; esac
 $sh -c "shopt -q 2>&-" && sh="$sh -O expand_aliases"
 echo "$sh"
 $sh -c 'alias >/dev/null 2>&-' || continue

 ## Expected Success
 $sh <<-EOT; echo_alias_result 'Normal'
  alias c=$EXE
  c
 EOT
 $sh <<-EOT; echo_alias_result 'Define inside, run outside'
  { alias c=$EXE; }
  c
 EOT
 $sh <<-EOT; echo_alias_result 'Define outside, run inside'
  alias c=$EXE
  { c; }
 EOT
 $sh <<-EOT; echo_alias_result 'Include1'
  { . ./$TMP_ALIAS_FILE1; }
 EOT
 $sh <<-EOT; echo_alias_result 'Include2'
  { . ./$TMP_ALIAS_FILE2; }
 EOT
 $sh <<-EOT 2>&-; echo_alias_result 'func(){}'
  func(){
   alias c=$EXE
  }
  func
  c
 EOT

 ## Expected Failure
 $sh -c 'alias c=$EXE | c'    2>&-; echo_alias_result '|'
 $sh -c 'alias c=$EXE & c'    2>&-; echo_alias_result '&'
 $sh -c 'alias c=$EXE; c'     2>&-; echo_alias_result ';'
 $sh -c 'alias c=$EXE && c'   2>&-; echo_alias_result '&&'
 $sh -c '! alias c=$EXE || c' 2>&-; echo_alias_result '||'

 $sh <<-EOT 2>&-; echo_alias_result 'if'
  if true
  then
   alias c=$EXE
   c
  fi
 EOT

 $sh <<-EOT 2>&-; echo_alias_result 'for'
  for i in 0
  do
   alias c=$EXE
   c
  done
 EOT

 $sh <<-EOT 2>&-; echo_alias_result 'case'
  case '' in *)
   alias c=$EXE
   c
  esac
 EOT

 $sh <<-EOT 2>&-; echo_alias_result 'while'
  i=0
  while [ \$((i+=1)) = 1 ]
  do
   alias c=$EXE
   c
  done
 EOT

 $sh <<-EOT 2>&-; echo_alias_result 'until'
  i=0
  until [ \$((i+=1)) = 2 ]
  do
   alias c=$EXE
   c
  done
 EOT

 $sh <<-EOT 2>&-; echo_alias_result '()'
  (
   alias c=$EXE
   c
  )
 EOT

 $sh <<-EOT 2>&-; echo_alias_result '{}'
  {
   alias c=$EXE
   c
  }
 EOT
 $sh <<-EOT 2>&-; echo_alias_result 'func(){}'
  func1(){
   alias c=$EXE
  }
  func2(){
   c
  }
  func1
  func2
 EOT
done

Alias definition and expansion check
Type Command ksh93 pdksh oksh lksh mksh posh dash bash zsh yash busybox
Normal alias c=:
c

Define inside, run outside { alias c=:; }
c

Define outside, run inside alias c=:
{ c; }

Include1 { . ./tmp-alias1.sh; }
# alias c=:
# c


Include2 { . ./tmp-alias2.sh; }
# { alias c=:; }
# c


func(){} func(){
alias c=:
}
func
c

| alias c=: | c










& alias c=: & c










; alias c=:; c










&& alias c=: && c










|| ! alias c=: || c










if if true
then
alias c=:
c
fi











for for i in 0
do
alias c=:
c
done











case case '' in *)
alias c=:
c
esac











while i=0
while [ $((i+=1)) = 1]
do
alias c=:
c
done











until i=0
until [ $((i+=1)) = 2]
do
alias c=:
c
done











() (
alias c=:
c
)











{} {
alias c=:
c
}











func(){} func1(){
alias c=:
}
func2(){
c
}
func1
func2











表の見方は,縦軸に実行したコマンドがあり,横軸に実行したシェルが並んでいる。○が付いた欄はalias展開に成功したことを示し,空欄は失敗したことを示す。

調査結果についての考察を以下にまとめた。

aliasの挙動の結果考察
  • poshはaliasコマンドが存在しないため,全ての欄が空欄となった。
  • 予想通り,複合文とリストではaliasは展開できなかった。
  • ksh93だけ複合文内でdot.コマンドでaliasを展開できず,他のシェルでは展開できた。

dot.コマンドで読み込む場合は特殊なようで,多くのシェルでは問題なく複合文内でもaliasを展開できた。しかし,ksh93だけはできなかった。

aliasは名前に使える文字が多く,再帰など興味深く応用が効きそうではある。しかし,関数内や複合文内での定義・実行に難があることがわかった。また,aliasコマンドはPOSIX 2008になるまでのPOSIX 1992,2001,2004ではUPE(User Portability Option)扱いであり,必須ではなかった。実際に,poshのようにaliasコマンドに対応していないシェルが存在する。これらのことから,シェルスクリプト内でのaliasの利用は控えたほうが無難かもしれない。

Result

ここまでで処理に名前を付けてグループ化する3種類の方法について紹介した。

実際にこれらの方法でコマンドを実行して,実行速度を計測した。

実行は以下の8パターンを試した。

  1. 通常::
  2. 変数(単独):MACRO_N
  3. 変数(eval):MACRO_E
  4. alias:MACRO_A
  5. 関数(波括弧):MACRO_B
  6. 関数(丸括弧):MACRO_P
  7. 複合文(波括弧):{ :; }
  8. 複合文(丸括弧):( : )

関数の実行方法が変わった際にどれくらい実行速度が変わるかを見たいだけなので,試験用のコマンドは何もしない組み込みコマンドの:を採用した。

上記の7と8は処理の命名グループ化とは直接は関係ないが,コマンド実行時間がどれくらいかかるかの参考にする良い機会と判断したので,追加した。

なお,使用したシェルは以下であり,Ubuntu 16.04で測定した。

ksh93   Version AJM 93u+ 2012-08-01
pdksh   @(#)PD KSH v5.2.14 99/07/13.2
oksh    @(#)PD KSH v5.2.14 99/07/13.2
lksh    @(#)LEGACY KSH R52 2016/04/09
mksh    @(#)MIRBSD KSH R52 2016/04/09
posh    0.12.6
dash    January 19, 2003
bash    GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)
zsh     zsh 5.1.1 (x86_64-ubuntu-linux-gnu)
yash    Yet another shell, version 2.39
busybox ash     BusyBox v1.22.1 (Ubuntu 1:1.22.0-15ubuntu1) multi-call binary.

検証には以下の2個のコードを利用し,./run-time-macro.shを実行した。

:
################################################################################
## \file      run-time-macro.sh
## \author    SENOO, Ken
## \copyright CC0
################################################################################

main(){
 init
 SHS='sh ksh ksh93 pdksh oksh lksh mksh posh ash dash bash zsh yash busybox'
 for sh in $SHS; do
  case $sh in busybox) sh='busybox ash'; esac
  is_exe_enabled $sh || continue
  printf "$sh: "
  $sh time-macro.sh
 done
}

## \brief Initialize POSIX shell environment
init(){
 PATH="$(command -p getconf PATH 2>&-):${PATH:-.}"
 export PATH="${PATH#:}" LC_ALL='C'
 umask 0022
 set -eu
}

is_exe_enabled(){
 IS_ENABLED_SET_E=$( case "$-" in (*e*) echo true;; (*) echo false;; esac )
 is_command_enabled(){ command -v : >/dev/null 2>&1; }

 $IS_ENABLED_SET_E && set +e
 if is_command_enabled; then
  $IS_ENABLED_SET_E && set -e
  command -v ${1+"$@"} >/dev/null 2>&1 && return || return
 fi
 $IS_ENABLED_SET_E && set -e

 IFS=:
 for path in $PATH; do
  unset IFS
  command -p [ -x "$path/"${1+"$@"} ] && return
 done
}

main
:
################################################################################
## \file      time-macro.sh
## \author    SENOO, Ken
## \copyright CC0
################################################################################

## \brief Compare macro for readable code.
## 0. none
## 1. variable
## 2. eval
## 3. alias
## 4. function(){}
## 5. function()()
## 6. {}
## 7. ()

main(){
 init
 echo "Execution time[s] for $N times"
 echo "only body"
 run_body
 echo "including definition"
 run_total
}

## \brief Initialize POSIX shell environment
init(){
 PATH="$(command -p getconf PATH 2>&-):${PATH:-.}"
 export PATH="${PATH#:}" LC_ALL='C'
 umask 0022
 set -eu

 is_exe_enabled shopt && shopt -s expand_aliases
 export N=100000
 TAB=$(printf '\t')
 N_TEST=$(( 8 + 1 ))
 EXE=':'
 MACRO_N="MACRO_N='$EXE'"
 MACRO_E="MACRO_E='eval $EXE'"
 MACRO_A="alias MACRO_A='$EXE'"
 MACRO_B="MACRO_B(){ $EXE; }"
 MACRO_P="MACRO_P()( $EXE  )"
 BRACE="{ $EXE; }"
 PAREN="( $EXE )"
}

is_exe_enabled(){
 IS_ENABLED_SET_E=$( case "$-" in (*e*) echo true;; (*) echo false;; esac )
 is_command_enabled(){ command -v : >/dev/null 2>&1; }

 $IS_ENABLED_SET_E && set +e
 if is_command_enabled; then
  $IS_ENABLED_SET_E && set -e
  command -v ${1+"$@"} >/dev/null 2>&1 && return || return
 fi
 $IS_ENABLED_SET_E && set -e

 IFS=:
 for path in $PATH; do
  unset IFS
  command -p [ -x "$path/"${1+"$@"} ] && return
 done
}

run_body(){
 {
  printf 'Command\nReal\nUser\nSys\n'
  timeit_body '$EXE' "$EXE"
  timeit_body '$MACRO_N' "$MACRO_N"
  timeit_body '$MACRO_E' "$MACRO_E"
  timeit_body  'MACRO_A' "$MACRO_A"
  timeit_body  'MACRO_B' "$MACRO_B"
  timeit_body  'MACRO_P' "$MACRO_P"
  timeit_body  "$BRACE" "$BRACE"
  timeit_body  "$PAREN" "$PAREN"
 } 2>&1 | format
}

## \brief Time exe only body
timeit_body(){
 EXE_STR="$1"; EXE_VAR="$2"
 echo "$EXE_VAR"

 \time -p sh <<-EOT
  eval "$EXE_VAR"
  for i in \$(yes | head -n $N); do
   $EXE_STR
  done
 EOT
}

run_total(){
 {
  printf 'Command\nReal\nUser\nSys\n'
  timeit_total '$EXE'     "$EXE"
  timeit_total '$MACRO_N' "$MACRO_N"
  timeit_total '$MACRO_E' "$MACRO_E"
  timeit_total  'MACRO_A' "$MACRO_A"
  timeit_total  'MACRO_B' "$MACRO_B"
  timeit_total  'MACRO_P' "$MACRO_P"
  timeit_total  "$BRACE" "$BRACE"
  timeit_total  "$PAREN" "$PAREN"
 } 2>&1 | format
}

## \brief Time exe including definition
timeit_total(){
 EXE_STR="$1"; EXE_VAR="$2"
 echo "$EXE_VAR"

 \time -p sh <<-EOT
  eval "$EXE_VAR"  ## for alias
  for i in \$(yes | head -n $N); do
   eval "$EXE_VAR"  ## run definition
   $EXE_STR
  done
 EOT
}

format(){
 pr -t"$N_TEST"s"$TAB" | sed -e 's/real *//g' -e 's/user *//g' -e 's/sys *//g'
}

main

実行結果を整理したものを以下の表に示した。

Group commands execution time[s] for 100000 times
Only body
Shell : MACRO_N=':' MACRO_E='eval :' alias MACRO_A=':' MACRO_B(){ :; } MACRO_P()( : ) { :; } ( : )
ksh93 0.02 0.03 0.08 0.02 0.05 13.88 0.02 15.43
pdksh 0.03 0.04 0.11 0.03 0.07 15.86 0.03 15.92
oksh 0.03 0.03 0.09 0.02 0.04 13.43 0.02 13.81
lksh 0.02 0.03 0.08 0.02 0.04 13.90 0.03 14.15
mksh 0.03 0.04 0.11 0.03 0.06 14.71 0.02 14.33
posh 0.02 0.03 0.09 0.03 0.04 14.20 0.03 14.05
dash 0.02 0.03 0.09 0.03 0.05 14.10 0.03 14.34
bash 0.05 0.05 0.13 0.02 0.04 14.02 0.02 13.47
zsh 0.02 0.03 0.08 0.02 0.04 13.78 0.02 14.32
yash 0.03 0.03 0.09 0.03 0.04 14.61 0.05 14.88
busybox ash 0.07 0.07 0.16 0.05 0.09 13.78 0.06 13.43
Including definition
Shell : MACRO_N=':' MACRO_E='eval :' alias MACRO_A=':' MACRO_B(){ :; } MACRO_P()( : ) { :; } ( : )
ksh93 0.10 0.12 0.18 0.13 0.16 14.10 0.10 30.06
pdksh 0.09 0.12 0.16 0.12 0.13 14.80 0.10 28.75
oksh 0.09 0.11 0.16 0.12 0.13 14.09 0.10 29.23
lksh 0.09 0.17 0.17 0.13 0.14 14.80 0.10 29.02
mksh 0.08 0.12 0.17 0.14 0.16 15.21 0.13 27.72
posh 0.09 0.12 0.17 0.12 0.14 15.18 0.10 29.55
dash 0.10 0.12 0.19 0.13 0.15 16.35 0.12 31.62
bash 0.09 0.12 0.16 0.12 0.14 14.71 0.10 28.33
zsh 0.09 0.11 0.17 0.12 0.13 15.00 0.10 29.09
yash 0.10 0.13 0.19 0.12 0.15 15.89 0.11 29.40
busybox ash 0.17 0.22 0.32 0.26 0.23 14.37 0.19 28.39

まず,表の見方について説明する。この表は10万回実行した際の経過時間[s]を示している。そのため,100倍して,単位にm(ミリ)をつければ1回あたりにの行速度となる。

(a) Only bodyはコマンドの定義を含めずに,本体部分を実行した際の速度となっている。そして,(b) Including definitionはコマンドの定義を含めた計測時間となっている。なお,aliasは複合文内で定義をすると実行できないため,計測のforループ外で定義する必要がある。処理の負担を平等にするため全てのケースでevalにより定義を実行している。そのため,Including definitionはevalのコマンドの実行分の約0.05 s/10万回の時間が余計にかかっている。また,最後の複合文2個は定義と本体が同一であるため,単純に2回実行することになり,時間がかかっている。2列目の:の列が命名によるグループ化を行っていないケースなので,これが実行速度の基準となる。

調査結果の考察を以下に掲載する。

命名グループ化の速度計測結果の考察
  • 定義を含めたとしても,変数(単独),alias,複合(波括弧)は基準ケースよりも3 ms程度遅れる程度であり,十分速度が速い。これらはシェルのコマンドライン解釈だけで展開されるため,プロセスの発生がないため速度が速いのだと思われる。
  • 関数(丸括弧)と複合(丸括弧)は,サブシェルで実行するため,基準ケースの60-70倍も実行時間を要しており,ループでの利用は極力控えたほうがよい
  • 変数(eval)は,基準ケースの2-3倍程度の実行時間を要しており,関数(波括弧)よりも遅いことが分かった。そのため,変数(eval)よりも,関数(波括弧)を利用したほうがよいといえる。
  • busyboxは全体として組み込みコマンドの実行速度が他のシェルの約2倍かかっており,遅いことが分かった。それ以外のシェルは概ね似たような実行速度だった。

Summary

シェルスクリプトの可読性を向上させるために,複数のコマンドに名前を付けてグループ化する方法を検討した。その概要を以下に示す。

命名グループ化の検討の概要
  • コマンドのグループ化には変数,関数,aliasの3種類の方法が存在。
  • aliasは関数と複合文での利用に注意が必要で,過去のPOSIXでオプションのため利用は非推奨
  • 実行速度の計測結果から,命名グループ化には変数(単独)と関数(波括弧)の利用を推奨

コマンドの実行というシェルスクリプトを書くうえでかなり基本的な部分に注目した検討を行った。実行速度を計測することで,今までなんとなく把握していた,丸括弧によるサブシェルでの実行が遅いということや,変数やalias展開は実行速度が速いということが定量的に分かった。

今後は今回の調査結果を利用して,可読性と実行速度に注意を払ったシェルスクリプトの作成を心がける。

0 件のコメント:

コメントを投稿