我读过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
然而,情况似乎并非如此。在bash
shell ( 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
混乱。yy
yyyy
xxx
这里$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
系统变量可能会导致您的会话无法使用!