【问题标题】:Is there a general way to add/prepend/remove paths from general environment variables in bash?是否有一种通用方法可以在 bash 中从通用环境变量中添加/添加/删除路径?
【发布时间】:2014-08-22 07:34:31
【问题描述】:

我想知道是否有人有一个从 bash 环境变量中添加/删除路径的好方法(不仅仅是 PATH 变量)。

我的情况是我在没有管理员权限的集群上实现一个简单的类似模块的系统。我希望能够编写如下脚本:

openmpi_module.sh

#!/bin/bash

if [ $1 == "load" ]; then
  path_prepend PATH "$HOME/apps/openmpi-1.8.1-gcc-4.9.0/bin"
  path_prepend CPATH "$HOME/apps/openmpi-1.8.1-gcc-4.9.0/include"
  ...
fi

if [ $1 == "unload" ]; then
  path_remove PATH "$HOME/apps/openmpi-1.8.1-gcc-4.9.0/bin"
  path_remove CPATH "$HOME/apps/openmpi-1.8.1-gcc-4.9.0/include"
  ...
fi

我发现 questions dealing 专门修改了 PATH 变量,但不是一般的环境变量。这些解决方案的通用版本似乎是一个有用的工具。

我目前的解决方案是在我的.bash_profile 中获取以下内容,但我想知道是否有人有更优雅的解决方案(没有安装实际Modules system 的本地副本)。大多数情况下,我对使用这么多 evals 和 echos 感到不舒服,我更愿意避免这种做法。

#!/bin/bash
# modified from https://stackoverflow.com/questions/370047/

function path_remove () {
eval export $(echo $1=\$\(echo -n $(echo -n "\$$1 | awk -v RS=: -v ORS=: '\$0 != \"'$2'\"' | sed 's/:\$//'")\))
}

function path_append () {
path_remove $1 $2
eval export $1="\$$1:$2"
}

function path_prepend () {
path_remove $1 $2
eval export $1="$2:\$$1"
}

【问题讨论】:

  • 确实,evals 是邪恶的,但你不需要它们;参见 BashFAQ #6,关于使用间接变量:mywiki.wooledge.org/BashFAQ/006
  • 顺便说一句,echo -necho -E 是 bash 不仅扩展 POSIX,而且实际上破坏了与标准的兼容性的少数几个地方。最好避免使用它们,并在您想做任何花哨的事情时使用printf

标签: bash


【解决方案1】:

这里是紧凑的bash 函数,其中:

  • 避免eval,使用printf -v间接设置变量,如@Charles Duffy's excellent answer
  • 仅使用参数扩展来操作路径列表
  • 自包含的(它们不相互依赖 - 以复制一些代码为代价)。
  • force pre/appending,就像在 OP 的函数中一样:即,如果指定的路径条目已经存在但在不同的位置,则将其强制到所需的位置。李>

警告:现有的重复条目只有在它们不直接相邻时才会被删除(参见下面的替代方法)。

#!/usr/bin/env bash

# The functions below operate on PATH-like variables whose fields are separated
# with ':'.
# Note: The *name* of the PATH-style variable must be passed in as the 1st
#       argument and that variable's value is modified *directly*.

# SYNOPSIS: path_prepend varName path
# Note: Forces path into the first position, if already present.
#       Duplicates are removed too, unless they're directly adjacent.
# EXAMPLE: path_prepend PATH /usr/local/bin
path_prepend() {
  local aux=":${!1}:"
  aux=${aux//:$2:/:}; aux=${aux#:}; aux=${aux%:}
  printf -v "$1" '%s' "${2}${aux:+:}${aux}"  
}

# SYNOPSIS: path_append varName path
# Note: Forces path into the last position, if already present.
#       Duplicates are removed too, unless they're directly adjacent.
# EXAMPLE: path_append PATH /usr/local/bin
path_append() {
  local aux=":${!1}:"
  aux=${aux//:$2:/:}; aux=${aux#:}; aux=${aux%:}
  printf -v "$1" '%s' "${aux}${aux:+:}${2}"
}

# SYNOPSIS: path_remove varName path
# Note: Duplicates are removed too, unless they're directly adjacent.
# EXAMPLE: path_remove PATH /usr/local/bin
path_remove() {
  local aux=":${!1}:"
  aux=${aux//:$2:/:}; aux=${aux#:}; aux=${aux%:}
  printf -v "$1" '%s' "$aux"
}

如果您确实需要处理直接相邻的重复项和/或希望能够指定不同的字段分隔符,这里是使用 @konsolebox' helpful answer 的strong>阵列技术。

# SYNOPSIS: field_prepend varName fieldVal [sep]
#   SEP defaults to ':'
# Note: Forces fieldVal into the first position, if already present.
#       Duplicates are removed, too.
# EXAMPLE: field_prepend PATH /usr/local/bin
field_prepend() {
    local varName=$1 fieldVal=$2 IFS=${3:-':'} auxArr
    read -ra auxArr <<< "${!varName}"
    for i in "${!auxArr[@]}"; do
        [[ ${auxArr[i]} == "$fieldVal" ]] && unset auxArr[i]
    done
    auxArr=("$fieldVal" "${auxArr[@]}")
    printf -v "$varName" '%s' "${auxArr[*]}"
}

# SYNOPSIS: field_append varName fieldVal [sep]
#   SEP defaults to ':'
# Note: Forces fieldVal into the last position, if already present.
#       Duplicates are removed, too.
# EXAMPLE: field_append PATH /usr/local/bin
field_append() {
    local varName=$1 fieldVal=$2 IFS=${3:-':'} auxArr
    read -ra auxArr <<< "${!varName}"
    for i in "${!auxArr[@]}"; do
        [[ ${auxArr[i]} == "$fieldVal" ]] && unset auxArr[i]
    done
    auxArr+=("$fieldVal")
    printf -v "$varName" '%s' "${auxArr[*]}"
}

# SYNOPSIS: field_remove varName fieldVal [sep]
#   SEP defaults to ':'
# Note: Duplicates are removed, too.
# EXAMPLE: field_remove PATH /usr/local/bin
field_remove() {
    local varName=$1 fieldVal=$2 IFS=${3:-':'} auxArr
    read -ra auxArr <<< "${!varName}"
    for i in "${!auxArr[@]}"; do
        [[ ${auxArr[i]} == "$fieldVal" ]] && unset auxArr[i]
    done
    printf -v "$varName" '%s' "${auxArr[*]}"
}

【讨论】:

  • @konsolebox:重新分裂想法:是的,这是你的,这就是为什么我在我的回答中给予你信任 - 我原则上喜欢你的回答,但我不喜欢它的事情我试图纠正我的回答:(a)使用printf -v 而不是eval,(b)使用小写变量名,(c)更好地处理$IFS。正如我所评论的,我也不喜欢全局 shopt -s extglob,但是从 eval 切换到 printf -v 后,这个问题就消失了。如果不是因为这些问题,我会投票赞成你的答案。
【解决方案2】:

export 设置一个标志,指定一个变量应该在环境中。但是,如果它已经存在,更新将始终传递到环境;你不需要做任何其他事情。

因此:

PATH=/new/value:$PATH

PATH=$PATH:/new/value

...完全足够了,除非您想添加自己的逻辑(如重复数据删除)。

如果您只想在不存在重复值的情况下采取行动,您可以编写如下内容:

prepend() {
  local var=$1
  local val=$2
  local sep=${3:-":"}
  [[ ${!var} =~ (^|"$sep")"$val"($|"$sep") ]] && return # already present
  [[ ${!var} ]] || { printf -v "$var" '%s' "$val" && return; } # empty
  printf -v "$var" '%s%s%s' "$val" "$sep" "${!var}" # prepend
}

append() {
  local var=$1
  local val=$2
  local sep=${3:-":"}
  [[ ${!var} =~ (^|"$sep")"$val"($|"$sep") ]] && return # already present
  [[ ${!var} ]] || { printf -v "$var" '%s' "$val" && return; } # empty
  printf -v "$var" '%s%s%s' "${!var}" "$sep" "${val}" # append
}

remove() {
  local var=$1
  local val=$2
  local sep=${3:-":"}
  while [[ ${!var} =~ (^|.*"$sep")"$val"($|"$sep".*) ]]; do
    if [[ ${BASH_REMATCH[1]} && ${BASH_REMATCH[2]} ]]; then
      # match is between both leading and trailing content
      printf -v "$var" '%s%s' "${BASH_REMATCH[1]%$sep}" "${BASH_REMATCH[2]}"
    elif [[ ${BASH_REMATCH[1]} ]]; then
      # match is at the end
      printf -v "$var" "${BASH_REMATCH[1]%$sep}"
    else
      # match is at the beginning
      printf -v "$var" "${BASH_REMATCH[2]#$sep}"
    fi
  done
}

...用作:

prepend PATH /usr/local/bin
remove PATH /usr/local/bin

注意:

  • 不需要function 关键字,它无缘无故破坏了POSIX 兼容性。 (我们做的其他事情打破了 POSIX,但实际上以某种方式增加了价值)。
  • 我们的正则表达式引用了应按字面解释的内容,而将应视为正则表达式字符的内容不加引号。请注意,此行为仅在 3.2 之后的 bash 版本中完全一致;如果您需要与旧版本兼容,则需要进行一些更新。

【讨论】:

  • 如果您的脚本以 Bash 为目标,则无需使用 POSIX。人们可能更喜欢使用function 来明确表明脚本只能与它一起运行。有些人可能还会发现原始 sh 语法中的() 并不真正适合其手段。当函数已经用function 声明时,建议不要使用() 实际上会更好。
  • @konsolebox,我倾向于考虑偏离 POSIX sh 知道和有意而不是习惯,这本身就是一种有益的行为。所以——如果有人想使用function,因为他们认为它更具可读性,对他们来说更强大。如果他们使用 function 是因为他们认为这是在 bash 中定义函数的规范正确方法,我认为这更值得怀疑。
  • +1 是一个可靠的答案,具有强大的通用功能。 Quibble:prepend()append() 的名称暗示了 unconditional 操作,但事实并非如此 - 可能类似于 prependUnlessPresent()?相反,有时您想强制预先/附加(即,删除现有的,如果存在,然后预先/附加) - 实际上,这似乎是 OP 想要的。
  • @CharlesDuffy 我并不是在暗示您已经很好地列举了它们,但是我看不出这些偏好中的任何一个可能是错误的,或者选择使用它们是错误的。如果它只是关于 POSIX,那么它什么都不是。好吧,你有自己的喜好。如果您选择保留不必要的兼容性,那么您可以自己拥有它。请注意:如果脚本确实需要兼容性,则不能只遵循 POSIX。 POSIX 是一个永远无法满足的标准,因为旧的语法 shell 现在仍然很活跃。
  • 当然,可以安全地使用 eval。不安全地使用 eval 也非常非常容易。我知道我想推广哪种做法。
【解决方案3】:

最简单且可能最一致的方法是首先将这些字符串拆分为数组:

IFS=: read -ra T <<< "$PATH"

然后将元素添加到这些数组中:

# Append

T+=("$SOMETHING")

# Prepend or Insert

T=("$SOMETHING" "${T[@]}")

# Remove

for I in "${!T[@]}"; do
    if [[ ${T[I]} == "$SOMETHING" ]]; then
        unset "T[$I]"
        break  ## You can skip breaking if you want to remove all matches not just the first one.
    fi
done

之后,您可以使用安全的 eval 将其放回原处:

IFS=: eval 'PATH="${T[*]}"'

其实如果你有点“保守”,可以先保存IFS

OLD_IFS=$IFS; IFS=:
PATH="${T[*]}"
IFS=$OLD_IFS

功能:

shopt -s extglob

function path_append {
    local VAR=$1 ELEM=$2 T
    IFS=: read -ra T <<< "${!VAR}"
    T+=("$ELEM")
    [[ $VAR == [[:alpha:]_]*([[:alnum:]_]) ]] && IFS=: eval "$VAR=\${T[*]}"
}

function path_prepend {
    local VAR=$1 ELEM=$2 T
    IFS=: read -ra T <<< "${!VAR}"
    T=("$ELEM" "${T[@]}")
    [[ $VAR == [[:alpha:]_]*([[:alnum:]_]) ]] && IFS=: eval "$VAR=\${T[*]}"
}

function path_remove {
    local VAR=$1 ELEM=$2 T
    IFS=: read -ra T <<< "${!VAR}"
    for I in "${!T[@]}"; do
        [[ ${T[I]} == "$ELEM" ]] && unset "T[$I]"
    done
    [[ $VAR == [[:alpha:]_]*([[:alnum:]_]) ]] && IFS=: eval "$VAR=\${T[*]}"
}

【讨论】:

  • 您用于检查有效变量名的扩展 glob 过于严格:它们无法识别单字符变量名,例如 v。使用 shopt -s extglob 更改全局状态(而不是在函数内部进行本地化)是有问题的。最好不要使用全大写的变量名(即使是本地的),以避免与环境变量冲突。
  • @mklement0 使用extglob 更改全局状态永远不会有问题,因为从4.1 开始,它已经在[[ ]] 内部成为默认行为,现在仅与路径名扩展有关。在函数内部使用它们将是多余的。而且我不确定变量的目标形式是否值得担心,尤其是 OP 想要更改 PATH 和 CPATH。
  • Re 变量名:OP 只说not just the PATH variable,它对变量名没有任何假设。鉴于您的解决方案 如此接近 可以识别 all 有效的变量名,为什么不将第二个 + 替换为 * 从而覆盖单字符变量名,也是?
  • 需要我指出使用printf -v 是否需要验证或清理目标变量名称? printf -v "$VAR" '%s' "${T[*]}" 是一个相当干净的替代品。
  • 是的,很遗憾您不能将printf -varray 元素一起使用;在这种情况下,您可以使用IFS=$'\n' read -r -d '' "$varName"[ndx] &lt;&lt;&lt;...。您在其他地方提到了declare -n,它更优雅地解决了数组和非数组变量的问题,但它需要 bash 4.3+。 utils_merge 有什么功能?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-01-03
  • 2021-11-18
  • 2022-06-10
  • 1970-01-01
  • 2011-03-05
  • 1970-01-01
  • 2019-02-27
相关资源
最近更新 更多