bashパラメータ拡張 - 「パターン」の*すべての*インスタンスを「文字列」に置き換える方法は?

bashパラメータ拡張 - 「パターン」の*すべての*インスタンスを「文字列」に置き換える方法は?

私が読んで「シェルパラメータ拡張」のGNUドキュメント次の構文が提供されます。

${parameter//pattern/string}

ファイル名拡張と同様に、パターンを拡張してパターンを生成します。範囲拡張され、最も長い一致模様 その値は次のように置き換えられます。ひも...2つのスラッシュで区切られた場合範囲そして模様...、すべてのゲーム模様に置き換えられますひも

「すべての一致」というフレーズが与えられると、模様に置き換えられますひも「上記で複数のインスタンスが発生すると予想しました。模様次に交換ひも 一度に/g、正規表現でグローバル検索と置換コマンドにフラグを使用する方法と同様に、単一の操作で実行されます。

remove_from_path次のように実装されたオープンソースコード(関数)があります。

remove_from_path() {
  local path_to_remove="$1"
  local path_before
  local result=":${PATH//\~/$HOME}:"
  local counter=0
  while [ "$path_before" != "$result" ]; do
    counter+=1
    echo "counter: $counter"
    path_before="$result"
    result="${result//:$path_to_remove:/:}"
  done
  result="${result%:}"
  echo "${result#:}"
}

元のコードにはこのcounter変数は含まれていません。ループが実行される繰り返しの回数を確認するためにこの変数を追加しましたwhile

ご覧のとおり、この行はresult="${result//:$path_to_remove:/:}"GNUドキュメントで説明されているのと同じ二重スラッシュ構文を使用しています。これを考えると、すべてのインスタンスを一度に削除するwhile必要があるため、ループを一度だけ実行したいと思います。path_to_removeresult

しかし、これは本当ではないようです。bashshell()で次のようにversion 3.2.57更新しました。$PATH

bash-3.2$ PATH="/foo/bar/baz:/foo/bar/baz:/foo/bar/baz:buzz"

その後、上記の関数をシェルにコピー/貼り付けて実行しました。私は次を見る:

bash-3.2$ remove_from_path "/foo/bar/baz"
counter: 01
counter: 011
counter: 0111
buzz

ループが3回実行されていることがわかるので、カウンターの増加が期待どおりに機能しないことを無視してくださいwhile${parameter//pattern/string}二重スラッシュ構文がすべての一致を置き換える場合模様そしてひもwhile、ループの1回の繰り返しでこれが行われないのはなぜですか?なぜ3回の繰り返しが必要なのですか?

答え1

ksh93の演算子${var//pattern/replacement}(zsh、bash、mkshでもサポートされています)重複なしパターンの発生回数です。

${var//xxx/y}重複する項目のうち4つを置き換えるように変更すると、非常にxxxxxx混乱します。yyyyyyxxx

これは$PATHディレクトリのリストを表します(~この場合、~現在の作業ディレクトリのサブディレクトリ$HOMEに変更するとエラーが発生します)。

多くのシェル(csh、tcsh、zsh、fish、yash)はそれらを配列変数の1つにマップします。

$PATHたとえば、zshから($pathなどの配列にマップされたcsh)ディレクトリ内のすべてのエントリを削除するには、次の手順を実行します。

path=( ${path:#$dir} )

(またはpath=( "${path[@]:#$dir}" )空の要素を保持しますが、そこに含めたくありません$PATH。)

bashこれは行いませんが、$PATHSplit + glob演算子を使用して配列に変換できます。

set -o noglob
IFS=:
path=( $PATH'' )

ksh93やzshなどのbashでは、${var//pattern/replacement}構文を使用して配列内のすべての要素に適用できますが、それを行うことは"${array[@]//pattern/replacement}"できないため役に立ちません。削除する要素を変更するだけです。

したがって、bash要素に対してのみ繰り返すことができます。

remove_from_PATH() {
  local - IFS=: dir to_remove result
  set -o noglob
  for dir in $PATH''; do
    for to_remove do
      if [[ $dir = "$to_remove" ]]; then
        continue 2
      fi
    done
    result+=( "$dir" )
  done
  PATH="${result[*]}"
}

(noglobなどの関数のローカルlocal -オプションを変更するためにAlmquistシェルからコピーするには、set -o比較的新しいバージョンのbashが必要であり、使用中と思われる古代の3.2バージョンでは機能しません。)


:に保存されている区切りリストを変更して要素を削除するには、$PATH各項目に次のものが必要です$to_remove

  • $to_remove:最初に見つかった項目を空の文字列に置き換えます。
  • :$to_remove:途中のすべての項目(一部は:sと重複する可能性があります)を次に置き換えます。:
  • :$to_remove最後に空の文字列を削除
  • $PATHのみが含まれている場合、$to_remove選択の余地はありません。空の場合は、$PATH現在のディレクトリからコマンドを検索するのが最後なので、望むものではないからです。これはバグでよりよく処理されなければならず、上記のように実際の生活で一般的に発生しない病理学的状況では無視できます。または、検索で何も見つからなかったかどうかを/dev/null確認できます。$PATH

だから:

remove_from_PATH() {
  local to_remove dir newpath="$PATH" prev_newpath
  for to_remove do
    while
      prev_newpath=$newpath
      newpath=${newpath#"$to_remove:"}
      newpath=${newpath%":$to_remove"}
      newpath=${newpath//":$to_remove:"/:}
      [[ $newpath != "$prev_newpath" ]]
    do
      continue
    done
  done
  if [[ -n $newpath ]]; then
    PATH=$newpath
  else
    echo >&2 'Refusing to make $PATH empty'
    return 1
  fi
}

答え2

問題を見つけたようですが、間違っている場合は今お知らせください。

$result変数には次の文字列があります。

:/foo/bar/baz:/foo/bar/baz:/foo/bar/baz:buzz:

適用すると、result="${result//:$path_to_remove:/:}"すべての:/foo/bar/baz:項目が置き換えられます:。ただし、パターンが与えられると、2番目のパスは次:の理由で実際には一致しません。

:/foo/バー/バズ:/foo/bar/baz:/foo/バー/バズ:うなり声:

太字のパスはパターンが現れる場所です。

テストのために、次の方法を試すことができます。

result=':/foo/bar/baz1:/foo/bar/baz2:/foo/bar/baz3:buzz:'
echo "${result//:'/foo/bar/baz'?:/:}"
#Output:
:/foo/bar/baz2:buzz:

上記のように、2番目のパス(/for/bar/baz2)は使用しているモードの影響を受けません。

したがって、パラメータ拡張のために次のことができます。

echo "${r//'/foo/bar/baz':/}" # The firsy ':' in the pattern was removed
#and instead of replace the pattern with ':' I'm replacing with nothing.

したがって、remove_from_path関数は次のようになります。

remove_from_path() {
  local path_to_remove="$1"
  local path_before
  local result=":${PATH//\~/$HOME}:"
  local counter=0
  while [ "$path_before" != "$result" ]; do
    counter+=1
    echo "counter: $counter"
    path_before="$result"
    result="${result//$path_to_remove:/}"
  done
  result="${result%:}"
  echo "${result#:}"
}

ただし、関数のロジックに応じて、whileループは2回実行されます。これは、パラメータ拡張によって他の値を設定するpath_before前に変数が設定されるためです。result

答え3

先行コロンが多すぎます。以下を追加しないでください。

result="/foo/bar/baz:/foo/bar/baz:/foo/bar/baz:buzz"
echo ${result//$path_to_remove:/:}
:::buzz

繰り返しを必要とせずにすべての項目を一度に削除することがわかります。PATHシステム変数を操作するとセッションが利用できなくなる可能性があることに注意してください。

関連情報