rcmdnk's blog

(テクノロジック) technologic 電池がいらない レーザーポインター USB 充電式 スタイリッシュ ワイヤレス プレゼンター

シェルスクリプトの関数は引数は値渡ししか出来ませんが、 ポインタ渡し的なことがしたいな、と思ったところがあったので作ってみました。

evalを使う

シェルスクリプトにポインタの概念はありませんし 参照渡しとかも存在しませんが、 evalを使うと文字列を一度解釈した上で式を実行してくれるので それっぽいことが出来ます。

pass_pointer.sh
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash

pass_pointer () {
  local var=$1
  eval "echo pass_pointer: name=$var, value\$$var"
  eval "$var=\$((var+1))"
}

foo=1
echo "foo=$foo"
pass_pointer foo
echo "foo=$foo"

こんな感じ。

$ ./pass_pointer.sh
foo=1
pass_pointer: name=foo, value=1
foo=2

てな具合にfooという変数をpass_pointer関数の中でいじって 外でも変更を確認出来ました。

シェルスクリプトだと普通に変数を宣言するとグローバル変数なので それをそのまま関数の中でいじることで変更することも多いですが、 汎用関数として使いたいような場合にはこんな感じでやるのもありかと。

もちろんこの引数で渡す変数もグローバル変数でないといけないので ちょっと注意。

グローバル変数で直接変更する

同じようなことを特別なことをせつにやりたい場合には 変更する変数をグローバル変数にして関数内では固定してしまうことです。

fixed_name.sh
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash

fixed_name () {
  echo pass_pointer: name=foo, value=$foo"
  foo=$((foo+1))"
}

foo=1
echo "foo=$foo"
fixed_name
echo "foo=$foo"

これも

$ ./fixed_name.sh
foo=1
fixed_name: name=foo, value=1
foo=2

となります。

fooしか変更できなくなりますが、 シェルスクリプトなんでこんな感じで使う方が一般的だと思います。

echoで返す

変更する変数の名前を固定したくない時、 通常はechoとかで値を返してfooに代入する、という形を使います。

echo_value.sh
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash

echo_value () {
  local var=$1
  echo "echo_value: value=$var" 1>&2
  echo $((var+1))
}

foo=1
echo "foo=$foo"
foo=$(echo_value "$foo")
echo "foo=$foo"

これで

$ ./echo_value
foo=1
foo=echo_value: value=1
2

これで一応同じ様にfooの値を関数内で参照して外側でfooの値を変更出来ています。

関数内でfooという名前が見えないのはそれほど問題ないと思います。

ただ、echoで情報を表示しようとするとそれが変数に入ってしまうので 無理やりエラー出力にして表示させたりしないといけないのがちょっと面倒なところです。

echo "echo_value: value=$var" > /dev/tty

の様に直接ターミナルに出力する方法もありますが、これだとスクリプト全体をログに書き出したい時とかに どうしようもなくなるのであまり使い勝手は良くないです。

まあ、こちらの方が素直かもしれませんが、もう一つの問題として、 こちらはサブシェルの呼び出しになるのでちょっと遅くなります。

まあ通常はほとんど気にしない所かもしれませんが、実際に測ってみると、

time_check.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env bash

loop=$(seq 0 10000)

echo loop_time
time for n in ${loop};do
  :
done
printf "\n\n"

echo pass_pointer
time for n in ${loop};do
  pass_pointer () {
    local var=$1
    eval "$var=\$((var+1))"
  }
  foo=1
  pass_pointer foo
done
printf "\n\n"

echo fixed_name
time for n in ${loop};do
  fixed_name () {
    foo=$((foo+1))
  }
  foo=1
  fixed_name
done
printf "\n\n"

echo echo_value
time for n in ${loop};do
  echo_value () {
    local var=$1
    echo $((var+1))
  }
  foo=1
  foo=$(echo_value $foo)
done

とかして1万回のループを試してみると

loop_time

real    0m0.037s
user    0m0.033s
sys     0m0.001s


pass_pointer

real    0m0.386s
user    0m0.341s
sys     0m0.006s


fixed_name

real    0m0.221s
user    0m0.159s
sys     0m0.002s


echo_value

real    0m16.376s
user    0m4.584s
sys     0m7.376s

という感じ。

最初のloop_timeものは単にループそのものの時間がわからないと比較も難しいときもあるので一応出してますが、 今回は十分に小さいです。

fixed_nameが一番余計なことをしないので一番速いです。 pass_pointerfixed_nameの倍くらい。

そしてecho_valueの方はpass_pointerの30倍位時間がかかっています。

1万回ループ回すことはそうそう々ないかもしれませんが、 千回位ならたまにはあるかと思います。 そういった時に1秒かかるかかからないか、は結構大きいかと。

ただecho_valueの方が素直ですしシェルスクリプトで関数から返り値をもらう、 といったらこの方法が普通なので一回きりの実行で 特に標準出力に別途出力する必要がないときなどはこの方法でやれば良いと思います。

returnで渡す

これは0以上の整数を扱う時限定になりますが シェルスクリプトの関数はreturnで終了値として数値を返せるのでそれを使う方法。

return_value.sh
1
2
3
4
5
6
7
8
9
10
11
return_value () {
  local var=$1
  echo "return_value: value=$var"
  return $((var+1))
}

foo=1
echo "foo=$foo"
return_value "$foo"
foo=$?
echo "foo=$foo"

これで

$ ./return_value
foo=1
foo=return_value: value=1
2

と、echo_valueと全く同じことが出来ます。

しかも今度は関数内でechoを標準出力で普通に使うことが出来るようになっています。

時間を測ってみると、

time_check_return_value.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash

loop=$(seq 0 10000)

echo loop_time
time for n in ${loop};do
  :
done
printf "\n\n"

echo return_value
time for n in ${loop};do
  return_value () {
    local var=$1
    return $((var+1))
  }
  return_value "$foo"
  foo=$?
done

これを使って

$ ./time_check_return_value.sh
loop_time

real    0m0.037s
user    0m0.035s
sys     0m0.000s


return_value

real    0m0.237s
user    0m0.233s
sys     0m0.001s

という感じでpass_pointerよりむしろ速いです。 (試行回数を10倍にして何度か試しましたが、有意に1.5倍位pass_pointerのが時間がかかります。)

シェルスクリプトのreturnをこういった使い方をするのが正しいのかどうか知りませんが、 他の言語的な考えであれば当たりまえの使い方ですし、 正の整数に限るところではありますがこういった使い方もありかと。

まとめ

シェルスクリプト関数でポインタ渡しもどきを実装するという話から、 ちょっとシェルスクリプトの実行時間についての話にもなりました。

ポインタ渡しもどきに関してはこんな感じでそれっぽいことが出来ます。

echoで返すやり方は一般的で通常はこれで良いと思いますが、 遅くなるし中でechoが使えなくなるしあまりうれしいものじゃないです。

ただ、eval自体はなんでも実行できてしまうので 時々脆弱性の話も出てきますし大事な所では使うのは避けたいかもしれません。

それぞれ

  • pass_pointer:
    • Pros: 速い。
    • Cons: 変数はグローバル変数。evalが脆弱性を生む可能性がある。
  • fixed_name:
    • Pros: 最も速い。一般的な書き方。
    • Cons: 変数はグローバル変数で一つに固定。
  • echo_value:
    • Pros: 変数がグローバル変数でなくても良い。一般的な書き方。
    • Cons: 遅い。標準出力を普通に使えない。
  • return_value:
    • Pros: 速い。変数がグローバル変数でなくても良い。
    • Cons: 0以上の整数しか使えない。ステータスチェックが出来なくなる。

と言った感じ。

通常はfixed_nameな方法とecho_valueな方法で作れば良いと思いますが、 他の方法も覚えておくと高速化出来たり便利に使えたりすると思います。

まあ、大事な所を操作したり速くしたいとかいうのであれば そもそも論シェルスクリプト使うな、って話なんですが。

Sponsored Links
Sponsored Links

« vim_ahkにjk/sdでNormalモードに入るキーを追加 シェルスクリプトで数字かどうか判断する方法(exprだけじゃない) »

}