kshではreadonlyにした環境変数は子プロセスでもreadonly (環境変数 A__z の謎)


タイトルのとおりですが ksh(KornShell)では環境変数を readonly にすると子プロセス(正確には子プロセスが ksh の場合)にも引き継がれて readonly のままになっています。なんだこれは?とハマったので。ネットを探してもほとんど情報がないです。(ここ「Remove the magic A__z environment var」ぐらい)

検証コード

$ ksh
$ readonly VAR=123
$ export VAR
$ ksh # 新たなkshを起動
$ VAR=456
ksh: VAR: is read only # readonlyになってる

もちろん OS としては環境変数に readonly なんてものはないので、子プロセスが ksh 以外だと普通に書き換えられます。ただし孫プロセスが ksh だと readonly 扱いです。(笑)

$ ksh
$ readonly VAR=123
$ export VAR

$ dash
$ echo $VAR
123
$ VAR=456 # dashで書き換えられる

$ ksh
$ echo $VAR
456
$ VAR=789
ksh: VAR: is read only # 書き換えられたのに readonly らしい

どうやって実現してるんだ?と少し考えて思い出したのが BASH のシェル関数のエクスポートの仕組みです。

bashではシェル関数を子プロセス(当然 bash に限る)にエクスポートできます。例えば export -f foo でシェル関数 foo をエクスポートすると BASH_FUNC_foo%% という環境変数に () { echo foo; } という値(コード)が代入されてエクスポートとされます。(注意 dash だとこの環境変数は見えません。おそらく環境変数名に %% という文字が含まれているからではないかと。POSIX 的には不正だと思うのですが動かない OS はないのでしょうか?コメント参照)

同様に ksh では A__z という環境変数に(複数の)環境変数の属性が代入されていました。

$ ksh
$ readonly VAR=123
$ export VAR
$ env | grep VAR
VAR=123
A__z="*SHLVL=! VAR

$ typeset -i VAR # VARを数値型にしてみる
$ env | grep VAR
VAR=123
A__z="*SHLVL=#*VAR

A__z 環境変数の値の意味はよくわかりませんがソースコードをざっと見る限り、typeset で指定できる属性が格納されるようですね。(ちなみに const char e_envmarker[] = "A__z";

stakputs(e_envmarker);
tell = staktell();
nv_scan(shp->var_tree, attstore,(void*)0,0 (NV_RDONLY|NV_UTOL|NV_LTOU|NV_RJUST|NV_LJUST|NV_ZFILL|NV_INTEGER));

そうした理由はわからなくはないですが子プロセス(サブシェルではない)に変数の属性が引き継がれて嬉しいことってなにかあるのでしょうか?

ちなみに私がやりたかったのは、親(ksh)プロセスで readonly にした変数を子(ksh)プロセスで書き換えたかったのです。zsh では typeset +r で readonly 属性を外せたりするのですが ksh や bash では外せません。そこで考え出したのが env を使って自分自身を起動し直す方法です。

if [ "${FLAG:-}" != 1 ]; then
  typeset +x VAR # readonlyとなってる環境変数VARのエクスポート属性を削除しただの変数にする
  exec env VAR="$VAR" FLAG=1 "$0" "$@" # env コマンドの引数でVAR変数を改めて環境変数にして呼び出す
fi

同じようにA__z 環境変数を削除することでも実現できると思います。この場合は属性全てが削除されます。