Bashの変数内文字列置換の少し変わった使い方

## 実行環境

```shell-session
$ bash --version
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
```

## 変数内文字列置換

シェルスクリプトとかワンライナーとかでよくあるアレ[^often-forgotten-stuff]です。
[^often-forgotten-stuff]:使い方をよく忘れるのは内緒です🤫

```shell-session:普通の使い方
$ foo=1/2/3/4/5
$ echo ${foo/[0-9]/x}  # [0-9] に最初に一致する部分が置換される
x/2/3/4/5
$ echo ${foo//[0-9]/x} # [0-9] に一致する部分が全て置換される
x/x/x/x/x
$ echo ${foo#*/}       # 先頭から */ に最短一致する部分が削除される
2/3/4/5
$ echo ${foo##*/}      # 先頭から */ に最長一致する部分が削除される
5
$ echo ${foo%/*}       # 末尾から /* に最短一致する部分が削除される
1/2/3/4
$ echo ${foo%%/*}      # 末尾から /* に最長一致する部分が削除される
1
```

```shell-session:少し変わった使い方
$ bar=/$foo      # bar=/1/2/3/4/5 (先頭に / が付いている)
$ echo ${foo#/*} # 変化なし
1/2/3/4/5
$ echo ${bar#/*} # 先頭の / が削除される
1/2/3/4/5
$ baz=$foo/      # baz=1/2/3/4/5/ (末尾に / が付いている)
$ echo ${foo%*/} # 変化なし
1/2/3/4/5
$ echo ${baz%*/} # 末尾の / が削除される
1/2/3/4/5
```

### これの何が嬉しいの？

:::note info
使いどころ
先頭や末尾の要らない文字を「__別のプロセスを生やさずに現在のプロセス内で__」「__条件分岐やループをせずに__」「__extglobを有効にせずに__」「__1文字だけ__」削除したい時
:::

例えば次のようなケースを想定してください。

:thinking: 「パスを絶対参照で受け取りたいけど`'/'`で始まる文字列が渡ってくる保証は無いよなぁ」

#### 普通のやり方

```bash:普通のやり方
#/bin/bash
absolutePathCanonically() {
  sed -E s%^/*%/%g <<< $1 # 先頭の0個以上の/を1個の/に置換する
}
```

:blush:「こうすれば先頭が `'/'` ではない場合でも __0個以上__ という条件に当てはまる、完璧でしょ」

```shell-session:動作例
$ absolutePathCanonically /usr/local/bin # 先頭に / があっても
/usr/local/bin
$ absolutePathCanonically usr/local/bin  # 先頭に / がなくても
/usr/local/bin
$ absolutePathCanonically ///foo/bar/baz # いっぱいあっても
/foo/bar/baz
```

:fearful:「ん？もしかしてこの関数って呼ばれるたびに `sed` を実行するサブプロセスが起動する？」
:scream:「もし100万回まわるようなループの中でこの関数を呼んだらサブプロセスが雨後の筍のようににょきにょき生えてきて大変なこと[^non-critical-issue]になってしまうのでは…」
[^non-critical-issue]:同時に100万プロセス起動するわけではなく只々生えてきては刈られていくだけなので、実際は特に何も問題ないです。本当ですよ？:rolling_eyes:

#### 外部コマンドを使わずに頑張ってみる

:cold_sweat:「shell-builtinだけでなんとかならないかな？」

```bash:なんとかなってる…？
#!/bin/bash
absolutePathEffortfully() {
  local path=$1
  while [[ $path =~ ^/ ]]; do
    path=${path:1}
  done
  echo $path
}
```

```shell-session:動作例
$ absolutePathEffortfully /usr/local/bin # 先頭に / があっても
/usr/local/bin
$ absolutePathEffortfully usr/local/bin  # 先頭に / がなくても
/usr/local/bin
$ absolutePathEffortfully ///foo/bar/baz # いっぱいあっても
/foo/bar/baz
```

😮‍💨「もし正規表現が使えたら例えば `/${path##^/+}` とか `${path/^\/*/\/}`[^low-readability] みたいな簡潔な書き方もできそうなのに…」
[^low-readability]:これはこれで読みづらいので `'/'` 以外の文字で文字列置換させてほしいですね。

#### 少し変わったやり方

```bash:少し変わったやり方
#!/bin/bash
absolutePathSlothfully() {
  echo "/${1#/*}"
}
```

```shell-session:動作例
$ absolutePathSlothfully /usr/local/bin # 先頭に / があっても
/usr/local/bin
$ absolutePathSlothfully usr/local/bin  # 先頭に / がなくても
/usr/local/bin
```

:stuck_out_tongue_winking_eye:「これでええやん:bulb:」

:::note alert
__注意__
削除したい文字が2個以上続く場合には使えません:no_good_tone1:
```shell-session
$ absolutePathSlothfully ///foo/bar/baz # ダメなパターン
///foo/bar/baz
```
:::

#### 賢そうなやり方

:nerd:「`extended pattern matching operator` を有効にしましょう。」

```bash:extended pattern matching operatorを使うやり方
#!/bin/bash
shopt -s extglob

absolutePath() {
  echo "/${1##*(/)}"
}
```

:ok_woman_tone1:「動作も問題ないです :white_check_mark:」

```shell-session:動作例
$ absolutePath /usr/local/bin
/usr/local/bin
$ absolutePath usr/local/bin
/usr/local/bin
$ absolutePath ///foo/bar/baz
/foo/bar/baz
```

## まとめ

#### `sed` を使うなら

:::note info
一般的だし柔軟性もある。普通って素晴らしい。
:::

:::note warn
プロセスを何度も起動することになるので「プロセスIDの桁数が増えて読みづらい」とか細かいことが気になる人は気にするかも？
:::

#### 外部コマンドを使わずに頑張るなら

:::note warn
頑張れば頑張っただけの柔軟性は確保できそうだけど、一般的ではない。正直あまりおすすめしない。
:::

#### 少し変わった使い方なら

:::note info
シェルプロセス内で簡潔に完結している。
:::

:::note warn
変な使い方なので一般性を求めてはいけない。2文字以上あると使えない。
:::

#### 賢そうな方法なら

:::note info
どれが正解とかは特に無いと思いますが、これが一番な気がします。
:::

---

## 蛇足

この [Note - 補足説明][note-url] の機能を使ってみたくて書きました:sweat_smile:

```markdown
:::note info
ほにゃらら
:::
```

[note-url]:https://qiita.com/Qiita/items/c686397e4a0f4f11683d#note---%E8%A3%9C%E8%B6%B3%E8%AA%AC%E6%98%8E