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

那分别是:

例如(我没有忘记任何 $ 符号):

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_PATHLIBRARY_PATH, INFOPATH, ...

困难的是,我希望这些函数适用于 绝对可以在 PATH 中出现的每个 合法路径,这基本上消除了字符 [= 26=]/:,但允许 所有 其他字符(假设它是有效编码的 UTF-8),包括特定的空格和换行符。

我想将 "no-one in their right mind would ever use a path like that" 的哲学讨论排除在外。如果我可以合法地创建这样的路径,并且将其放入 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' 将取而代之只需删除 /foobar 的所有实例。命令替换 $() 的一个偷偷摸摸的问题也在这里浮出水面,它会截断所有尾随的换行符。尝试:

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"; }