[Bash] シェルオプション assoc_expand_once

シェルオプション `assoc_expand_once` を有効にすると、一部の場所で連想配列の添字を 1 回のみ展開するようになります。

Bash バージョン 5.2 以降、一部の場所で 1 回のみ展開するようになりましたが、シェルオプション `assoc_expand_once` の挙動と異なります。

※シェルオプション `assoc_expand_once` は Bash バージョン 5.2 以降でも影響があります。

展開については以下を参照。

参考「[3.5 Shell Expansions - Bash Reference Manual](https://www.gnu.org/software/bash/manual/bash.html#Shell-Expansions)」

※本記事ではパラメータ展開の例を記載しますが、コマンド置換等の他の展開も同様の扱いになります。

# 1. 基本

Bash バージョン 5.2 現在、シェルオプション `assoc_expand_once` の影響を受ける場所は以下の通りです:

- 算術式
    - `let` コマンド
- 組み込みコマンドによる変数代入
    - `printf` コマンド
    - `read` コマンド
    - `wait` コマンド
- 組み込みコマンドによる配列要素の操作
    - `unset` コマンド
    - `test expr` および `[ expr ]` の 条件式の演算子 `-v` の引数

※ Bash バージョン 5.2 で影響を受ける場所は、互換モードのバージョン 5.1 以前でも影響があります。

## 1.1. 算術式

### 1.1.1. `let` コマンド

```bash
# 
function f() {

	local -rAi assoc_array=([x]=23 ['$x']=42)
	local -r x='x'

	local -i y
	let 'y = assoc_array[$x]'

	echo "$y"

}

shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f

# 
function g() {

	local -Ai assoc_array=()
	local -r x='x'

	let 'assoc_array[$x] = 23'

	echo "${!assoc_array[@]}"

}

shopt -u assoc_expand_once
g
shopt -s assoc_expand_once
g
```

```text:実行結果
23
42
x
$x
```

ちなみに、整数属性が設定された変数に対して代入文を用いて代入する場合は、シェルオプション `assoc_expand_once` の影響を受けません。

```bash
# 
function f() {

	local -rAi assoc_array=([x]=23 ['$x']=42)
	local -r x='x'

	local -ri y='assoc_array[$x]'

	echo "$y"

}

unset BASH_COMPAT
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f

BASH_COMPAT=5.0
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
23
23
23
23
23
23
```

## 1.2. 組み込みコマンドによる変数代入

### 1.2.1. `printf` コマンド

```bash
# 
function f() {

	local -A assoc_array=()
	local -r x='x'

	printf -v 'assoc_array[$x]' foo

	echo "${!assoc_array[@]}"

}

shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
x
$x
```

### 1.2.2. `read` コマンド

```bash
# 
function f() {

	local -A assoc_array=()
	local -r x='x'

	read 'assoc_array[$x]' <<< foo

	echo "${!assoc_array[@]}"

}

shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
x
$x
```

### 1.2.3. `wait` コマンド

```bash
# 
function f() {

	local -A assoc_array=()
	local -r x='x'

	: &
	wait -p 'assoc_array[$x]' $!

	echo "${!assoc_array[@]}"

}

shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
x
$x
```

## 1.3. 組み込みコマンドによる配列要素の操作

### 1.3.1. `unset` コマンド

```bash
# 
function f() {

	local -A assoc_array=([x]=foo ['$x']=bar)
	local -r x='x'

	unset 'assoc_array[$x]'

	echo "${!assoc_array[@]}"

}

shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
$x
x
```

### 1.3.2. `test expr` および `[ expr ]` の 条件式の演算子 `-v` の引数

```bash
# 
function f() {

	local -rA assoc_array=([x]=foo)
	local -r x='x'

	test -v 'assoc_array[$x]' && echo 'OK' || echo 'NG'
	[ -v 'assoc_array[$x]' ] && echo 'OK' || echo 'NG'

}

shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
OK
OK
NG
NG
```

# 2. Bash バージョン 5.1 以前

Bash バージョン 5.1 以前、シェルオプション `assoc_expand_once` の影響を受ける場所は以下の通りです:

- 算術式
    - 算術式展開 `$(( expression ))`
    - 算術コマンド `(( expression ))`
    - 算術 `for` 文 `for (( expr1 ; expr2 ; expr3 ))`
    - インデックス配列の添字
    - パラメータ展開の部分列 `${parameter:offset}` および `${parameter:offset:length}`
    - 条件コマンド `[[ expression ]]` の算術演算子の引数
- 組み込みコマンドによる配列要素の操作
    - 条件コマンド `[[ expression ]]` の条件式の演算子 `-v` の引数

※ Bash バージョン 5.2 で影響を受ける場所は、互換モードのバージョン 5.1 以前でも影響があります。

## 2.1. 算術式

### 2.1.1. 算術式展開 `$(( expression ))`

```bash
# 
function f() {

	local -rAi assoc_array=([x]=23 ['$x']=42)
	local -r x='x'

	local -r expression='assoc_array[$x]'
	echo "$(( expression ))"

	local -r subscript='$x'
	echo "$(( assoc_array[$subscript] ))"

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
23
23
42
42
```

### 2.1.2. 算術コマンド `(( expression ))`

```bash
# 
function f() {

	local -rAi assoc_array=([x]=1 ['$x']=0)
	local -r x='x'

	local -r expression='assoc_array[$x]'
	(( expression )) && echo 'OK' || echo 'NG'

	local -r subscript='$x'
	(( assoc_array[$subscript] )) && echo 'OK' || echo 'NG'

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
OK
OK
NG
NG
```

### 2.1.3. 算術 `for` 文 `for (( expr1 ; expr2 ; expr3 ))`

```bash
# 
function f() {

	local -rAi assoc_array=([x]=23 ['$x']=42)
	local -r x='x'

	local -r expression='assoc_array[$x]'
	local -i i
	for (( i = expression; i < 1 + expression; i++ )); do
		echo "$i"
	done
	unset i

	local -r subscript='$x'
	local -i i
	for (( i = assoc_array[$subscript]; i < 1 + assoc_array[$subscript]; i++ )); do
		echo "$i"
	done
	unset i

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
23
23
42
42
```

### 2.1.4. インデックス配列の添字

```bash
# 
function f() {

	local -a indexed_array=(foo bar)

	local -rAi assoc_array=([x]=0 ['$x']=1)
	local -r x='x'

	local -r expression='assoc_array[$x]'
	indexed_array[expression]=baz

	echo "${indexed_array[@]}"

	local -r subscript='$x'
	indexed_array[assoc_array[$subscript]]=baz

	echo "${indexed_array[@]}"

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f

# 
function g() {

	local -a indexed_array=(foo bar)

	local -rAi assoc_array=([x]=0 ['$x']=1)
	local -r x='x'

	local -r expression='assoc_array[$x]'
	echo "${indexed_array[expression]}"

	local -r subscript='$x'
	echo "${indexed_array[assoc_array[$subscript]]}"

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
g
shopt -s assoc_expand_once
g
```

```text:実行結果
baz bar
baz bar
foo baz
foo baz
foo
foo
bar
bar
```

### 2.1.5. パラメータ展開の部分列 `${parameter:offset}` および `${parameter:offset:length}`

```bash
# 
function f() {

	local -r parameter=foo

	local -rAi assoc_array=([x]=0 ['$x']=1)
	local -r x='x'

	local -r offset='assoc_array[$x]'
	echo "${parameter:offset}"

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f

# 
function g() {

	local -r parameter=foo

	local -rAi assoc_array=([x]=3 ['$x']=2)
	local -r x='x'

	local -r length='assoc_array[$x]'
	echo "${parameter:0:length}"

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
g
shopt -s assoc_expand_once
g
```

```text:実行結果
foo
oo
foo
fo
```

### 2.1.6. 条件コマンド `[[ expression ]]` の算術演算子の引数

|条件式の算術演算子|算術式の演算子|意味|
|-|-|-|
|`-eq`|`==`|等値|
|`-ne`|`!=`|非等値|
|`-lt`|`<`|小なり|
|`-le`|`<=`|小なりイコール|
|`-gt`|`>`|大なり|
|`-ge`|`>=`|大なりイコール|

```bash
# 
function f() {

	local -rAi assoc_array=([x]=1 ['$x']=0)
	local -r x='x'

	[[ 'assoc_array[$x]' -ne 0 ]] && echo 'OK' || echo 'NG'

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
OK
NG
```

## 2.2. 組み込みコマンドによる配列要素の操作

### 2.2.1. 条件コマンド `[[ expression ]]` の条件式の演算子 `-v` の引数

```bash
# 
function f() {

	local -rA assoc_array=([x]=foo)
	local -r x='x'

	[[ -v 'assoc_array[$x]' ]] && echo 'OK' || echo 'NG'

}

BASH_COMPAT=5.1
shopt -u assoc_expand_once
f
shopt -s assoc_expand_once
f
```

```text:実行結果
OK
NG
```

# 3. 参考

## 3.1. シェルオプション

参考「[4.3.2 The Shopt Builtin - Bash Reference Manual](https://www.gnu.org/software/bash/manual/bash.html#The-Shopt-Builtin)」

シェルオプション `assoc_expand_once` が有効の場合、連想配列の添字の多重評価を抑制します:

- **算術式**
- 組み込みコマンドによる**変数代入**
- 組み込みコマンドによる**配列の参照先の取得**

## 3.2. NEWS

参考「[NEWS - bash.git - bash](https://git.savannah.gnu.org/cgit/bash.git/tree/NEWS?h=bash-5.2)」(バージョン 5.2)

- bash-5.2
    - シェルオプション `assoc_expand_once` が設定されていない場合でも、組み込みコマンド `unset` は添字の解析や展開を行わずに引数を配列の添字として扱おうとする
    - シェルオプション `assoc_expand_once` が設定されている場合、一部の組み込みコマンドは配列添字を解析しない
        - `printf`
        - `test`
        - `read`
        - `wait`
- bash-5.0
    - 連想配列の添字を 1 回だけ展開しようとする新しいシェルオプション `assoc_expand_once`

## 3.3. COMPAT

参考「[COMPAT - bash.git - bash](https://git.savannah.gnu.org/cgit/bash.git/tree/COMPAT?h=bash-5.2)」(バージョン 5.2)

以前のバージョンとの互換性:

- Bash-5.2 では、あたかもシェル オプション `assoc_expand_once` が設定されているかのように動作することにより、特定の状況下、特に算術評価における配列添字の二重展開を防止しようとする

互換性レベル:

- バージョン 5.1
    - 条件コマンド `[[` の演算子 (例: `[[ -v`) の引数として使用されるインデックス配列および連想配列の添字は複数回展開する
        - Bash-5.2 では、`assoc_expand_once` オプションが有効になっているかのように動作する

## 3.4. CHANGES

参考「[CHANGES - bash.git - bash](https://git.savannah.gnu.org/cgit/bash.git/tree/CHANGES?h=bash-5.2)」(バージョン 5.2)

- bash-5.2-alpha
    - シェルオプション `assoc_expand_once` が設定されていない場合でも、組み込みコマンド `unset` は添字の解析や展開を行わずに引数を配列の添字として扱おうとする
    - シェルオプション `assoc_expand_once` が設定されている場合、一部の組み込みコマンドは配列添字を解析しない
        - `printf`
        - `test`
        - `read`
        - `wait`
- bash-5.1-rc3
    - シェルオプション `assoc_expand_once` は条件式の演算子 `-v` の引数および条件コマンド `[[` の評価に影響する
- bash-5.0-alpha
    - 連想配列の添字を 1 回だけ展開しようとする新しいシェルオプション `assoc_expand_once`

## 3.5. 互換モード

参考「[6.12 Shell Compatibility Mode - Bash Reference Manual](https://www.gnu.org/software/bash/manual/bash.html#Shell-Compatibility-Mode)」

互換性レベル:

- バージョン 5.1
    - 算術式内の展開を複数回行う
        - 算術コマンド `(( expression ))`
        - 算術 `for` 文 `for (( expr1 ; expr2 ; expr3 ))`
        - 条件コマンド `[[ expression ]]` の算術演算子の引数
        - パラメータ展開の部分列 `${parameter:offset}` および `${parameter:offset:length}`
        - 算術式展開 `$(( expression ))`
        - インデックス配列の添字

## 3.6. Changelog

### 3.6.1. バージョン 5.0

参考「[changelog\CWRU - bash.git - bash](https://git.savannah.gnu.org/cgit/bash.git/tree/CWRU/changelog?h=bash-5.0)」(バージョン 5.0)

- 2016-12-12
    - **arrayfunc.c**
        - `assoc_expand_once`: 新しい変数
    - **arrayfunc.h**
        - `assoc_expand_once`
    - **builtins/set.def**
        - `unset_builtin`: `assoc_expand_once`
    - **builtins/shopt.def**
        - `assoc_expand_once`: 新しいシェルオプション
    - **expr.c**
        - `assoc_expand_once`
- 2016-12-14
    - **expr.c**
        - `expr_bind_variable`: `assoc_expand_once`
- 2016-12-15
    - **builtins/read.def**
        - `read_builtin`: `assoc_expand_once`
        - `bind_read_variable`: `assoc_expand_once`
    - **builtins/printf.def**
        - `printf_builtin`: `assoc_expand_once`
        - `bind_printf_variable`: `assoc_expand_once`
- 2018-01-31
    - **doc/{bash.1,bashref.texi}**
        - `shopt`: `assoc_expand_once`
- 2018-09-23
    - **arrayfunc.c**
        - `assign_array_element`: `assoc_expand_once`
- 2018-09-29
    - **arrayfunc.c**
        - `valid_array_reference`: `assoc_expand_once`
- 2018-10-16
    - **expr.c**
        - `expr_skipsubscript`: `assoc_expand_once`

### 3.6.2. バージョン 5.1

参考「[changelog\CWRU - bash.git - bash](https://git.savannah.gnu.org/cgit/bash.git/tree/CWRU/changelog?h=bash-5.1)」(バージョン 5.1)

- 2018-11-10
    - **test.c**
        - `unary_test`: `assoc_expand_once`
            - `[[`
            - `[`

### 3.6.3. バージョン 5.2

参考「[changelog\CWRU - bash.git - bash](https://git.savannah.gnu.org/cgit/bash.git/tree/CWRU/changelog?h=bash-5.2)」(バージョン 5.2)

- 2021-05-06
    - **execute_cmd.c**
        - `execute_cond_command`: `-v`  `unary_test` `assoc_expand_once`
- 2021-05-07
    - **builtins/common.[ch]**
    	- `set_expand_once`: `array_expand_once`
    - **execute_cmd.c**
    	- `execute_cond_node`: `-v` `unary_test` `set_expand_once` `array_expand_once`
- 2021-05-09
    - **builtins/shopt.def**
        - `expand_once_flag`: `assoc_expand_once` `set_assoc_expand`
        - `set_assoc_expand`: `expand_once_flag` `assoc_expand_once`
    - **builtins/common.h**
        - `expand_once_flag`
- 2021-05-11
    - **test.c**
        - `unary_test`: `-v` `valid_array_reference` `assoc_expand_once`
- 2021-11-16
    - **builtins/common.c**
        - `set_expand_once`: `-v` `assoc_expand_once`
            - `[[`
- 2021-12-29
    - **builtins/common.h**
        - `SET_VFLAGS`: `assoc_expand_once`
            - `vflags`: `valid_array_reference`
            - `bindflags`: `assign_array_element` `bind_int_variable`
    - **builtins/printf.def**
        - `printf_builtin`: `SET_VFLAGS` `assoc_expand_once`
    - **builtins/read.def**
        - `read_builtin`: `SET_VFLAGS` `assoc_expand_once`