Bash:用于管理类 PATH 变量的防弹单行代码
Bash: Bullet-proof one-liners for managing PATH-like variables
我正在寻找四个简单的、防弹、尽可能短的Bash函数:
PrependPath name path [path ...]
AppendPath name path [path ...]
RemovePath name path [path ...]
UniquePath name
那分别是:
- 将路径添加到
name
指定的 Bash 变量,如果这些路径尚未包含在该变量的某处,
- 将路径附加到
name
指定的 Bash 变量,如果这些路径尚未包含在该变量的某处,
- 从 Bash 变量中删除给定路径的所有实例,该变量由
name
、 指定
- 过滤掉变量中除了第一个(最左边)出现的所有重复项。
例如(我没有忘记任何 $
符号):
export PATH=/bin:/usr/bin:/bin
PrependPath PATH /usr/local/bin # --> PATH=/usr/local/bin:/bin:/usr/bin:/bin
AppendPath PATH /usr/local/bin # --> PATH=/usr/local/bin:/bin:/usr/bin:/bin (nothing changed, as was already there from PrependPath)
RemovePath PATH /usr/local/bin # --> PATH=/bin:/usr/bin:/bin
UniquePath PATH # --> PATH=/bin:/usr/bin
一旦我们有了这些函数,我们就可以将它们用于遵循与 PATH
变量相同(或非常相似的规则)的任何路径变量,例如 LD_LIBRARY_PATH
、LIBRARY_PATH
, INFOPATH
, ...
困难的是,我希望这些函数适用于 绝对可以在 PATH
中出现的每个 合法路径,这基本上消除了字符 [= 26=]
、/
和 :
,但允许 所有 其他字符(假设它是有效编码的 UTF-8),包括特定的空格和换行符。
我想将 "no-one in their right mind would ever use a path like that" 的哲学讨论排除在外。如果我可以合法地创建这样的路径,并且将其放入 PATH
中是合法的,那么它就是公平的游戏。
更多注意事项:
- 除了 exporting/changing 指定的路径变量之外,该函数不应以 任何 方式修改父环境(例如向其添加 functions/variables)。 =119=]
- 该函数不应等同于
/usr/
和 /usr
之类的路径,它们的区别仅在于斜杠。
- 该函数应该正确处理空路径(指定当前目录),如果它们不存在则不会意外地将它们添加到路径变量中,如果它们存在则不会意外删除它们 已经存在了。
- 即使
PATH
为 空 ,该函数也应该工作。不能排除这种情况是不可能的。
我创建了一个单元测试脚本,它体现了我认为对四个函数的行为的完全合理和常识性的期望(我想避免外部 link):
function PrependPath() { TODO }
function AppendPath() { TODO }
function RemovePath() { TODO }
function UniquePath() { TODO }
function Assert() { ((i++)); [[ "$TESTPATH" == "" ]] && echo "$i) SUCCESS" || echo "$i) FAILURE => Got >$TESTPATH<"; }
i=0
echo "Test: PrependPath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /usr/sbin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /bin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /usr/slash
Assert "/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /new
Assert "/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /new
Assert "/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH ""
Assert ":/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH ""
Assert ":/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH "/usr"
Assert "/usr::/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
TESTPATH=
PrependPath TESTPATH /multiple /new
Assert "/new:/multiple"
PrependPath TESTPATH /and /new /foo /and
Assert "/foo:/and:/new:/multiple"
TESTPATH=":/foo:/bar:"
PrependPath TESTPATH /bar
Assert ":/foo:/bar:"
TESTPATH="/foo:/bar:"
PrependPath TESTPATH ""
Assert "/foo:/bar:"
TESTPATH=":/foo:/bar"
PrependPath TESTPATH ""
Assert ":/foo:/bar"
TESTPATH="/foo::/bar"
PrependPath TESTPATH ""
Assert "/foo::/bar"
TESTPATH=$'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
PrependPath TESTPATH /ano
Assert $'/ano:/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
PrependPath TESTPATH $'/te st\nnew/foo'
Assert $'/ano:/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
PrependPath TESTPATH $'/ne w\ner\n'
Assert $'/ne w\ner\n:/ano:/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
TESTPATH="/foo:/bar"
PrependPath TESTPATH "/b.r" "/fo+" "/fo*" "/b[a]r" "\foo" "/food?"
Assert "/food?:\foo:/b[a]r:/fo*:/fo+:/b.r:/foo:/bar"
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
PrependPath TESTPATH "/a[d" "/mor(e" "/other{" "/ano"
Assert $'/ano:/other{::/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
TESTPATH=$'\new'
PrependPath TESTPATH "\new"
Assert $'\new:\new'
TESTPATH=
PrependPath TESTPATH "/foo"
Assert "/foo"
PrependPath TESTPATH ""
Assert ":/foo"
TESTPATH=$'\n:/foo:/bar:\n'
PrependPath TESTPATH "/bar"
Assert $'\n:/foo:/bar:\n'
PrependPath TESTPATH $'\n'
Assert $'\n:/foo:/bar:\n'
PrependPath TESTPATH "/new"
Assert $'/new:\n:/foo:/bar:\n'
PrependPath TESTPATH ""
Assert $':/new:\n:/foo:/bar:\n'
TESTPATH=$':/foo:/bar:\n\n'
PrependPath TESTPATH "/bar"
Assert $':/foo:/bar:\n\n'
PrependPath TESTPATH $'\n'
Assert $'\n::/foo:/bar:\n\n'
PrependPath TESTPATH $'\n\n'
Assert $'\n::/foo:/bar:\n\n'
PrependPath TESTPATH "/new"
Assert $'/new:\n::/foo:/bar:\n\n'
TESTPATH=
PrependPath TESTPATH ""
Assert ":"
PrependPath TESTPATH ""
Assert ":"
PrependPath TESTPATH /new
Assert "/new:"
PrependPath TESTPATH ""
Assert "/new:"
TESTPATH=":/bin:"
PrependPath TESTPATH ""
Assert ":/bin:"
PrependPath TESTPATH "/foo"
Assert "/foo::/bin:"
TESTPATH="::"
PrependPath TESTPATH ""
Assert "::"
PrependPath TESTPATH "/foo"
Assert "/foo:::"
TESTPATH=":::"
PrependPath TESTPATH ""
Assert ":::"
PrependPath TESTPATH "/foo"
Assert "/foo::::"
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /usr/sbin /bin /usr/slash /new /foo /new
Assert "/foo:/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PATH="$ORIGPATH"
i=0
echo
echo "Test: AppendPath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /usr/sbin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /bin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /usr/slash
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash"
AppendPath TESTPATH /new
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new"
AppendPath TESTPATH /new
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new"
AppendPath TESTPATH ""
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new:"
AppendPath TESTPATH ""
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new:"
AppendPath TESTPATH "/usr"
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new::/usr"
TESTPATH=
AppendPath TESTPATH /multiple /new
Assert "/multiple:/new"
AppendPath TESTPATH /and /new /foo /and
Assert "/multiple:/new:/and:/foo"
TESTPATH=":/foo:/bar:"
AppendPath TESTPATH /bar
Assert ":/foo:/bar:"
TESTPATH="/foo:/bar:"
AppendPath TESTPATH ""
Assert "/foo:/bar:"
TESTPATH=":/foo:/bar"
AppendPath TESTPATH ""
Assert ":/foo:/bar"
TESTPATH="/foo::/bar"
AppendPath TESTPATH ""
Assert "/foo::/bar"
TESTPATH=$'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
AppendPath TESTPATH /ano
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo::/ano'
AppendPath TESTPATH $'/te st\nnew/foo'
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo::/ano'
AppendPath TESTPATH $'/ne w\ner\n'
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo::/ano:/ne w\ner\n'
TESTPATH="/foo:/bar"
AppendPath TESTPATH "/b.r" "/fo+" "/fo*" "/b[a]r" "/bar" "\foo" "/food?"
Assert "/foo:/bar:/b.r:/fo+:/fo*:/b[a]r:\foo:/food?"
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
AppendPath TESTPATH "/a[d" "/mor(e" "/other{" "/ano"
Assert $':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e::/other{:/ano'
TESTPATH=$'\new'
AppendPath TESTPATH "\new"
Assert $'\new:\new'
TESTPATH=
AppendPath TESTPATH "/foo"
Assert "/foo"
AppendPath TESTPATH ""
Assert "/foo:"
TESTPATH=$'\n:/foo:/bar:\n'
AppendPath TESTPATH "/bar"
Assert $'\n:/foo:/bar:\n'
AppendPath TESTPATH $'\n'
Assert $'\n:/foo:/bar:\n'
AppendPath TESTPATH "/new"
Assert $'\n:/foo:/bar:\n:/new'
AppendPath TESTPATH ""
Assert $'\n:/foo:/bar:\n:/new:'
TESTPATH=$':/foo:/bar:\n\n'
AppendPath TESTPATH "/bar"
Assert $':/foo:/bar:\n\n'
AppendPath TESTPATH $'\n'
Assert $':/foo:/bar:\n\n:\n'
AppendPath TESTPATH $'\n\n'
Assert $':/foo:/bar:\n\n:\n'
AppendPath TESTPATH "/new"
Assert $':/foo:/bar:\n\n:\n:/new'
TESTPATH=
AppendPath TESTPATH ""
Assert ":"
AppendPath TESTPATH ""
Assert ":"
AppendPath TESTPATH /new
Assert ":/new"
AppendPath TESTPATH ""
Assert ":/new"
TESTPATH=":/bin:"
AppendPath TESTPATH ""
Assert ":/bin:"
AppendPath TESTPATH "/foo"
Assert ":/bin::/foo"
TESTPATH="::"
AppendPath TESTPATH ""
Assert "::"
AppendPath TESTPATH "/foo"
Assert ":::/foo"
TESTPATH=":::"
AppendPath TESTPATH ""
Assert ":::"
AppendPath TESTPATH "/foo"
Assert "::::/foo"
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /usr/sbin /bin /usr/slash /new /foo /new
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new:/foo"
PATH="$ORIGPATH"
i=0
echo
echo "Test: RemovePath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/"
RemovePath TESTPATH /usr/sbin
Assert "/usr/bin:/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/:/foo"
RemovePath TESTPATH /usr/bin /usr/sbin /foo
Assert "/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:'
RemovePath TESTPATH /and
Assert $':/te st\nnew/foo:/ano\nth er:/more:'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
RemovePath TESTPATH $'/te st\nnew/foo'
Assert $':/and:/ano\nth er:/more:'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:'
RemovePath TESTPATH ""
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:'
RemovePath TESTPATH "/a.d" "\and" "/andy?" $'/te st\nnew/fo+' "/an*"
Assert $':/te st\nnew/foo:/and:/ano\nth er:/more:'
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
RemovePath TESTPATH "/a[d" "/mor(e" "/other{" "/ano"
Assert $':/te[ st\nnew/foo:/ano\nth er:'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:\n:/more:'
RemovePath TESTPATH "/sp ace" $'/new\nline' $'\n' "" $'/ano\nth er'
Assert $'/te st\nnew/foo:/and:/more'
TESTPATH="\no:\newline"
RemovePath TESTPATH $'\no'
Assert "\no:\newline"
RemovePath TESTPATH "\no"
Assert "\newline"
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH ""
Assert $'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH "\n"
Assert $'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH $'\n\n'
Assert $'\n:/more:/of:\n:/th is:\n'
RemovePath TESTPATH $'\n'
Assert $'/more:/of:/th is'
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH $'\n'
Assert $'/more:\n\n:/of:/th is:\n\n'
RemovePath TESTPATH /of
Assert $'/more:\n\n:/th is:\n\n'
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is::\n'
RemovePath TESTPATH $'\n'
Assert $'/more:\n\n:/of:/th is:'
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is::\n'
RemovePath TESTPATH ""
Assert $'\n:/more:\n\n:/of:\n:/th is:\n'
TESTPATH=":::"
RemovePath TESTPATH "/foo"
Assert ":::"
RemovePath TESTPATH ""
Assert ""
TESTPATH="::"
RemovePath TESTPATH "/foo"
Assert "::"
RemovePath TESTPATH ""
Assert ""
TESTPATH=":"
RemovePath TESTPATH "/foo"
Assert ":"
RemovePath TESTPATH ""
Assert ""
TESTPATH="::/foo"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH="::/foo"
RemovePath TESTPATH ""
Assert "/foo"
TESTPATH=":/foo"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH=":/foo"
RemovePath TESTPATH ""
Assert "/foo"
RemovePath TESTPATH "/foo"
Assert ""
TESTPATH="/foo::"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH="/foo::"
RemovePath TESTPATH ""
Assert "/foo"
TESTPATH="/foo:"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH="/foo:"
RemovePath TESTPATH ""
Assert "/foo"
TESTPATH=""
RemovePath TESTPATH ""
Assert ""
RemovePath TESTPATH "/foo"
Assert ""
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/"
RemovePath TESTPATH /usr/sbin /bin /foo
Assert "/usr/bin:/usr/sbin/tmp:/usr/sbin/"
PATH="$ORIGPATH"
i=0
echo
echo "Test: UniquePath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/:/usr/sbin:/bin"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH=":/foo:/bar:/foo"
UniquePath TESTPATH
Assert ":/foo:/bar"
TESTPATH="/foo:/bar:/foo:"
UniquePath TESTPATH
Assert "/foo:/bar:"
TESTPATH=":/foo:/bar:/foo:"
UniquePath TESTPATH
Assert ":/foo:/bar"
TESTPATH="/foo:/bar::/foo"
UniquePath TESTPATH
Assert "/foo:/bar:"
TESTPATH="/foo:/bar::/foo:"
UniquePath TESTPATH
Assert "/foo:/bar:"
TESTPATH=$':/te st\nnew/foo:/ano:/ano\nth er:th er::/more:/te st\nnew/foo:'
UniquePath TESTPATH
Assert $':/te st\nnew/foo:/ano:/ano\nth er:th er:/more'
TESTPATH=$'/te st\nnew/foo:/and:/ano\nth er:/more:/a.d:\and:/andy?:/te st\nnew/fo+:/an*:/more'
UniquePath TESTPATH
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/a.d:\and:/andy?:/te st\nnew/fo+:/an*'
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:/mor(e:/other{:/ano:/a[d'
UniquePath TESTPATH
Assert $':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:/other{:/ano'
TESTPATH=$'\no:\newline:\no:\newline:\no'
UniquePath TESTPATH
Assert $'\no:\newline:\no:\newline'
TESTPATH="/foo:/foo"
UniquePath TESTPATH
Assert "/foo"
UniquePath TESTPATH
Assert "/foo"
TESTPATH=":"
UniquePath TESTPATH
Assert ":"
TESTPATH="::"
UniquePath TESTPATH
Assert ":"
TESTPATH=":::"
UniquePath TESTPATH
Assert ":"
TESTPATH=$'\n:/more::/of:\n:/th is:\n\n:\n'
UniquePath TESTPATH
Assert $'\n:/more::/of:/th is:\n\n'
TESTPATH=$':/more:/of::\n\n::/th is:\n\n:\n'
UniquePath TESTPATH
Assert $':/more:/of:\n\n:/th is:\n'
TESTPATH=$'/foo:\n:/foo'
UniquePath TESTPATH
Assert $'/foo:\n'
TESTPATH=$'/foo::'
UniquePath TESTPATH
Assert $'/foo:'
TESTPATH=
UniquePath TESTPATH
Assert ""
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/:/bin"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
PATH="$ORIGPATH"
如果您的函数通过了这些测试中的每一项,并且没有其他人能够发现我没有在脚本中检查过的缺陷,那么您就完全回答了我的问题。
自然地,我在 Stack Overflow 上尝试过的任何其他相关问题的答案中 none 为我提供了解决此问题的方法 (1, 2, 3, 4, 5, 6, 7, 8, 9)。也许我漏掉了一个,但鉴于我正在寻找我指定的通用级别的所有四个函数,这仍然不能完全回答我的问题。
我试过的
使用字符串指定要修改的变量很容易使用间接扩展来处理,例如如果 </code> 是 <code>PATH
那么 ${!1}
就是 $PATH
。 Setting/exporting 可以使用 export ""=XXX
间接完成变量。在最终解决方案中,可能有更多 efficient/elegant 方法来处理多个输入,但作为后备解决方案,您始终可以只对它们使用 for 循环,因此任务可以减少到只需要四个函数来执行使用单个参数对 PATH
变量执行所需的操作。
使用变量的local
关键字可以实现不污染父命名空间,但是子函数最多只能是unset -f
,这并不能真正满足不可能改变父命名空间的要求环境。附加到 PATH
变量可以使用替代参数扩展巧妙地完成,即类似 PATH="${PATH:+${PATH}:}/new/path"
的东西。前置就像 PATH="/new/path${PATH:+:${PATH}}"
.
这些表达式可以正确地处理是否添加冒号分隔符 几乎 所有的时间,并且对于它们不存在的特殊情况,可能可以添加一个简单的条件来修复视情况而定。有了所有这些构建块,我的问题的症结在于如何以防弹的方式稳健地分割路径,以便能够检查某个路径是否已经存在。我的尝试之一是只搜索字符串而不提取组件:
function AppendPath() { echo "$PATH" | grep -Eq "(^|:)($|:)" || export PATH="${PATH:+:${PATH}}"; }
但这失败了,因为 </code> 的内容被解释为正则表达式。此外,当 <code>PATH
为空时,对 grep 的调用会失败,这意味着需要像 /bin/grep
这样的绝对路径,但这并不是特别便携。对于 RemovePath
,我尝试使用空字符作为分隔符并进行反向 grep,例如类似于:
function RemovePath() { __path="$(echo -n "$PATH" | tr ':' '[=15=]' | grep -zxvF "" | tr '[=15=]' ':')"; export PATH="${__path%:}"; }
我在 grep
上使用 -F
以确保它不会被视为正则表达式,用 -x
匹配整行,用 -z
使用空分隔符, 并用 -v
反转匹配。除了某些极端情况和空 PATH
情况的明显问题外,grep -zF
仍然在每个换行符处划分搜索模式,尽管使用了空定界符,这意味着尝试删除 $'/foo\nbar'
将取而代之只需删除 /foo
和 bar
的所有实例。命令替换 $()
的一个偷偷摸摸的问题也在这里浮出水面,它会截断所有尾随的换行符。尝试:
echo -n "$(echo -n $'Hey\nthere\n\n\n')"
我尝试使用 IFS=:
将 PATH
分解成一个数组,但除其他外,命令替换是我在尝试用 "$(IFS=:; echo -n "${pathparts[*]}")"
之类的东西重新组装它时再次卡住的地方.现在我正在研究基于 read
命令和手动循环的 bash 内置想法,以避免命令替换,grep
和空 PATH
问题。我实际上想避免用很多不起作用的东西来污染我的问题,但这是被要求的,我希望至少现在人们相信我在发布之前已经认真对待了这个问题。
这是一个解决方案,在我的测试范围内,在管理 PATH
类变量的每种情况下总是会做正确的事情:
# Example: PrependPath PATH /usr/local/cuda/bin
function PrependPath() {
local __args __item
for __args in "${@:2}"; do
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
[[ "$__item" == "$__args" ]] && continue 2
done <<< "${!1}:"
([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args+=':'
export ""="$__args${!1}"
done
}
# Example: AppendPath PATH /usr/local/cuda/bin
function AppendPath() {
local __args __item
for __args in "${@:2}"; do
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
[[ "$__item" == "$__args" ]] && continue 2
done <<< "${!1}:"
([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args=":$__args"
export ""="${!1}$__args"
done
}
# Example: RemovePath INFOPATH /usr/local/texlive/2019/texmf-dist/doc/info
function RemovePath() {
local __item __args __path=
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
for __args in "${@:2}"; do
[[ "$__item" == "$__args" ]] && continue 2
done
__path="$__path:$__item"
done <<< "${!1}:"
[[ "$__path" != ":" ]] && __path="${__path#:}"
export ""="$__path"
}
# Example: UniquePath LD_LIBRARY_PATH
function UniquePath() {
local __item __args __path= __seen=()
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
for __args in "${__seen[@]}"; do
[[ "$__item" == "$__args" ]] && continue 2
done
__seen+=("$__item")
__path="$__path:$__item"
done <<< "${!1}:"
[[ "$__path" != ":" ]] && __path="${__path#:}"
export ""="$__path"
}
解决问题的关键是使用read
以:
作为分隔符,并且只使用bash循环到check/assemble所需的输出路径, 没有命令替换, grep
, awk
, sed
或任何其他外部命令。
汇总到 'one-liners' 以便可以轻松地在脚本、命令行和 .bashrc
中使用它们:
# Functions to manage arbitrary PATH-like variables
function PrependPath() { local __args __item; for __args in "${@:2}"; do [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do [[ "$__item" == "$__args" ]] && continue 2; done <<< "${!1}:"; ([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args+=':'; export ""="$__args${!1}"; done; }
function AppendPath() { local __args __item; for __args in "${@:2}"; do [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do [[ "$__item" == "$__args" ]] && continue 2; done <<< "${!1}:"; ([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args=":$__args"; export ""="${!1}$__args"; done; }
function RemovePath() { local __item __args __path=; [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do for __args in "${@:2}"; do [[ "$__item" == "$__args" ]] && continue 2; done; __path="$__path:$__item"; done <<< "${!1}:"; [[ "$__path" != ":" ]] && __path="${__path#:}"; export ""="$__path"; }
function UniquePath() { local __item __args __path= __seen=(); [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do for __args in "${__seen[@]}"; do [[ "$__item" == "$__args" ]] && continue 2; done; __seen+=("$__item"); __path="$__path:$__item"; done <<< "${!1}:"; [[ "$__path" != ":" ]] && __path="${__path#:}"; export ""="$__path"; }
我正在寻找四个简单的、防弹、尽可能短的Bash函数:
PrependPath name path [path ...]
AppendPath name path [path ...]
RemovePath name path [path ...]
UniquePath name
那分别是:
- 将路径添加到
name
指定的 Bash 变量,如果这些路径尚未包含在该变量的某处, - 将路径附加到
name
指定的 Bash 变量,如果这些路径尚未包含在该变量的某处, - 从 Bash 变量中删除给定路径的所有实例,该变量由
name
、 指定
- 过滤掉变量中除了第一个(最左边)出现的所有重复项。
例如(我没有忘记任何 $
符号):
export PATH=/bin:/usr/bin:/bin
PrependPath PATH /usr/local/bin # --> PATH=/usr/local/bin:/bin:/usr/bin:/bin
AppendPath PATH /usr/local/bin # --> PATH=/usr/local/bin:/bin:/usr/bin:/bin (nothing changed, as was already there from PrependPath)
RemovePath PATH /usr/local/bin # --> PATH=/bin:/usr/bin:/bin
UniquePath PATH # --> PATH=/bin:/usr/bin
一旦我们有了这些函数,我们就可以将它们用于遵循与 PATH
变量相同(或非常相似的规则)的任何路径变量,例如 LD_LIBRARY_PATH
、LIBRARY_PATH
, INFOPATH
, ...
困难的是,我希望这些函数适用于 绝对可以在 PATH
中出现的每个 合法路径,这基本上消除了字符 [= 26=]
、/
和 :
,但允许 所有 其他字符(假设它是有效编码的 UTF-8),包括特定的空格和换行符。
我想将 "no-one in their right mind would ever use a path like that" 的哲学讨论排除在外。如果我可以合法地创建这样的路径,并且将其放入 PATH
中是合法的,那么它就是公平的游戏。
更多注意事项:
- 除了 exporting/changing 指定的路径变量之外,该函数不应以 任何 方式修改父环境(例如向其添加 functions/variables)。 =119=]
- 该函数不应等同于
/usr/
和/usr
之类的路径,它们的区别仅在于斜杠。 - 该函数应该正确处理空路径(指定当前目录),如果它们不存在则不会意外地将它们添加到路径变量中,如果它们存在则不会意外删除它们 已经存在了。
- 即使
PATH
为 空 ,该函数也应该工作。不能排除这种情况是不可能的。
我创建了一个单元测试脚本,它体现了我认为对四个函数的行为的完全合理和常识性的期望(我想避免外部 link):
function PrependPath() { TODO }
function AppendPath() { TODO }
function RemovePath() { TODO }
function UniquePath() { TODO }
function Assert() { ((i++)); [[ "$TESTPATH" == "" ]] && echo "$i) SUCCESS" || echo "$i) FAILURE => Got >$TESTPATH<"; }
i=0
echo "Test: PrependPath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /usr/sbin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /bin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /usr/slash
Assert "/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /new
Assert "/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /new
Assert "/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH ""
Assert ":/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH ""
Assert ":/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH "/usr"
Assert "/usr::/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
TESTPATH=
PrependPath TESTPATH /multiple /new
Assert "/new:/multiple"
PrependPath TESTPATH /and /new /foo /and
Assert "/foo:/and:/new:/multiple"
TESTPATH=":/foo:/bar:"
PrependPath TESTPATH /bar
Assert ":/foo:/bar:"
TESTPATH="/foo:/bar:"
PrependPath TESTPATH ""
Assert "/foo:/bar:"
TESTPATH=":/foo:/bar"
PrependPath TESTPATH ""
Assert ":/foo:/bar"
TESTPATH="/foo::/bar"
PrependPath TESTPATH ""
Assert "/foo::/bar"
TESTPATH=$'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
PrependPath TESTPATH /ano
Assert $'/ano:/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
PrependPath TESTPATH $'/te st\nnew/foo'
Assert $'/ano:/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
PrependPath TESTPATH $'/ne w\ner\n'
Assert $'/ne w\ner\n:/ano:/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
TESTPATH="/foo:/bar"
PrependPath TESTPATH "/b.r" "/fo+" "/fo*" "/b[a]r" "\foo" "/food?"
Assert "/food?:\foo:/b[a]r:/fo*:/fo+:/b.r:/foo:/bar"
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
PrependPath TESTPATH "/a[d" "/mor(e" "/other{" "/ano"
Assert $'/ano:/other{::/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
TESTPATH=$'\new'
PrependPath TESTPATH "\new"
Assert $'\new:\new'
TESTPATH=
PrependPath TESTPATH "/foo"
Assert "/foo"
PrependPath TESTPATH ""
Assert ":/foo"
TESTPATH=$'\n:/foo:/bar:\n'
PrependPath TESTPATH "/bar"
Assert $'\n:/foo:/bar:\n'
PrependPath TESTPATH $'\n'
Assert $'\n:/foo:/bar:\n'
PrependPath TESTPATH "/new"
Assert $'/new:\n:/foo:/bar:\n'
PrependPath TESTPATH ""
Assert $':/new:\n:/foo:/bar:\n'
TESTPATH=$':/foo:/bar:\n\n'
PrependPath TESTPATH "/bar"
Assert $':/foo:/bar:\n\n'
PrependPath TESTPATH $'\n'
Assert $'\n::/foo:/bar:\n\n'
PrependPath TESTPATH $'\n\n'
Assert $'\n::/foo:/bar:\n\n'
PrependPath TESTPATH "/new"
Assert $'/new:\n::/foo:/bar:\n\n'
TESTPATH=
PrependPath TESTPATH ""
Assert ":"
PrependPath TESTPATH ""
Assert ":"
PrependPath TESTPATH /new
Assert "/new:"
PrependPath TESTPATH ""
Assert "/new:"
TESTPATH=":/bin:"
PrependPath TESTPATH ""
Assert ":/bin:"
PrependPath TESTPATH "/foo"
Assert "/foo::/bin:"
TESTPATH="::"
PrependPath TESTPATH ""
Assert "::"
PrependPath TESTPATH "/foo"
Assert "/foo:::"
TESTPATH=":::"
PrependPath TESTPATH ""
Assert ":::"
PrependPath TESTPATH "/foo"
Assert "/foo::::"
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PrependPath TESTPATH /usr/sbin /bin /usr/slash /new /foo /new
Assert "/foo:/new:/usr/slash:/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
PATH="$ORIGPATH"
i=0
echo
echo "Test: AppendPath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /usr/sbin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /bin
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /usr/slash
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash"
AppendPath TESTPATH /new
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new"
AppendPath TESTPATH /new
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new"
AppendPath TESTPATH ""
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new:"
AppendPath TESTPATH ""
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new:"
AppendPath TESTPATH "/usr"
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new::/usr"
TESTPATH=
AppendPath TESTPATH /multiple /new
Assert "/multiple:/new"
AppendPath TESTPATH /and /new /foo /and
Assert "/multiple:/new:/and:/foo"
TESTPATH=":/foo:/bar:"
AppendPath TESTPATH /bar
Assert ":/foo:/bar:"
TESTPATH="/foo:/bar:"
AppendPath TESTPATH ""
Assert "/foo:/bar:"
TESTPATH=":/foo:/bar"
AppendPath TESTPATH ""
Assert ":/foo:/bar"
TESTPATH="/foo::/bar"
AppendPath TESTPATH ""
Assert "/foo::/bar"
TESTPATH=$'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
AppendPath TESTPATH /ano
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo::/ano'
AppendPath TESTPATH $'/te st\nnew/foo'
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo::/ano'
AppendPath TESTPATH $'/ne w\ner\n'
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo::/ano:/ne w\ner\n'
TESTPATH="/foo:/bar"
AppendPath TESTPATH "/b.r" "/fo+" "/fo*" "/b[a]r" "/bar" "\foo" "/food?"
Assert "/foo:/bar:/b.r:/fo+:/fo*:/b[a]r:\foo:/food?"
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
AppendPath TESTPATH "/a[d" "/mor(e" "/other{" "/ano"
Assert $':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e::/other{:/ano'
TESTPATH=$'\new'
AppendPath TESTPATH "\new"
Assert $'\new:\new'
TESTPATH=
AppendPath TESTPATH "/foo"
Assert "/foo"
AppendPath TESTPATH ""
Assert "/foo:"
TESTPATH=$'\n:/foo:/bar:\n'
AppendPath TESTPATH "/bar"
Assert $'\n:/foo:/bar:\n'
AppendPath TESTPATH $'\n'
Assert $'\n:/foo:/bar:\n'
AppendPath TESTPATH "/new"
Assert $'\n:/foo:/bar:\n:/new'
AppendPath TESTPATH ""
Assert $'\n:/foo:/bar:\n:/new:'
TESTPATH=$':/foo:/bar:\n\n'
AppendPath TESTPATH "/bar"
Assert $':/foo:/bar:\n\n'
AppendPath TESTPATH $'\n'
Assert $':/foo:/bar:\n\n:\n'
AppendPath TESTPATH $'\n\n'
Assert $':/foo:/bar:\n\n:\n'
AppendPath TESTPATH "/new"
Assert $':/foo:/bar:\n\n:\n:/new'
TESTPATH=
AppendPath TESTPATH ""
Assert ":"
AppendPath TESTPATH ""
Assert ":"
AppendPath TESTPATH /new
Assert ":/new"
AppendPath TESTPATH ""
Assert ":/new"
TESTPATH=":/bin:"
AppendPath TESTPATH ""
Assert ":/bin:"
AppendPath TESTPATH "/foo"
Assert ":/bin::/foo"
TESTPATH="::"
AppendPath TESTPATH ""
Assert "::"
AppendPath TESTPATH "/foo"
Assert ":::/foo"
TESTPATH=":::"
AppendPath TESTPATH ""
Assert ":::"
AppendPath TESTPATH "/foo"
Assert "::::/foo"
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/"
AppendPath TESTPATH /usr/sbin /bin /usr/slash /new /foo /new
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/slash/:/usr/slash:/new:/foo"
PATH="$ORIGPATH"
i=0
echo
echo "Test: RemovePath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/"
RemovePath TESTPATH /usr/sbin
Assert "/usr/bin:/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/:/foo"
RemovePath TESTPATH /usr/bin /usr/sbin /foo
Assert "/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:'
RemovePath TESTPATH /and
Assert $':/te st\nnew/foo:/ano\nth er:/more:'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:/te st\nnew/foo:'
RemovePath TESTPATH $'/te st\nnew/foo'
Assert $':/and:/ano\nth er:/more:'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:'
RemovePath TESTPATH ""
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:/more:'
RemovePath TESTPATH "/a.d" "\and" "/andy?" $'/te st\nnew/fo+' "/an*"
Assert $':/te st\nnew/foo:/and:/ano\nth er:/more:'
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:'
RemovePath TESTPATH "/a[d" "/mor(e" "/other{" "/ano"
Assert $':/te[ st\nnew/foo:/ano\nth er:'
TESTPATH=$':/te st\nnew/foo:/and:/ano\nth er:\n:/more:'
RemovePath TESTPATH "/sp ace" $'/new\nline' $'\n' "" $'/ano\nth er'
Assert $'/te st\nnew/foo:/and:/more'
TESTPATH="\no:\newline"
RemovePath TESTPATH $'\no'
Assert "\no:\newline"
RemovePath TESTPATH "\no"
Assert "\newline"
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH ""
Assert $'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH "\n"
Assert $'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH $'\n\n'
Assert $'\n:/more:/of:\n:/th is:\n'
RemovePath TESTPATH $'\n'
Assert $'/more:/of:/th is'
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is:\n\n:\n'
RemovePath TESTPATH $'\n'
Assert $'/more:\n\n:/of:/th is:\n\n'
RemovePath TESTPATH /of
Assert $'/more:\n\n:/th is:\n\n'
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is::\n'
RemovePath TESTPATH $'\n'
Assert $'/more:\n\n:/of:/th is:'
TESTPATH=$'\n:/more:\n\n:/of:\n:/th is::\n'
RemovePath TESTPATH ""
Assert $'\n:/more:\n\n:/of:\n:/th is:\n'
TESTPATH=":::"
RemovePath TESTPATH "/foo"
Assert ":::"
RemovePath TESTPATH ""
Assert ""
TESTPATH="::"
RemovePath TESTPATH "/foo"
Assert "::"
RemovePath TESTPATH ""
Assert ""
TESTPATH=":"
RemovePath TESTPATH "/foo"
Assert ":"
RemovePath TESTPATH ""
Assert ""
TESTPATH="::/foo"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH="::/foo"
RemovePath TESTPATH ""
Assert "/foo"
TESTPATH=":/foo"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH=":/foo"
RemovePath TESTPATH ""
Assert "/foo"
RemovePath TESTPATH "/foo"
Assert ""
TESTPATH="/foo::"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH="/foo::"
RemovePath TESTPATH ""
Assert "/foo"
TESTPATH="/foo:"
RemovePath TESTPATH "/foo"
Assert ":"
TESTPATH="/foo:"
RemovePath TESTPATH ""
Assert "/foo"
TESTPATH=""
RemovePath TESTPATH ""
Assert ""
RemovePath TESTPATH "/foo"
Assert ""
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/"
RemovePath TESTPATH /usr/sbin /bin /foo
Assert "/usr/bin:/usr/sbin/tmp:/usr/sbin/"
PATH="$ORIGPATH"
i=0
echo
echo "Test: UniquePath"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/:/usr/sbin:/bin"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
TESTPATH=":/foo:/bar:/foo"
UniquePath TESTPATH
Assert ":/foo:/bar"
TESTPATH="/foo:/bar:/foo:"
UniquePath TESTPATH
Assert "/foo:/bar:"
TESTPATH=":/foo:/bar:/foo:"
UniquePath TESTPATH
Assert ":/foo:/bar"
TESTPATH="/foo:/bar::/foo"
UniquePath TESTPATH
Assert "/foo:/bar:"
TESTPATH="/foo:/bar::/foo:"
UniquePath TESTPATH
Assert "/foo:/bar:"
TESTPATH=$':/te st\nnew/foo:/ano:/ano\nth er:th er::/more:/te st\nnew/foo:'
UniquePath TESTPATH
Assert $':/te st\nnew/foo:/ano:/ano\nth er:th er:/more'
TESTPATH=$'/te st\nnew/foo:/and:/ano\nth er:/more:/a.d:\and:/andy?:/te st\nnew/fo+:/an*:/more'
UniquePath TESTPATH
Assert $'/te st\nnew/foo:/and:/ano\nth er:/more:/a.d:\and:/andy?:/te st\nnew/fo+:/an*'
TESTPATH=$':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:/mor(e:/other{:/ano:/a[d'
UniquePath TESTPATH
Assert $':/te[ st\nnew/foo:/a[d:/ano\nth er:/mor(e:/other{:/ano'
TESTPATH=$'\no:\newline:\no:\newline:\no'
UniquePath TESTPATH
Assert $'\no:\newline:\no:\newline'
TESTPATH="/foo:/foo"
UniquePath TESTPATH
Assert "/foo"
UniquePath TESTPATH
Assert "/foo"
TESTPATH=":"
UniquePath TESTPATH
Assert ":"
TESTPATH="::"
UniquePath TESTPATH
Assert ":"
TESTPATH=":::"
UniquePath TESTPATH
Assert ":"
TESTPATH=$'\n:/more::/of:\n:/th is:\n\n:\n'
UniquePath TESTPATH
Assert $'\n:/more::/of:/th is:\n\n'
TESTPATH=$':/more:/of::\n\n::/th is:\n\n:\n'
UniquePath TESTPATH
Assert $':/more:/of:\n\n:/th is:\n'
TESTPATH=$'/foo:\n:/foo'
UniquePath TESTPATH
Assert $'/foo:\n'
TESTPATH=$'/foo::'
UniquePath TESTPATH
Assert $'/foo:'
TESTPATH=
UniquePath TESTPATH
Assert ""
ORIGPATH="$PATH"
PATH=
TESTPATH="/usr/bin:/usr/sbin:/bin:/usr/sbin:/usr/sbin/tmp:/usr/sbin/:/bin"
UniquePath TESTPATH
Assert "/usr/bin:/usr/sbin:/bin:/usr/sbin/tmp:/usr/sbin/"
PATH="$ORIGPATH"
如果您的函数通过了这些测试中的每一项,并且没有其他人能够发现我没有在脚本中检查过的缺陷,那么您就完全回答了我的问题。
自然地,我在 Stack Overflow 上尝试过的任何其他相关问题的答案中 none 为我提供了解决此问题的方法 (1, 2, 3, 4, 5, 6, 7, 8, 9)。也许我漏掉了一个,但鉴于我正在寻找我指定的通用级别的所有四个函数,这仍然不能完全回答我的问题。
我试过的
使用字符串指定要修改的变量很容易使用间接扩展来处理,例如如果 </code> 是 <code>PATH
那么 ${!1}
就是 $PATH
。 Setting/exporting 可以使用 export ""=XXX
间接完成变量。在最终解决方案中,可能有更多 efficient/elegant 方法来处理多个输入,但作为后备解决方案,您始终可以只对它们使用 for 循环,因此任务可以减少到只需要四个函数来执行使用单个参数对 PATH
变量执行所需的操作。
使用变量的local
关键字可以实现不污染父命名空间,但是子函数最多只能是unset -f
,这并不能真正满足不可能改变父命名空间的要求环境。附加到 PATH
变量可以使用替代参数扩展巧妙地完成,即类似 PATH="${PATH:+${PATH}:}/new/path"
的东西。前置就像 PATH="/new/path${PATH:+:${PATH}}"
.
这些表达式可以正确地处理是否添加冒号分隔符 几乎 所有的时间,并且对于它们不存在的特殊情况,可能可以添加一个简单的条件来修复视情况而定。有了所有这些构建块,我的问题的症结在于如何以防弹的方式稳健地分割路径,以便能够检查某个路径是否已经存在。我的尝试之一是只搜索字符串而不提取组件:
function AppendPath() { echo "$PATH" | grep -Eq "(^|:)($|:)" || export PATH="${PATH:+:${PATH}}"; }
但这失败了,因为 </code> 的内容被解释为正则表达式。此外,当 <code>PATH
为空时,对 grep 的调用会失败,这意味着需要像 /bin/grep
这样的绝对路径,但这并不是特别便携。对于 RemovePath
,我尝试使用空字符作为分隔符并进行反向 grep,例如类似于:
function RemovePath() { __path="$(echo -n "$PATH" | tr ':' '[=15=]' | grep -zxvF "" | tr '[=15=]' ':')"; export PATH="${__path%:}"; }
我在 grep
上使用 -F
以确保它不会被视为正则表达式,用 -x
匹配整行,用 -z
使用空分隔符, 并用 -v
反转匹配。除了某些极端情况和空 PATH
情况的明显问题外,grep -zF
仍然在每个换行符处划分搜索模式,尽管使用了空定界符,这意味着尝试删除 $'/foo\nbar'
将取而代之只需删除 /foo
和 bar
的所有实例。命令替换 $()
的一个偷偷摸摸的问题也在这里浮出水面,它会截断所有尾随的换行符。尝试:
echo -n "$(echo -n $'Hey\nthere\n\n\n')"
我尝试使用 IFS=:
将 PATH
分解成一个数组,但除其他外,命令替换是我在尝试用 "$(IFS=:; echo -n "${pathparts[*]}")"
之类的东西重新组装它时再次卡住的地方.现在我正在研究基于 read
命令和手动循环的 bash 内置想法,以避免命令替换,grep
和空 PATH
问题。我实际上想避免用很多不起作用的东西来污染我的问题,但这是被要求的,我希望至少现在人们相信我在发布之前已经认真对待了这个问题。
这是一个解决方案,在我的测试范围内,在管理 PATH
类变量的每种情况下总是会做正确的事情:
# Example: PrependPath PATH /usr/local/cuda/bin
function PrependPath() {
local __args __item
for __args in "${@:2}"; do
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
[[ "$__item" == "$__args" ]] && continue 2
done <<< "${!1}:"
([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args+=':'
export ""="$__args${!1}"
done
}
# Example: AppendPath PATH /usr/local/cuda/bin
function AppendPath() {
local __args __item
for __args in "${@:2}"; do
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
[[ "$__item" == "$__args" ]] && continue 2
done <<< "${!1}:"
([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args=":$__args"
export ""="${!1}$__args"
done
}
# Example: RemovePath INFOPATH /usr/local/texlive/2019/texmf-dist/doc/info
function RemovePath() {
local __item __args __path=
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
for __args in "${@:2}"; do
[[ "$__item" == "$__args" ]] && continue 2
done
__path="$__path:$__item"
done <<< "${!1}:"
[[ "$__path" != ":" ]] && __path="${__path#:}"
export ""="$__path"
}
# Example: UniquePath LD_LIBRARY_PATH
function UniquePath() {
local __item __args __path= __seen=()
[[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do
for __args in "${__seen[@]}"; do
[[ "$__item" == "$__args" ]] && continue 2
done
__seen+=("$__item")
__path="$__path:$__item"
done <<< "${!1}:"
[[ "$__path" != ":" ]] && __path="${__path#:}"
export ""="$__path"
}
解决问题的关键是使用read
以:
作为分隔符,并且只使用bash循环到check/assemble所需的输出路径, 没有命令替换, grep
, awk
, sed
或任何其他外部命令。
汇总到 'one-liners' 以便可以轻松地在脚本、命令行和 .bashrc
中使用它们:
# Functions to manage arbitrary PATH-like variables
function PrependPath() { local __args __item; for __args in "${@:2}"; do [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do [[ "$__item" == "$__args" ]] && continue 2; done <<< "${!1}:"; ([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args+=':'; export ""="$__args${!1}"; done; }
function AppendPath() { local __args __item; for __args in "${@:2}"; do [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do [[ "$__item" == "$__args" ]] && continue 2; done <<< "${!1}:"; ([[ -n "${!1#:}" ]] || [[ -z "$__args" ]]) && __args=":$__args"; export ""="${!1}$__args"; done; }
function RemovePath() { local __item __args __path=; [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do for __args in "${@:2}"; do [[ "$__item" == "$__args" ]] && continue 2; done; __path="$__path:$__item"; done <<< "${!1}:"; [[ "$__path" != ":" ]] && __path="${__path#:}"; export ""="$__path"; }
function UniquePath() { local __item __args __path= __seen=(); [[ -n "${!1}" ]] && while IFS= read -r -d ':' __item; do for __args in "${__seen[@]}"; do [[ "$__item" == "$__args" ]] && continue 2; done; __seen+=("$__item"); __path="$__path:$__item"; done <<< "${!1}:"; [[ "$__path" != ":" ]] && __path="${__path#:}"; export ""="$__path"; }