如何干净地添加到$ PATH?


31

我想要一种在系统范围内或为单个用户向$ PATH添加内容的方法,而不必多次添加相同的路径。

想要这样做的一个原因是,可以在中添加内容.bashrc,而无需登录,并且在使用(例如)的系统中更有用lightdm,该系统从不调用.profile

我知道有关如何从$ PATH 清除重复项的问题,但是我不想删除重复项。我想只在尚不存在的情况下添加路径。



goldi,我不知道为什么,但即使有空,我也看到了您的第一条评论。但是,是的,名称前缀也可以使用,不用担心!关闭另一种方法也很好。
Ciro Santilli新疆改造中心法轮功六四事件

好的,只要您能收到我的消息。有时进行这样的逆转会造成一些混乱,我想我们会看到发生了什么。
goldilocks 2015年

Answers:


35

假设我们要添加的新路径是:

new=/opt/bin

然后,使用任何POSIX shell,我们可以进行测试以查看new路径中是否已经存在,如果没有,则添加它:

case ":${PATH:=$new}:" in
    *:"$new":*)  ;;
    *) PATH="$new:$PATH"  ;;
esac

注意冒号的使用。如果没有冒号,我们可能会认为它new=/bin已经在路径中,因为它的模式与on匹配/usr/bin。尽管PATH通常包含许多元素,但PATH中零和一元素的特殊情况也将得到处理。的路径最初不具有元素(即空的)的情况下,通过使用处理的${PATH:=$new}哪个受让人PATH$new,如果它是空的。以这种方式设置参数的默认值是所有POSIX Shell的功能:请参阅POSIX文档的 2.6.2节。)

可调用函数

为了方便起见,可以将以上代码放入一个函数中。可以在命令行中定义此函数,或者要使其永久可用,可以将其放入外壳程序的初始化脚本中(对于bash用户,应为~/.bashrc):

pupdate() { case ":${PATH:=$1}:" in *:"$1":*) ;; *) PATH="$1:$PATH" ;; esac; }

要使用此路径更新功能将目录添加到当前PATH:

pupdate /new/path

@hammar好的。我为此添加了一个案例。
John1024 '04

1
您可以保存2个区分大小写的内容-cf. unix.stackexchange.com/a/40973/1131
maxschlepzig 2014年

3
如果PATH为空,这将为添加一个空条目(即当前目录)PATH。我认为您还需要另一种情况。
CB Bailey 2014年

2
@CharlesBailey没有别的case。做吧case "${PATH:=$new}"。请参阅我自己的答案以获取类似的后备广告。
mikeserv

1
@ mc0e我添加了一个示例,该示例说明了如何使用Shell函数隐藏“线噪声”。
John1024 '16

9

创建一个/etc/profile.d名为的文件,例如mypath.sh(或您想要的任何文件)。如果您使用的是lightdm,请确保它是可行的,否则请使用/etc/bashrc或来自该文件的文件。此外,还具有以下功能:

checkPath () {
        case ":$PATH:" in
                *":$1:"*) return 1
                        ;;
        esac
        return 0;
}

# Prepend to $PATH
prependToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$a:$PATH
                fi
        done
        export PATH
}

# Append to $PATH
appendToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$PATH:$a
                fi
        done
        export PATH
}

$ PATH开头(前面)的内容优先于后面的内容,反之,结尾(后面)的内容将被前面的内容取代。这意味着如果$ PATH是/usr/local/bin:/usr/bin并且gotcha两个目录中都有一个可执行文件,则其中一个/usr/local/bin默认情况下将使用。

现在,您可以-在同一文件,另一个Shell配置文件或命令行中-使用:

appendToPath /some/path /another/path
prependToPath /some/path /yet/another/path

如果位于中.bashrc,则在启动新Shell时将防止该值出现多次。有一个局限性,如果您想添加预先添加的内容(即在$ PATH中移动路径),反之亦然,则必须自己完成。


$PATH与分开IFS=:最终比更加灵活case
mikeserv

@mikeserv毫无疑问。这是caseIMO 的一种骇客用途。我想awk在这里也可以很好地利用它。
goldilocks 2014年

那是个很好的观点。而且,我认为gawk可以直接分配$PATH
mikeserv 2014年

5

您可以这样操作:

echo $PATH | grep /my/bin >/dev/null || PATH=$PATH:/my/bin

注意:如果从其他变量构建PATH,请检查它们是否不为空,因为许多shell会将“”解释为“”。。


+1表示-qPOSIX需要grep 的手册页,但是我不知道这是否意味着仍然有一些(非POSIX)不具备此功能。
goldilocks 2014年

1
请注意,grep模式过于宽泛。考虑使用egrep -q“(^ |:)/ my / bin(:| \ $)”代替grep / my / bin> / dev / null。经过修改后,您的解决方案是正确的,并且我认为这是比@ john1024当前首选的答案更具可读性的解决方案。请注意,我使用了双引号,所以您使用变量替换来代替/my/bin
mc0e

5

该代码的重要部分是检查是否PATH包含特定路径:

printf '%s' ":${PATH}:" | grep -Fq ":${my_path}:"

也就是说,保证在每个路径PATH上分隔由双方PATH分隔符(:),然后检查-q)是否为文字字符串(-F)组成的的PATH分离,你的路径,另一个PATH分离器中存在。如果没有,则可以安全地添加路径:

if ! printf '%s' ":${PATH-}:" | grep -Fq ":${my_path-}:"
then
    PATH="${PATH-}:${my_path-}"
fi

这应该与POSIX兼容,并且应该与任何不包含换行符的路径一起使用。如果希望它在与POSIX兼容的情况下与包含换行符的路径一起使用,则更为复杂,但是如果您具有grep支持的支持-z,则可以使用它。


4

多年来,我一直在各种~/.profile文件中随身携带这个小功能。我认为它是由系统管理员在我曾经工作过的实验室中编写的,但我不确定。无论如何,它与Goldilock的方法类似,但略有不同:

pathmunge () {
        if ! echo $PATH | /bin/grep -Eq "(^|:)$1($|:)" ; then
           if [ "$2" = "after" ] ; then
              PATH=$PATH:$1
           else
              PATH=$1:$PATH
           fi
        fi
}

因此,要将新目录添加到的开头PATH

pathmunge /new/path

到最后:

pathmunge /new/path after

这对我有用!但是我交换了逻辑,默认情况下将其放置在after之后,并用“ before”覆盖。:)
Kevin Pauli 2015年

pathmunge是linux centos发行版/ etc / profile的一部分,它前后都有一个参数。我没有看到它在我的最新的Ubuntu 16
克敏周

/bin/grep->grep
Ben Creasy

4

更新:

我注意到您自己的答案有一个单独的函数,分别用于将追加或添加到$PATH。我喜欢这个主意。因此,我添加了一些参数处理。我还正确_命名了它:

_path_assign() { oFS=$IFS ; IFS=: ; add=$* ; unset P A ; A=
    set -- ${PATH:=$1} ; for p in $add ; do {
        [ -z "${p%-[AP]}" ] && { unset P A
                eval ${p#-}= ; continue ; }
        for d ; do [ -z "${d%"$p"}" ] && break
        done ; } || set -- ${P+$p} $* ${A+$p}
        done ; export PATH="$*" ; IFS=$oFS
}

% PATH=/usr/bin:/usr/yes/bin
% _path_assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/bin/nope \
    -P \
    /usr/nope/bin \
    /usr/bin \
    -A \
    /nope/usr/bin \
    /usr/nope/bin

% echo $PATH

输出:

/usr/nope/bin:/usr/bin:/usr/yes/bin:/usr/bin/nope:/nope/usr/bin

默认情况下,它会-A附加到$PATH,但是您可以-P通过-P在参数列表中的任意位置添加此行为来使其重复。您可以再次-A递给它,使其切换回挂起状态-A

安全评估

在大多数情况下,我建议人们避免使用eval。但是,我认为这是其永久使用的一个典范在这种情况下,唯一eval 可以看到的语句是P=A=。它的参数值在调用之前就经过严格测试。这是为了什么eval

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- ${PATH:=$1} ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH="$*" ; IFS=$oFS
}

这将接受您提供的任意数量的参数,$PATH并且仅在参数尚未添加时才将其添加一次$PATH。它仅使用完全可移植的POSIX shell脚本,仅依赖于shell内置,并且速度非常快。

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

@ TAFKA'goldilocks'在这里查看更新-您启发了我。
mikeserv

+1出于好奇(也许这将是一个很好的单独的问答),_前缀为shell函数使它们成为“适当命名空间”的想法从何而来?在其他语言中,它通常表示一个内部全局函数(即,需要是全局函数,但不打算在外部用作API的一部分)。我的名字当然不是一个好选择,但是在我看来,仅使用_它根本无法解决冲突问题-例如,使用一个实际的名称空间会更好。mikeserv_path_assign()
goldilocks 2014年

@ TAFKA'goldilocks'-最好使其更加具体,但名称越长,使用起来就越不方便。但是,如果您以任何适当的可执行二进制文件为前缀,_则需要切换软件包管理器。无论如何,从本质上讲,这只是一个“全局的,内部的函数” -它对于从声明它的shell调用的每个shell都是全局的,并且它只是解释器的内存中悬挂的一点解释语言脚本。unix.stackexchange.com/questions/120528/…–
mikeserv

您能否unset a(或等效)在个人资料末尾?
sourcejedi

0

看哪!工业强度的12行...技术上 bash-和zsh的便携外壳功能悉心爱你的~/.bashrc~/.zshrc选择的启动脚本:

# void +path.append(str dirname, ...)
#
# Append each passed existing directory to the current user's ${PATH} in a
# safe manner silently ignoring:
#
# * Relative directories (i.e., *NOT* prefixed by the directory separator).
# * Duplicate directories (i.e., already listed in the current ${PATH}).
# * Nonextant directories.
+path.append() {
    # For each passed dirname...
    local dirname
    for   dirname; do
        # Strip the trailing directory separator if any from this dirname,
        # reducing this dirname to the canonical form expected by the
        # test for uniqueness performed below.
        dirname="${dirname%/}"

        # If this dirname is either relative, duplicate, or nonextant, then
        # silently ignore this dirname and continue to the next. Note that the
        # extancy test is the least performant test and hence deferred.
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue

        # Else, this is an existing absolute unique dirname. In this case,
        # append this dirname to the current ${PATH}.
        PATH="${PATH}:${dirname}"
    done

    # Strip an erroneously leading delimiter from the current ${PATH} if any,
    # a common edge case when the initial ${PATH} is the empty string.
    PATH="${PATH#:}"

    # Export the current ${PATH} to subprocesses. Although system-wide scripts
    # already export the ${PATH} by default on most systems, "Bother free is
    # the way to be."
    export PATH
}

为瞬时的荣耀做好准备。然后,不要这样做,而是希望能做到最好:

export PATH=$PATH:~/opt/bin:~/the/black/goat/of/the/woods/with/a/thousand/young

改为这样做,并确保获得最佳,无论您是否真的想要:

+path.append ~/opt/bin ~/the/black/goat/of/the/woods/with/a/thousand/young

很好,定义“最佳”。

安全地添加和添加到当前${PATH}内容上并不是一件平常的事。虽然方便且看似明智,但这种形式的export PATH=$PATH:~/opt/bin单线会带来如下恶魔般的并发症:

  • 偶然的相对目录名(例如export PATH=$PATH:opt/bin)。虽然bashzsh默默承受,大多忽略了相对dirnames中大多数情况下,相对dirnames中的任何前缀ht(和其他可能的恶意字符)原因既可耻残害自己ALA小林正树的开创性1962年的杰作切腹

    # Don't try this at home. You will feel great pain.
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:harakiri && echo $PATH
    /usr/local/bin:/usr/bin:arakiri
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:tanuki/yokai && echo $PATH
    binanuki/yokai   # Congratulations. Your system is now face-up in the gutter.
  • 偶然重复的目录名。尽管重复的${PATH}dirname在很大程度上是无害的,但它们也是不必要的,繁琐的,效率低下的,阻碍了可调试性并促进了驱动器磨损–有点像这样的答案。虽然NAND式固态硬盘(当然免疫)读取磨损,硬盘都没有。对每个尝试执行的命令进行不必要的文件系统访问意味着以相同的速度不必要地磨损了读取头。当在嵌套子流程中调用嵌套壳时,重复项特别无害,此时,似乎无害的一线希望像export PATH=$PATH:~/wat迅速爆炸到${PATH}地狱第七圈PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat。如果随后在其上附加其他目录名,则只有Beelzebubba可以帮助您。(不要让这种情况发生在您宝贵的孩子身上。

  • 意外缺少目录名。同样,虽然缺少的${PATH}目录名在很大程度上是无害的,但它们通常也是不需要的,繁琐的,效率低下的,阻碍可调试性并加剧了驱动器的磨损。

如此,友好的自动化功能就像上面定义的shell函数一样。我们必须从自己中拯救自己。

但是...为什么是“ + path.append()”?为什么不简单的append_path()?

为了避免歧义(例如,使用当前命令${PATH}或系统范围内其他地方定义的外部命令中的外部命令),理想情况下,用户定义的壳函数应以唯一的子字符串作为前缀或后缀,这些子字符串受标准命令基本名支持bashzsh但对于标准命令基本名,则被禁止(例如)+

嘿。有用。不要评价我

但是...为什么是“ + path.append()”?为什么不使用“ + path.prepend()”?

因为追加到当前${PATH}要比追加到当前更安全${PATH},所以所有事物都是平等的,而从没有过。用特定于用户的命令覆盖系统范围的命令最好是不卫生的,最坏的情况是疯狂的。例如,在Linux下,下游应用程序通常期望使用GNU coreutils命令的变体,而不是自定义的非标准派生或替代。

也就是说,绝对有有效的用例。定义等效+path.prepend()函数很简单。Sans prolix模糊不清,因为他和她共同的理智:

+path.prepend() {
    local dirname
    for dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${dirname}:${PATH}"
    done
    PATH="${PATH%:}"
    export PATH
}

但是……为什么不吉尔斯?

吉尔 ' 接受的答案在别处是在一般情况下,作为一个令人印象深刻的最佳‘壳不可知幂等追加’。在一般情况下bash,并zsh不良符号链接,但是,所需要的性能损失做这样伤心的Gentoo ricer的我。即使存在不希望的符号链接,是否add_to_PATH()值得为每个参数派生一个子shell 是否值得潜在地插入符号链接重复项。

对于要求甚至消除符号链接重复项的严格用例,此zsh特定于变体的方法是通过高效的内置函数而不是效率低下的fork:

+path.append() {
    local dirname
    for   dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname:A}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${PATH}:${dirname}"
    done
    PATH="${PATH#:}"
    export PATH
}

请注意,*":${dirname:A}:"*而不是*":${dirname}:"*原始的。可悲的是,在大多数其他壳层之下(包括),都缺少:A一种奇妙的zsh主义bash。引用man zshexpn

:像a修饰符一样,将文件名转换为绝对路径,然后将结果传递给realpath(3)库函数以解析符号链接。注:没有一个系统的realpath(3)库函数,符号链接没有解决,所以这些系统aA是等价的。

没有其他问题。

别客气。享受安全的炮击。您现在应该得到它。


0

这是我的函数编程风格的版本。

  • 适用于任何以冒号分隔的*PATH变量,不仅限于PATH
  • 不访问全局状态
  • 仅在其给定的不变输入下工作
  • 产生单个输出
  • 没有副作用
  • 可记忆(原则上)

还值得注意的是:

  • 不可知论关于export荷兰国际集团; 留给调用者(参见示例)
  • bash; 没有分叉
path_add(){
  #$ 1:确保在给定路径字符串中的元素恰好在一次
  #$ 2:现有路径字符串值(“ $ PATH”,而不是“ PATH”)
  #$ 3(可选,任何内容):如果有,则附加$ 1;否则,前置
  #
  # 例子:
  #$ export PATH = $(path_add'/ opt / bin'“ $ PATH”)
  #$ CDPATH = $(path_add'/ Music'“ $ CDPATH” at_end)

  本地-r已经_present =“(^ |:)$ {1}($ | :)”
  如果[[“ $ 2” =〜$ already_present]]; 然后
    回声“ $ 2”
  elif [[$#== 3]]; 然后
    回声“ $ {2}:$ {1}”
  其他
    回声“ $ {1}:$ {2}”
  科幻
}

0

此脚本允许您在末尾添加$PATH

PATH=path2; add_to_PATH after path1 path2:path3
echo $PATH
path2:path1:path3

或在以下内容的开头添加$PATH

PATH=path2; add_to_PATH before path1 path2:path3
echo $PATH
path1:path3:path2

# Add directories to $PATH iff they're not already there
# Append directories to $PATH by default
# Based on https://unix.stackexchange.com/a/4973/143394
# and https://unix.stackexchange.com/a/217629/143394
add_to_PATH () {
  local prepend  # Prepend to path if set
  local prefix   # Temporary prepended path
  local IFS      # Avoid restoring for added laziness

  case $1 in
    after)  shift;; # Default is to append
    before) prepend=true; shift;;
  esac

  for arg; do
    IFS=: # Split argument by path separator
    for dir in $arg; do
      # Canonicalise symbolic links
      dir=$({ cd -- "$dir" && { pwd -P || pwd; } } 2>/dev/null)
      if [ -z "$dir" ]; then continue; fi  # Skip non-existent directory
      case ":$PATH:" in
        *":$dir:"*) :;; # skip - already present
        *) if [ "$prepend" ]; then
           # ${prefix:+$prefix:} will expand to "" if $prefix is empty to avoid
           # starting with a ":".  Expansion is "$prefix:" if non-empty.
            prefix=${prefix+$prefix:}$dir
          else
            PATH=$PATH:$dir  # Append by default
          fi;;
      esac
    done
  done
  [ "$prepend" ] && [ "$prefix" != "" ] && PATH=$prefix:$PATH
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.