Bash: ${v:-} と ${v-} は同じだが ${v:-replaced} と ${v-replaced} は少しだけ違う

# 実際にご覧ください

```bash
#!/usr/bin/env bash

set -u  # 未定義変数でエラーにする

v='string'
echo "01. \$v is '$v'"              # > 01. $v is 'string'
echo "02. \$v is '${v:-replaced}'"  # > 02. $v is 'string'
echo "03. \$v is '${v-replaced}'"   # > 03. $v is 'string'
echo "04. \$v is '${v:-}'"          # > 04. $v is 'string'
echo "05. \$v is '${v-}'"           # > 05. $v is 'string'

unset v
# echo "06. \$v is '$v'"            # > v: unbound variable
echo "07. \$v is '${v:-replaced}'"  # > 07. $v is 'replaced'
echo "08. \$v is '${v-replaced}'"   # > 08. $v is 'replaced'
echo "09. \$v is '${v:-}'"          # > 09. $v is ''
echo "10. \$v is '${v-}'"           # > 10. $v is ''

v=''
echo "11. \$v is '$v'"              # > 11. $v is ''
echo "12. \$v is '${v:-replaced}'"  # > 12. $v is 'replaced'
echo "13. \$v is '${v-replaced}'"   # > 13. $v is ''
echo "14. \$v is '${v:-}'"          # > 14. $v is ''
echo "15. \$v is '${v-}'"           # > 15. $v is ''
```

01～05 と 11 はただの基本動作の確認。

06 は `set -u` の効果で意図した通りエラーになるのでコメントアウトしている。
このオプションを切れば `06. $v is ''` となる。

07 と 08、09 と 10、14 と 15 はそれぞれ同じだが、**12 と 13 だけはコロンの有無で結果が違う**ことがわかる。

## 筆者環境

- Windows 10 Home 64-bit 22H2
- Git for Windows 64-bit v2.45.2
  - Git Bash v5.2.26(1)-release


# なぜこうなるか

Bash のマニュアルの日本語訳を見てみる。

- Manpage of BASH - JM Project
  http://surf.ml.seikei.ac.jp/~nakano/JMwww/html/GNU_bash/man1/bash.1.html
  - &gt; [パラメータの展開](http://surf.ml.seikei.ac.jp/~nakano/JMwww/html/GNU_bash/man1/bash.1.html#lbAZ)

関わりのある 2 つの記法について、引用しつつ述べる。

## 1. パラメータ展開によるデフォルト値の設定記法

<blockquote>
<dl>
<dt>${<i>parameter</i><b>:-</b><i>word</i>}</dt>
<dd><b>デフォルトの値を使います</b>。<i>parameter</i> が設定されていないか空文字列であれば、<i>word</i> を展開したものに置換されます。そうでなければ、<i>parameter</i> の値に置換されます。
</dd>
</dl>
</blockquote>

これは `$v` に対する `${v:-replaced}` または `${v:-}` の挙動を説明している。

**変数 `$v`** が**未設定**（未定義）か**空文字列**である場合、`${v:-replaced}` と書くと `:-` より後の内容で置換されて文字列 `replaced` になる。
もちろん、変数 `$v` に空でない文字列が代入されていれば特に何もしない。
`:-` より後の部分は空文字列でも良いので、置換される場合に `${v:-}` と書くと空文字列になる。

```bash:上例から抜粋して再掲
v='string'
echo "02. \$v is '${v:-replaced}'"  # > 02. $v is 'string'
echo "04. \$v is '${v:-}'"          # > 04. $v is 'string'

unset v
echo "07. \$v is '${v:-replaced}'"  # > 07. $v is 'replaced'
echo "09. \$v is '${v:-}'"          # > 09. $v is ''

v=''
echo "12. \$v is '${v:-replaced}'"  # > 12. $v is 'replaced'
echo "14. \$v is '${v:-}'"          # > 14. $v is ''
```

空文字列を偽とする言語において、論理和の短絡評価でデフォルト値を用意するようなものだろう。

```perl:Perl
my $v = 何らかの値;  # 未定義かもしれないし定義済みの偽値かもしれない

$v = $v || 'replaced';  # 定義済みの偽値だとしても置き換わる
$v ||= 'replaced';      # 簡潔な記法
```

```js:JavaScript
let v = 何らかの値;  // 未定義や null かもしれないし定義済みの偽値かもしれない

v = v || 'replaced';  // 定義済みの偽値だとしても置き換わる
v ||= 'replaced';     // 簡潔な記法
```

<!-- また、特に変数 `$v` が未設定の場合、たとえ **`set -u`** (`set -o nounset`) が指定されていても**この置換を行えばエラーにならない**。
（これについてはどこに書かれているのかわからないが…。） -->

で、私は引用元のページで上記の説明を眺めながら「`${v-replaced}` とか `${v-}` みたいな**コロン無しの記法**についてはどこに載ってるんだ…？」と悩んでいた。
コロンありの記法と何が違うのかという以前に、そもそもなぜコロン無しの記法が認められているのかがわからなかった。

答えはその箇所ではなく、ひとつ手前の段落にあった。

## 2. コロンの無い記法

> コロンを省略した場合には設定されているかどうかのみを調べます。

これが、ハイフン `-` の前にコロン `:` がある時と無い時の違いを説明している。

コロン無しで **`${v-replaced}`** と書くと、変数 `$v` が**未設定の場合にのみ文字列 `replaced` への置換を行う**一方、変数 `$v` に**あえて空文字列が代入されている場合には置換を行わない**。
コロンありの `${v:-replaced}` では変数 `$v` が空文字列だったとしても元の値が保持されず置換されてしまうので、わずかな場合分けの差ではあるが**決定的に異なる挙動**をする。

```bash:上例から抜粋して再掲
v='string'
echo "02. \$v is '${v:-replaced}'"  # > 02. $v is 'string'
echo "03. \$v is '${v-replaced}'"   # > 03. $v is 'string'

unset v
echo "07. \$v is '${v:-replaced}'"  # > 07. $v is 'replaced'
echo "08. \$v is '${v-replaced}'"   # > 08. $v is 'replaced'

v=''
echo "12. \$v is '${v:-replaced}'"  # > 12. $v is 'replaced'
echo "13. \$v is '${v-replaced}'"   # > 13. $v is ''
```

`${v-}` と書けば、「未定義なら空文字列に、空文字列なら**元のまま**に、その他何らかの値なら元のままに」と指定していることになる。
比較として、`${v:-}` と書いた時には「未定義なら空文字列に、空文字列なら**新しい空文字列**に、その他何らかの値なら元のままに」と指定していることになるだろう。
結果的には `${v-}` と `${v:-}` は完全に同一となる。

今回の例の 11 以外で空文字列となったものに無理矢理にでも違いを見出すとすれば、13・15 には置換**前**の空文字列が、09・10・14 には置換**後**の空文字列が出力されていると言える。

```bash:上例から抜粋して再掲
unset v
echo "07. \$v is '${v:-replaced}'"  # > 07. $v is 'replaced'
echo "08. \$v is '${v-replaced}'"   # > 08. $v is 'replaced'
echo "09. \$v is '${v:-}'"          # > 09. $v is ''
echo "10. \$v is '${v-}'"           # > 10. $v is ''

v=''
echo "12. \$v is '${v:-replaced}'"  # > 12. $v is 'replaced'
echo "13. \$v is '${v-replaced}'"   # > 13. $v is ''
echo "14. \$v is '${v:-}'"          # > 14. $v is ''
echo "15. \$v is '${v-}'"           # > 15. $v is ''
```

コロン無しの置換は要するにヌル合体演算子のようなものであり、より影響範囲を絞ったデフォルト値の供給方法と思われる。

```perl:Perl
my $v = 何らかの値;  # 未定義かもしれないし定義済みの偽値かもしれない

$v = $v // 'replaced';  # 未定義の場合のみ置き換わる
$v //= 'replaced';      # 簡潔な記法
```

```js:JavaScript
let v = 何らかの値;  // 未定義や null かもしれないし定義済みの偽値かもしれない

v = v ?? 'replaced';  // 未定義か null の場合のみ置き換わる
v ??= 'replaced';     // 簡潔な記法
```


# じゃあコロンの有無はどっちが良いのか

普通のプログラミング的な発想であれば、未設定と空文字列が一緒くたの例外になる `${v:-replaced}` よりは、未設定のみを例外とする `${v-replaced}` の方がより厳密で安全そうだと思う。

しかし Bash は**シェル言語**である以上、シェル特有の文字列取り扱いの癖や使われ方についても考慮せざるを得ない。
例えば次のような対話的な入力待ちをするとして…

```bash
read -p '値を入力して下さい: ' v
```

ユーザーがいきなり Enter を押した場合、それを「何も入力されなかった」としたいか「空文字列が入力された」としたいかと考えると、どちらにも一理ある気がする。

それに、言っちゃ何だが**シェル言語なんてのは小規模な手抜きをするためのもの**でもあるので、その目的に適うなら行儀の良い厳密性より**書き捨てられる雑さ**の方が愛されることも絶対にある。
（スクリプトファイルとしてでなくターミナルに打ち込むコマンドとしてなら特に！）

そう考えると、月並みな結論だが、単にどちらが良い悪いというよりは**それぞれの違いを把握して選択することが大事**だろう。


<!--
…

……

………そうかなぁ。
やっぱ普通に**厳密で安全な方が良い**と思う…。
「選択する」なんて偉そうに言ったけど、現実的には<wbr>**「危険性を認識して使用を避ける」**<wbr>じゃないか…？
-->

ただ、`${v:-}` と `${v-}` については単純に短さで後者の勝ち。


# まとめ

仰々しく図や表に起こすならこんな感じか。

```mermaid
flowchart LR
  str{{"<code>#quot;${v:-replaced}#quot;</code>\n"}} --> if{"<code>$v</code> が…"}
  if -- 未設定 --> rpl["<code>'replaced'</code>"]
  if -- 空文字列 --> rpl
  if -- 空でない文字列 --> orig["<code>#quot;$v#quot;</code>\n（元のまま）"]
```

```mermaid
%%{ init: { 'theme': 'forest' } }%%
flowchart LR
  str{{"<code>#quot;${v-replaced}#quot;</code>\n（コロンの無い記法）"}} --> if{"<code>$v</code> が…"}
  if -- 未設定 --> rpl["<code>'replaced'</code>"]
  if -- 空文字列 --> orig
  if -- 空でない文字列 --> orig["<code>#quot;$v#quot;</code>\n（元のまま）"]
```

| 元の `$v` | `${v:-replaced}` | `${v-replaced}` | `${v:-}` | `${v-}` |
|:-:|:-:|:-:|:-:|:-:|
| 未設定<br>（未定義） | `'replaced'` | `'replaced'` | `''` | `''` |
| 空文字列<br>(`''`) | `'replaced'` | `''` | `''` | `''` |
| 空でない<br>文字列 | `$v` | `$v` | `$v` | `$v` |
| 評価 | :arrow_right: より曖昧 | :arrow_left: より厳密 | :arrow_right: より<br>長いだけ | :arrow_left: より<br>短い |


# 余談：なんでこんな記事を書いたか

Git for Windows の `git-prompt.sh` を見ていたらコロン無しの方が出てきて、知らない記法だったから。

- git/contrib/completion/git-prompt.sh at v2.45.2.windows.1 · git-for-windows/git
  https://github.com/git-for-windows/git/blob/v2.45.2.windows.1/contrib/completion/git-prompt.sh
  - &gt; [144 行目](https://github.com/git-for-windows/git/blob/v2.45.2.windows.1/contrib/completion/git-prompt.sh#L144)等

シェルの変数置換記法、ググりにくすぎ…。

&ZeroWidthSpace;

おわり