どうすれば$ PATHにきれいに追加できますか?


31

同じパスを複数回追加することなく、システム全体または個々のユーザーのために、$ PATHに物事を追加する方法が必要です。

これを行う理由の1つ.bashrcは、ログインを必要とせずに追加できるようにするためです。またlightdm、を呼び出さない(たとえば)を使用するシステムでもより便利です.profile

$ PATHから重複を削除する方法に関する質問は承知していますが、重複を削除したくありませんパスがまだ存在しない場合にのみパス追加する方法が必要です。



goldi、理由はわかりませんが、空であっても最初のコメントを見ました。しかし、はい、名前の接頭辞も機能し、心配はありません!他の方法を閉じることも問題ありません。
Ciro Santilli新疆改造中心法轮功六四事件

わかりました、あなたが私のメッセージを受け取った限り。このような反転を行うと、多少の混乱が生じることがあります。何が起こるかわかります。
goldilocks

回答:


35

追加する新しいパスが次のとおりであると仮定します。

new=/opt/bin

次に、POSIXシェルを使用newして、パスに既に存在するかどうかをテストし、存在しない場合は追加できます。

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

コロンの使用に注意してください。コロンがないと、たとえば、new=/binパターンが一致したため、すでにパスにいたと考えるかもしれません/usr/bin。PATHには通常多くの要素がありますが、PATH内の要素が0と1の特殊なケースも処理されます。最初に要素を持たない(空の)PATHの場合は、空の場合に${PATH:=$new}割り当てPATHられるを使用して処理され$newます。この方法でパラメーターのデフォルト値を設定することは、すべてのPOSIXシェルの機能です。POSIXドキュメントのセクション2.6.2を参照してください)。

呼び出し可能な関数

便宜上、上記のコードを関数に追加できます。この関数は、コマンドラインで定義するか、永続的に使用可能にするために、シェルの初期化スクリプトに配置できます(bashユーザーの場合は、次のようになります~/.bashrc)。

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

このパス更新機能を使用して、ディレクトリを現在のPATHに追加するには:

pupdate /new/path

@hammar OK。そのためのケースを追加しました。
ジョン1024 14

1
2つのケースの区別を保存できます-cf. unix.stackexchange.com/a/40973/1131
maxschlepzig 14

3
PATHが空の場合、空のエントリ(つまり、現在のディレクトリ)がに追加されますPATH。別のケースが必要だと思います。
CBベイリー

2
@CharlesBailey別ではないcase。ただやるcase "${PATH:=$new}"。同様のフォールバックについては、私自身の回答をご覧ください。
mikeserv

1
@ mc0eシェル関数を使用して「ラインノイズ」を隠す方法の例を追加しました。
John1024

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がある場合、デフォルトでその中の1つが使用されることを意味します。

この同じファイル、別のシェル構成ファイル、またはコマンドラインから、次を使用できます。

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

これがにある場合.bashrc、新しいシェルを開始するときに値が複数回表示されるのを防ぎます。先頭に追加されたものを追加する(つまり、$ PATH内でパスを移動する)場合、またはその逆の場合、自分で行う必要があるという制限があります。


$PATHwithを分割することIFS=:は、最終的にはより柔軟ですcase
mikeserv 14

@mikeserv間違いない。これはcase、IMOの一種のハックです。awkここでもうまく利用できると思います。
goldilocks 14

それは良い点です。そして、私が考えるように、gawk直接割り当てることができます$PATH
mikeserv 14

5

次の方法で実行できます。

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

注:他の変数からPATHを構築する場合、多くのシェルが「」のような「」を解釈するため、それらが空でないことを確認してください。。


+1 manページによると、-qgrepにはPOSIXが必要ですが、それがそれを持たない(POSIX以外の)grepsがまだあることを意味するかどうかはわかりません。
goldilocks 14

1
grepパターンが広すぎることに注意してください。grep / my / bin> / dev / nullの代わりにegrep -q "(^ |:)/ my / bin(:| \ $)"を使用することを検討してください。その修正により、あなたのソリューションは正しいです、そして、これは@ 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ファイルで持ち歩いてきました。私が働いていたラボのシステム管理者によって書かれたと思いますが、よくわかりません。とにかく、それはゴールディロックのアプローチに似ていますが、わずかに異なります:

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

これは私のために働く!しかし、デフォルトではロジックを後回しにし、「before」でオーバーライドします。:)
ケビン・パウリ

pathmungeは、Linux CentOSディストリビューション/ etc / profileの一部であり、前後にパラメーターがあります。私は私の最新のUbuntu 16でそれを見ることはありません
ケミン周

macOS 10.12で問題なく動作するようです/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にPPEND $PATHていますが、この動作を変更することができます-P追加することにより、repend -P引数のリスト内の任意の場所を。もう一度-A渡すことで、保留に戻すことができ-Aます。

安全な評価

ほとんどの場合、人はの使用を避けることをお勧めしますeval。しかし、これは永久に使用されている例として際立っていると思いますこの場合、表示eval できるステートメントはP=or のみ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シェルスクリプトのみを使用し、シェル組み込みのみに依存し、非常に高速です。

% 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 14

+1好奇心から(これは適切なQ&Aである可能性があります)、_シェル関数をプレフィックスすることで「適切に名前空間化された」という考えはどこから来るのでしょうか?他の言語では、通常、内部グローバル関数(つまり、グローバルである必要があるが、APIの一部として外部で使用することを意図していない関数)を示します。私の名前は確かに素晴らしい選択肢ではありませんが、使用_するだけでは衝突の問題はまったく解決しないようです-実際の名前空間にタックすることをお勧めします。mikeserv_path_assign()
goldilocks 14

@ TAFKA'goldilocks '-それをさらに具体的にする方が良いでしょうが、名前が長くなればなるほど、その使用は便利ではなくなります。ただし、適切な実行可能バイナリのプレフィックスが付いている_場合は、パッケージマネージャーを切り替える必要があります。いずれにせよ、これは本質的には「グローバル、内部、関数」です -宣言されたシェルから呼び出されたすべてのシェルに対してグローバルであり、インタープリターのメモリでハングアウトする解釈された言語スクリプトのほんの一部です。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。一方でbashかつzsh静かに受け入れ、主に相対にdirnamesを無視し、ほとんどの場合、相対的にdirnamesは、いずれかの方法で接頭辞hまたはt(およびおそらく他の極悪非道な文字)の両方が恥正樹小林さんた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}ディレクトリ名はほとんど無害ですが、これらは望ましくなく、扱いにくく、やや非効率であり、デバッグ性を妨げ、ドライブの摩耗を促進します。NANDスタイルのSSDは(もちろん)読み取り摩耗の影響を受けませんが、HDDはそうではありません。試行されたコマンドごとに不必要にファイルシステムにアクセスすることは、同じテンポで読み取りヘッドが不必要に消耗することを意味します。その時点で、一見のような無害なワンライナー、ネストされたサブプロセスにネストされたシェルを起動するときに重複が特に油剤ありexport PATH=$PATH:~/wat、急速には、第七のサークルの中に爆発していない、これはあなたの貴重な子供たちに身を任せるようにしてください。${PATH}のような地獄PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat。それに追加のディレクトリ名を追加した場合に役立つのは、Belzebubbaだけです。(

  • 誤ってディレクトリ名が欠落しています。繰り返しますが、欠落している${PATH}ディレクトリ名はほとんど無害ですが、通常は望ましくなく、扱いにくく、やや非効率的で、デバッグ性を妨げ、ドライブの摩耗を促進します。

エルゴ、上記で定義されたシェル関数のようなフレンドリーな自動化。私たちは自分から自分を救わなければなりません。

しかし...なぜ「+ path.append()」ですか?単にappend_path()しないのはなぜですか?

disambiguityのために(例えば、現在では外部コマンドを持つ${PATH}か、システム全体のシェル関数は、他の場所で定義された)、ユーザー定義のシェル関数は、理想的に前置されているかでサポートされているユニークなストリング接尾辞bashzsh、たとえば、のような-しかし、そうでない場合は、標準のコマンドベース名は禁止します+

ねえ。できます。私を判断しないでください。

しかし...なぜ「+ path.append()」ですか?なぜ「+ path.prepend()」ではないのですか?

currentに追加する方がcurrentに追加する${PATH}よりも安全であるため${PATH}、すべてのものが等しくなります。システム全体のコマンドをユーザー固有のコマンドで上書きすることは、せいぜい不衛生であり、最悪の場合は狂気に陥ることがあります。たとえば、Linuxでは、ダウンストリームアプリケーションは通常、カスタムの非標準の派生物や代替物ではなく、GNU coreutilsのコマンドのバリアントを期待しています

そうは言っても、そうするための有効なユースケースは絶対にあります。同等の+path.prepend()関数を定義するのは簡単です。Sans prolix nebulosity、彼と彼女が共有する正気のために:

+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
}

しかし...なぜGillesではないのですか?

他の場所で受け入れられているGilles回答は、一般的なケースでは「シェルに依存しないべき等性付加」として印象的に最適です。一般的な場合bashzshしていない望ましくないシンボリックリンク、しかし、パフォーマンスの低下はとても悲しく行うために必要なのGentoo ricerを私の中で。望ましくないシンボリックリンクが存在する場合でも、add_to_PATH()引数ごとに1つのサブシェルをフォークすることは、シンボリックリンクの重複を挿入する価値があるかどうかは議論の余地があります。

シンボリックリンクの重複さえも排除することを要求する厳しいユースケースの場合、このzsh特定のバリアントは、非効率的なフォークではなく、効率的なビルトインを介してそうします:

+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

Aa修飾子のようにファイル名を絶対パスに変換し、realpath(3)ライブラリ関数に結果を渡してシンボリックリンクを解決します。注:realpath(3)ライブラリー関数を持たないシステムでは、シンボリックリンクは解決されないため、これらのシステムaAは同等です。

それ以上の質問はありません。

どういたしまして。安全な砲撃をお楽しみください。あなたは今、それに値する。


0

これが私の機能プログラミングスタイルのバージョンです。

  • *PATHだけでなく、コロン区切りの変数でも機能しPATHます。
  • グローバル状態にアクセスしません
  • 指定された不変の入力でのみ機能します
  • 単一の出力を生成します
  • 副作用なし
  • メモ可能(原則)

また注目に値する:

  • exporting に関する不可知論者; それは呼び出し元に任されています(例を参照)
  • 純粋なbash; フォークなし
path_add(){
  #$ 1:指定されたパス文字列に確実に1回だけ含まれる要素
  #$ 2:既存のパス文字列値(「PATH」ではなく「$ PATH」)
  #$ 3(オプション、任意):指定されている場合、$ 1を追加します。それ以外の場合、先頭に追加
  #
  #例:
  #$ export PATH = $(path_add '/ opt / bin' "$ PATH")
  #$ CDPATH = $(path_add '/ Music' "$ CDPATH" at_end)

  local -r already_present = "(^ |:)$ {1}($ | :)"
  if [["$ 2" =〜$ already_present]]; それから
    エコー「$ 2」
  elif [[$#== 3]]; それから
    echo "$ {2}:$ {1}"
  他に
    echo "$ {1}:$ {2}"
  fi
}

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
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.