bash 参数扩展 - 如何用“字符串”替换“模式”的*所有*实例?

bash 参数扩展 - 如何用“字符串”替换“模式”的*所有*实例?

我读过GNU 关于“Shell 参数扩展”的文档给定以下语法:

${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_remove删除。result

然而,情况似乎并非如此。在bashshell ( version 3.2.57) 中,我将其更新$PATH为以下内容:

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

然后,我将上述函数复制/粘贴到我的 shell 中,然后运行它。我看到以下内容:

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是错误的)

许多 shell(csh、tcsh、zsh、fish、yash)将其映射到其数组变量之一。

例如,在 zsh 中,从$PATH(映射到$path数组,如csh)中删除所有出现的目录只需执行以下操作:

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

(或者path=( "${path[@]:#$dir}" )保留空元素,但您不希望 中存在空元素$PATH)。

bash不会这样做,但您可以$PATH使用 split+glob 运算符将其转换为数组:

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

在 bash 中,就像在 ksh93 或 zsh 中一样,${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[*]}"
}

local -,从 Almquist shell 复制来更改函数set -o本地的选项(如 noglob)需要相对较新版本的 bash,不适用于您似乎正在使用的古老 3.2 版本)。


:要通过修改存储在 中的 -separated 列表来删除元素$PATH,您需要为每个$to_remove

  • $to_remove:将开头找到的a 替换为空字符串。
  • :$to_remove:将中间的所有内容(有些可能:与 s 重叠)替换为:
  • 删除:$to_remove末尾的空字符串
  • 如果$PATH仅 contains $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/bar/baz:/foo/bar/baz:/foo/bar/baz:嗡嗡声:

粗体路径是模式的出现位置。

您可以尝试使用以下方法进行测试:

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系统变量可能会导致您的会话无法使用!

相关内容