実行環境
$ 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.
変数内文字列置換
シェルスクリプトとかワンライナーとかでよくあるアレ1です。
$ 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
$ 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
これの何が嬉しいの?
使いどころ
先頭や末尾の要らない文字を「別のプロセスを生やさずに現在のプロセス内で」「条件分岐やループをせずに」「extglobを有効にせずに」「1文字だけ」削除したい時
例えば次のようなケースを想定してください。
「パスを絶対参照で受け取りたいけど'/'
で始まる文字列が渡ってくる保証は無いよなぁ」
普通のやり方
#/bin/bash
absolutePathCanonically() {
sed -E s%^/*%/%g <<< $1 # 先頭の0個以上の/を1個の/に置換する
}
「こうすれば先頭が '/'
ではない場合でも 0個以上 という条件に当てはまる、完璧でしょ」
$ absolutePathCanonically /usr/local/bin # 先頭に / があっても
/usr/local/bin
$ absolutePathCanonically usr/local/bin # 先頭に / がなくても
/usr/local/bin
$ absolutePathCanonically ///foo/bar/baz # いっぱいあっても
/foo/bar/baz
「ん?もしかしてこの関数って呼ばれるたびに sed
を実行するサブプロセスが起動する?」
「もし100万回まわるようなループの中でこの関数を呼んだらサブプロセスが雨後の筍のようににょきにょき生えてきて大変なこと2になってしまうのでは…」
外部コマンドを使わずに頑張ってみる
「shell-builtinだけでなんとかならないかな?」
#!/bin/bash
absolutePathEffortfully() {
local path=$1
while [[ $path =~ ^/ ]]; do
path=${path:1}
done
echo $path
}
$ absolutePathEffortfully /usr/local/bin # 先頭に / があっても
/usr/local/bin
$ absolutePathEffortfully usr/local/bin # 先頭に / がなくても
/usr/local/bin
$ absolutePathEffortfully ///foo/bar/baz # いっぱいあっても
/foo/bar/baz
😮💨「もし正規表現が使えたら例えば /${path##^/+}
とか ${path/^\/*/\/}
3 みたいな簡潔な書き方もできそうなのに…」
少し変わったやり方
#!/bin/bash
absolutePathSlothfully() {
echo "/${1#/*}"
}
$ absolutePathSlothfully /usr/local/bin # 先頭に / があっても
/usr/local/bin
$ absolutePathSlothfully usr/local/bin # 先頭に / がなくても
/usr/local/bin
「これでええやん」
注意
削除したい文字が2個以上続く場合には使えません
$ absolutePathSlothfully ///foo/bar/baz # ダメなパターン
///foo/bar/baz
賢そうなやり方
「extended pattern matching operator
を有効にしましょう。」
#!/bin/bash
shopt -s extglob
absolutePath() {
echo "/${1##*(/)}"
}
「動作も問題ないです 」
$ absolutePath /usr/local/bin
/usr/local/bin
$ absolutePath usr/local/bin
/usr/local/bin
$ absolutePath ///foo/bar/baz
/foo/bar/baz
まとめ
sed
を使うなら
一般的だし柔軟性もある。普通って素晴らしい。
プロセスを何度も起動することになるので「プロセスIDの桁数が増えて読みづらい」とか細かいことが気になる人は気にするかも?
外部コマンドを使わずに頑張るなら
頑張れば頑張っただけの柔軟性は確保できそうだけど、一般的ではない。正直あまりおすすめしない。
少し変わった使い方なら
シェルプロセス内で簡潔に完結している。
変な使い方なので一般性を求めてはいけない。2文字以上あると使えない。
賢そうな方法なら
どれが正解とかは特に無いと思いますが、これが一番な気がします。
蛇足
この Note - 補足説明 の機能を使ってみたくて書きました
:::note info
ほにゃらら
:::