やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Bashの宣言・スコープ・ライフサイクル

 変数・関数について。

成果物

目次

  1. スコープ
  2. シェル変数のライフサイクル
    1. プロセス・スコープ
    2. ファイル・スコープ
    3. ストリーム・スコープ
  3. パターン網羅
  4. 最適解

1. スコープ

 (bashのmanではスコープ、ライフサイクルなどの用語は見たことがない。ここでは勝手にそう呼ぶ)

変数の分類

 Bashの変数・関数は2種類に大別できる。(生存期間(ライフサイクル)? に応じて)

呼称 スコープ 例(変数) 例(関数)
環境変数 全プロセス共有 export K=V F(){ echo A; }; export -f F;
シェル変数 カレントプロセスのみ K=V F(){ echo A; }

 シェル変数はさらに2種類に大別できる。

宣言箇所 スコープ
関数内 Local(関数内のみ)
関数外 Global(カレントプロセス共有)

 よってシェル変数は以下3つに大別できる。

参照できる箇所 変数の種類 コード例(変数) コード例(配列) コード例(連想配列)
全プロセス全箇所 環境変数(Env) export K=V export A=(A B) declare -A AA=([A]=1 [B]=2); export AA;
自プロセス全箇所 シェル変数(Global) K=V A=(A B) declare -A AA=([A]=1 [B]=2)
自プロセス自関数内 シェル変数(Local) F(){ local K=V; } F(){ local A=(A B); } F(){ declare -A AA=([A]=1 [B]=2); }

 なお、Localにできるのは変数のみ。関数はGlobalか環境変数にしかできない。関数内に関数を定義してもGlobalになる。

プロセスのライフサイクル

 別プロセス化によりスコープを分けることができる。(Globalシェル変数) 正確にはスコープでなくライフサイクル。子プロセス化すればその中で宣言されたものは実行後に死ぬため名前汚染を防げる。

方法 コード例
サブプロセス unset K; ( declare K=V; echo $K; ); [ -v K ] && echo '定義済み' || echo '未定義';
別プロセス(pipe) unset K; K=A | { K=B; echo $K; }; [ -v K ] && echo '定義済み' || echo '未定義';

 コマンド実行なら変数を設定して実行できる。

対象
環境変数 LC_ALL=C man bash
シェル変数 env LC_ALL=C man bash

 ただし組込関数echoなどは実行できない。(unset K; K=V echo $K

 変数設定に縛られずあらゆるコマンドを別プロセスで実行するなら以下。

  • bash -c "{ LC_ALL=C; [[ $LC_ALL=C ]] && man bash; }"

 大別すると以下4つの観点に絞られる。

プロセス

表記 説明
Local。同関数内のみ参照可。
Global。同プロセスまたは子孫プロセスのみ参照可。
Environment。プロセス間共有できる。

2. シェル変数のライフサイクル

  1. プロセス・スコープ
  2. ファイル・スコープ
  3. ストリーム・スコープ

 こんな名前はない。便宜上呼んでいるだけ。

2-1. プロセス・スコープ

 別プロセス化・関数化にてグローバル名前汚染を最小限にできる。

方法 コード例
サブプロセス unset K; ( declare K=V; echo $K; ); [ -v K ] && echo '定義済み' || echo '未定義';
別プロセス(pipe) unset K; K=A | { K=B; echo $K; }; [ -v K ] && echo '定義済み' || echo '未定義';
関数化(local) unset K; F() { local K=V; echo $K; }; F; [ -v K ] && echo '定義済み' || echo '未定義';
関数化(declare) unset K; F() { declare K=V; echo $K; }; F; [ -v K ] && echo '定義済み' || echo '未定義';

 グルーピングはカレントプロセス内である。関数でもないためLocal化できない。

汚染する方法 コード例
グルーピング unset K; { declare K=V; echo $K; }; [ -v K ] && echo '定義済み' || echo '未定義';

2-2. ファイル・スコープ

別ファイルをカレントプロセス化する

 . <path>を使う。C言語#include同様テキスト結合。結果としてカレントプロセスになる。

define.sh

K=V

main.sh

unset K
. define.sh
echo $K
[ -v K ] && echo '定義済み' || echo '未定義'
$ ./main.sh
V
定義済み

別ファイルをサブプロセス化する

 bash <path>を使う。

run.sh

unset K
bash main.sh
[ -v K ] && echo '定義済み' || echo '未定義'
$ ./run.sh
V
未定義

2-2. ストリーム・スコープ

ストリームをカレントプロセス化する

unset K
. <(echo 'K=V; echo $K')
[ -v K ] && echo '定義済み' || echo '未定義'

ストリームをサブプロセス化する

unset K
bash -c '{ K=V; echo $K; }'
[ -v K ] && echo '定義済み' || echo '未定義'
unset K
(
    . <(echo 'K=V; echo $K')
)
[ -v K ] && echo '定義済み' || echo '未定義'

パターン網羅

defines.sh

#!/bin/bash
Env() {
    export K=V
    export A=(A B C)
    #export AA=([a]=1 [b]=2) # declare -ax AA=([0]="2") となる
    #`export declare -Ag AA=([A]=1 [B]=2)`はエラーになる(`bash: export: `-Ag': 有効な識別子ではありません`)
    #`declare -A`では関数外`set`に表示されない。`${#AA[@]}`などの参照も不可。`-g`がないとGlobalにならずLocalになり本関数内で死ぬ。
    declare -Ag AA=([A]=1 [B]=2)
    export AA
    F() { echo MyFunc; }
    export F
    # exportした配列は`env`,`printenv`だと出ないが`declare -x`,`set`だと出る
    set | grep '^K='
    set | grep '^A='
    set | grep '^AA='
    set | grep '^F ()'
    echo "$K"
    echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
    for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo '';
    echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
    for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo '';
    F
}
Global() {
    K=V
    A=(A B C)
    declare -Ag AA=([A]=1 [B]=2)
    F() { echo MyFunc; }
    set | grep '^K='
    set | grep '^A='
    set | grep '^AA='
    set | grep '^F ()'
    echo "$K"
    echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
    for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo '';
    echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
    for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo '';
}
Local() {
    local K=V
    local A=(A B C)
    declare -A AA=([A]=1 [B]=2)
    # 予期しないトークン `(' 周辺に構文エラーがあります
    #local F() { echo MyFunc; }
    # 予期しないトークン `(' 周辺に構文エラーがあります
    #declare F() { echo MyFunc; };
    # Local化されずGlobalになる...
    F() { echo MyFunc; }
    # declareでローカル化しようとしたが効かず
    declare F
    set | grep '^K='
    set | grep '^A='
    set | grep '^AA='
    set | grep '^F ()'
    echo "$K"
    echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
    for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo '';
    echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
    for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo '';
}
SubProcEnv() {
    (
        export K=V
        export A=(A B C)
        declare -Ag AA=([A]=1 [B]=2)
        export AA
        F() { echo MyFunc; }
    )
    set | grep '^K='
    set | grep '^A='
    set | grep '^AA='
    set | grep '^F ()'
    [ -n "$(set | grep '^K=')" ] && echo "$K"
    [ -n "$(set | grep '^A=')" ] && echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
    [ -n "$(set | grep '^A=')" ] && { for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo ''; }
    [ -n "$(set | grep '^AA=')" ] && echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
    [ -n "$(set | grep '^AA=')" ] && { for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo ''; }
}
SubProcGlobal() {
    (
        K=V
        A=(A B C)
        declare -Ag AA=([A]=1 [B]=2)
        F() { echo MyFunc; }
    )
    set | grep '^K='
    set | grep '^A='
    set | grep '^AA='
    set | grep '^F ()'
    [ -n "$(set | grep '^K=')" ] && echo "$K"
    [ -n "$(set | grep '^A=')" ] && echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
    [ -n "$(set | grep '^A=')" ] && { for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo ''; }
    [ -n "$(set | grep '^AA=')" ] && echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
    [ -n "$(set | grep '^AA=')" ] && { for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo ''; }
}
Group() {
    {
        K=V
        A=(A B C)
        declare -Ag AA=([A]=1 [B]=2)
        F() { echo MyFunc; }
    }
    set | grep '^K='
    set | grep '^A='
    set | grep '^AA='
    set | grep '^F ()'
    [ -n "$(set | grep '^K=')" ] && echo "$K"
    [ -n "$(set | grep '^A=')" ] && echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
    [ -n "$(set | grep '^A=')" ] && { for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo ''; }
    [ -n "$(set | grep '^AA=')" ] && echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
    [ -n "$(set | grep '^AA=')" ] && { for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo ''; }
}
# ------------------------------------------------------------------
# Readonlyにするとunsetできなくなる! 困るのでサブプロセス内でやる。
# ------------------------------------------------------------------
Global_Readonly() {
    (
        readonly K=V
        readonly A=(A B C)
        # readonly: `-A': 有効な識別子ではありません
      #readonly declare -A AA=([A]=1 [B]=2)
      declare -Agr AA=([A]=1 [B]=2)

      set | grep '^K='
      set | grep '^A='
      set | grep '^AA='
      echo "$K"
      echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
      for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo '';
      echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
      for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo '';
  )
  (
      declare -gr K=V
      declare -agr A=(A B C)
      declare -Agr AA=([A]=1 [B]=2)

      set | grep '^K='
      set | grep '^A='
      set | grep '^AA='
      echo "$K"
      echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
      for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo '';
      echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
      for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo '';
  )
}
Local_Readonly() {
  (
      OF1() {
          local -r K=V
          local -r A=(A B C)
          declare -A AA=([A]=1 [B]=2)
          F() { echo MyFunc; }
      }
      OF2() {
          declare -r K=V
          declare -ar A=(A B C)
          declare -Ar AA=([A]=1 [B]=2)
          F() { echo MyFunc; }
      }
      set | grep '^K='
      set | grep '^A='
      set | grep '^AA='
      set | grep '^F ()'
      [ -n "$(set | grep '^K=')" ] && echo "$K"
      [ -n "$(set | grep '^A=')" ] && echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
      [ -n "$(set | grep '^A=')" ] && { for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo ''; }
      [ -n "$(set | grep '^AA=')" ] && echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
      [ -n "$(set | grep '^AA=')" ] && { for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo ''; }
  )
}
# テキストをコードとして実行する`. <(echo $Code)`。ファイルなら`. file.sh`
Stream() {
  (
      Code=$(cat <<- 'EOS'
          K=V
          A=(A B C)
          declare -A AA=([A]=1 [B]=2)
          F() { echo MyFunc; }
      EOS
      )
      . <(echo "$Code")
      set | grep '^K='
      set | grep '^A='
      set | grep '^AA='
      set | grep '^F ()'
      echo "$K"
      echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
      for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo '';
      echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
      for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo '';
      F
  )
}
StreamSubProc() {
  (
      Code=$(cat <<- 'EOS'
          K=V
          A=(A B C)
          declare -A AA=([A]=1 [B]=2)
          F() { echo MyFunc; }
      EOS
      )
      bash -c "$Code"
      [ -n "$(set | grep '^K=')" ] && echo "$K"
      [ -n "$(set | grep '^A=')" ] && echo "A { num: ${#A[@]}, keys: ${!A[@]} }"
      [ -n "$(set | grep '^A=')" ] && { for ((i=0; i<${#A[@]}; i++)); do echo -n "${A[$i]} "; done; echo ''; }
      [ -n "$(set | grep '^AA=')" ] && echo "AA { num: ${#AA[@]}, keys: ${!AA[@]} }"
      [ -n "$(set | grep '^AA=')" ] && { for key in ${!AA[@]}; do echo -n "${AA[$key]} "; done; echo ''; }
      [ -n "$(set | grep '^F ()')" ] && F
  )
}

Run() {
  Funcs='Env Global Local SubProcEnv SubProcGlobal Group Global_Readonly Local_Readonly Stream StreamSubProc'
  for func in $Funcs; do
      echo "========== ${func} =========="
      unset K A AA F
      eval $(echo $func)
      echo "----- global ref -----"
      set | grep '^K='
      set | grep '^A='
      set | grep '^AA='
      set | grep '^F ()'
  done
  unset K A AA F
}
Run

最適解

 どの方法で宣言するのが最適か。

`declare``local`修飾なし
関数外GG
関数内LLG

関数

スコープ コード
環境関数 F() { echo A; }; export -f F;
Global F() { echo A; }

環境変数

export declare; export;
変数 export K=V declare K=V; export K;
配列 export A=(A B C) declare -a A=(A B C); export A;
連想配列 なし declare -A AA=([A]=1 [B]=2); export AA;

シェル変数

 何を優先するかによる。

短く書く

- Env Global Global-Readonly Local Local-Readonly
変数 export K=V K=V readonly K=V local K=V local -r K=V
配列 export A=(A B) A=(A B) readonly A=(A B) local A=(A B) local -r A=(A B)
連想配列 declare -Ag AA=([A]=1 [B]=2); export AA; declare -Ag AA=([A]=1 [B]=2) declare -Agr AA=([A]=1 [B]=2) declare -A AA=([A]=1 [B]=2) declare -Ar AA=([A]=1 [B]=2)

統一する

- Env Global Global-Readonly Local Local-Readonly
変数 declare -g K=V; export K; declare -g K=V declare -r K=V declare K=V declare -r K=V
配列 declare -ag A=(A B); export A; declare -ag A=(A B) declare -agr A=(A B) declare -a A=(A B) declare -ar A=(A B)
連想配列 declare -Ag AA=([A]=1 [B]=2); export AA; declare -Ag AA=([A]=1 [B]=2) declare -Agr AA=([A]=1 [B]=2) declare -A AA=([A]=1 [B]=2) declare -Ar AA=([A]=1 [B]=2)

 長所は以下。

  • 関数の内から外へコピペしてもそのまま動作する
    • localは関数の外へコピペするとエラーになるので使わない
      • bash: local: 関数の中でのみ使用できます
    • -gがないときの挙動が理想的
      • 関数内ならLocalになる
      • 関数外ならGlobalになる
      • 結論
        • 関数内でGlobalを作りたいとき以外-gは使わないほうがコピペ可搬性高い
          • 関数内でGlobalなど作るべきでない(わかりにくくなるため)
  • すぐに変更できる
    • 配列・連想配列にしたいときはフラグ付与すればいい(-a, -A
    • Global化・ReadOnly化したいときはフラグ付与すればいい(-g, -r

 -gフラグがなければ理想的な挙動をする。

declare K=G
F() { declare K=L; echo $K; }
echo $K
F
G
L

 localは関数外だとエラーになる。

local K=G
bash: local: 関数の中でのみ使用できます

 連想配列declareでないと正しく宣言されない。

AA=([A]=10 [B]=20)

 配列とみなされる? 0番目に宣言時最後の要素値20が入る。

$ echo ${#AA[@]}
1
$ echo ${!AA[@]}
0
$ echo ${AA[0]}
20

 「連想配列のときだけはdeclare -Aを使う」など覚えていられない。そこで統一する(全部覚える)という本末転倒な発想が生まれる。

 理想はC言語の宣言みたく素でlocalになってほしいのだが……Shellの宣言は冗長かつわかりづらくなる宿命か。

 未定義の変数があると勝手に宣言したことになって空白値が入る。バグどころか破滅の元。

 以下コードは些細なタイポでhome全ファイル削除されてしまう例。

TmpFile=foo.tmp
rm -fr ~/"$TempFile"

 set -uで変数が未定義のときエラーにしてくれる。

対象環境

  • Raspbierry pi 3 Model B+
  • Raspbian stretch 9.0 2018-11-13
  • bash 4.4.12
$ uname -a
Linux raspberrypi 4.14.98-v7+ #1200 SMP Tue Feb 12 20:27:48 GMT 2019 armv7l GNU/Linux