R.A. Epigonos et al.

[dash] シェルスクリプト関数のローカル変数には local を付けなさい

シェルスクリプトの関数内で変数にlocalとreadonlyを使うと悲しい。readonly はグローバル変数だけに限るべきなのかもしれない。

1つの引数を受け取って、1 を足す関数 increment を考える。以下のとおり定義して、実行すると成功。関数 increment の定義は問題ないように見える。

$ cat --number ./test.sh
     1	#!/bin/dash
     2	set -eux;
     3	
     4	increment () {
     5		local _A;
     6		readonly _A="${1}";
     7		echo "$((_A+1))";
     8		return 0;
     9	}
    10	
    11	calc () {
    12		local _A;
    13		_A='100';
    14		increment "${_A}";
    15		increment "${_A}";
    16		return 0;
    17	}
    18	
    19	calc;
    20	
    21	exit 0;
$ ./test.sh
+ calc
+ local _A
+ _A=100
+ increment 100
+ local _A
+ readonly _A=100
+ echo 101
101
+ return 0
+ increment 100
+ local _A
+ readonly _A=100
+ echo 101
101
+ return 0
+ return 0
+ exit 0

次に increment を呼び出す前に increment の中で readonly 宣言されている変数 _A と同名の変数を readonly 宣言した状態で実行する。increment の呼び出し前に readonly 宣言された変数が increment 内で書き換えられているため (6行目) エラーが出て止まる。僕にとってこれは期待した結果ではなかった。なぜなら、関数の中で local 宣言した変数を関数内でいろいろと操作している間に誤って書き換えてしまうことを避けるために readonly 宣言していたからだ。

$ cat --number ./test.sh
     1	#!/bin/dash
     2	set -eux;
     3	
     4	increment () {
     5		local _A;
     6		readonly _A="${1}";
     7		echo "$((_A+1))";
     8		return 0;
     9	}
    10	
    11	calc () {
    12		local _A;
    13		readonly _A='100';
    14		increment "${_A}";
    15		increment "${_A}";
    16		return 0;
    17	}
    18	
    19	calc;
    20	
    21	exit 0;
$ ./test.sh
+ calc
+ local _A
+ readonly _A=100
+ increment 100
+ local _A
+ readonly _A=100
./test.sh: 6: readonly: _A: is read only

この挙動については dash(1) に説明がある。すなわち、local 宣言された変数と同名の変数が local 宣言前に存在していた場合、その変数の値と readonly 属性は local 宣言された変数に継承されるわけだ。

$ MANWIDTH=69 man 1 dash | sed --quiet --expression '/When a variable is made local/,/^\s*$/p'
     When a variable is made local, it inherits the initial value
     and exported and readonly flags from the variable with the
     same name in the surrounding scope, if there is one.  Other‐
     wise, the variable is initially unset.  The shell uses dynamic
     scoping, so that if you make the variable x local to function
     f, which then calls function g, references to the variable x
     made inside g will refer to the variable x declared inside f,
     not to the global variable named x.

同じスコープ内で同名の変数の readonly 属性を切り替えることが難しそうなので、local 宣言した変数に readonly 属性を付けることによるスコープ内での変数の書き換えチェックは自分で行うことにする。そうすると、ローカル変数に対して readonly 属性をつけない方針が取れるので次のようにかける。この場合は成功する。

$ cat --number ./test.sh
     1	#!/bin/dash
     2	set -eux;
     3	
     4	increment () {
     5		local _A;
     6		_A="${1}";
     7		echo "$((_A+1))";
     8		return 0;
     9	}
    10	
    11	calc () {
    12		local _A;
    13		_A='100';
    14		increment "${_A}";
    15		increment "${_A}";
    16		return 0;
    17	}
    18	
    19	calc;
    20	
    21	exit 0;
$ ./test.sh
+ calc
+ local _A
+ _A=100
+ increment 100
+ local _A
+ _A=100
+ echo 101
101
+ return 0
+ increment 100
+ local _A
+ _A=100
+ echo 101
101
+ return 0
+ return 0
+ exit 0

変数に readonly 属性をつけなくなったことで、スコープ内で変数の期待しない書き換えに対してエラーが出なくなった。すなわち、以下のとおり increment 関数の中で _A 変数を書き換えた際にエラーを出さなくなってしまった。しかしながら、_A を書き換えているのと同じスコープ内で _A 変数は local 宣言されている。このため、このスコープから抜ければ _A は元の値に戻る。これにより、increment 関数を2回呼び出した場合でも1回目に行った _A の書き換えが 2回目の計算に影響をおよぼすことがなくなっている。

$ cat --number ./test.sh
     1	#!/bin/dash
     2	set -eux;
     3	
     4	increment () {
     5		local _A;
     6		_A="${1}";
     7		echo "$((_A+1))";
     8		_A="$((_A+1))";
     9		return 0;
    10	}
    11	
    12	calc () {
    13		local _A;
    14		_A='100';
    15		increment "${_A}";
    16		increment "${_A}";
    17		return 0;
    18	}
    19	
    20	calc;
    21	
    22	exit 0;
$ ./test.sh
+ calc
+ local _A
+ _A=100
+ increment 100
+ local _A
+ _A=100
+ echo 101
101
+ _A=101
+ return 0
+ increment 100
+ local _A
+ _A=100
+ echo 101
101
+ _A=101
+ return 0
+ return 0
+ exit 0

意図せずとも、変数名のバッティングの起こる可能性は常にある。したがって、変数名がバッティングしたことで止まるような関数は適切な関数とは言えないような気がする。今回はこれを避けるために、これまで付けていた readonly 属性をつけない方針をとった。こうすることで、変数の書き換えチェックはできなくなったが、それはプログラマが気をつければなんとかなると思いたい。

リファレンス

  1. dash local variable readonly - Google 検索
  2. #613556 - Why dash's local variable inherits its value from outter env? - Debian Bug report logs
  3. local and readonly variables in #shell · GitHub
  4. cannot declare local variables if they're readonly

ソーシャルブックマーク

  1. はてなブックマーク
  2. Google Bookmarks
  3. del.icio.us

ChangeLog

  1. Posted: 2008-02-10T15:08:57+09:00
  2. Modified: 2008-02-10T15:08:57+09:00
  3. Generated: 2023-08-27T23:09:13+09:00