bash 매개변수 확장 - "패턴"의 *모든* 인스턴스를 "문자열"로 바꾸는 방법은 무엇입니까?

bash 매개변수 확장 - "패턴"의 *모든* 인스턴스를 "문자열"로 바꾸는 방법은 무엇입니까?

내가 읽고"쉘 매개변수 확장"에 대한 GNU 문서다음 구문이 제공됩니다.

${parameter//pattern/string}

파일 이름 확장과 마찬가지로 패턴을 확장하여 패턴을 생성합니다.범위확장되고 가장 긴 일치무늬 그에 대한 값은 다음으로 대체됩니다....두 개의 슬래시로 구분되는 경우범위그리고무늬..., 모든 게임들무늬로 대체됩니다.

"모든 일치"라는 문구가 주어지면무늬로 대체됩니다"위에서 여러 인스턴스가 발생할 것으로 예상했습니다.무늬다음으로 교체 한꺼번에/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, 루프의 한 번의 반복에서 이것이 수행되지 않는 이유는 무엇입니까 ? 왜 3번의 반복이 필요한가요?

답변1

ksh93의 연산자 ${var//pattern/replacement}(zsh, bash 및 mksh에서도 지원됨)는중복 없음패턴의 발생 횟수입니다.

${var//xxx/y}중복되는 항목 중 4개를 대체하도록 변경하면 매우 xxxxxx혼란스러울 것입니다.yyyyyyxxx

이는 $PATH디렉토리 목록을 나타냅니다( ~이 경우 ~현재 작업 디렉토리의 하위 디렉토리 $HOME로 변경하면 오류가 발생합니다).

많은 쉘(csh, tcsh, zsh, fish, yash)은 이를 배열 변수 중 하나에 매핑합니다.

$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:항목이 대체됩니다 :. 그러나 패턴이 주어지면 두 번째 경로는 다음 :과 같은 이유로 실제로 일치하지 않습니다.

:/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:

위에서 볼 수 있듯이 두 번째 경로( /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 루프는 두 번 실행됩니다. 이는 매개변수 확장을 통해 다른 값을 설정하기 path_before전에 변수가 설정 되기 때문입니다.result

답변3

선행 콜론이 너무 많습니다. 다음을 추가하지 말고 시도해 보세요.

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

반복이 필요 없이 모든 항목을 한 번에 제거한다는 것을 알 수 있습니다. PATH시스템 변수를 조작하면 세션을 사용할 수 없게 될 수 있다는 점 에 유의하세요 !

관련 정보