1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-31

実行環境

$ 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文字だけ」削除したい時

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

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

普通のやり方

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

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

動作例
$ 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万回まわるようなループの中でこの関数を呼んだらサブプロセスが雨後の筍のようににょきにょき生えてきて大変なこと2になってしまうのでは…」

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

:cold_sweat:「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

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

注意
削除したい文字が2個以上続く場合には使えません:no_good_tone1:

$ absolutePathSlothfully ///foo/bar/baz # ダメなパターン
///foo/bar/baz

賢そうなやり方

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

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

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

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

動作例
$ 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 - 補足説明 の機能を使ってみたくて書きました:sweat_smile:

:::note info
ほにゃらら
:::
  1. 使い方をよく忘れるのは内緒です🤫

  2. 同時に100万プロセス起動するわけではなく只々生えてきては刈られていくだけなので、実際は特に何も問題ないです。本当ですよ?:rolling_eyes:

  3. これはこれで読みづらいので '/' 以外の文字で文字列置換させてほしいですね。

1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?