Bagaimana saya bisa menambahkan $ PATH dengan bersih?


31

Saya ingin cara menambahkan sesuatu ke $ PATH, seluruh sistem atau untuk pengguna individu, tanpa berpotensi menambahkan jalur yang sama beberapa kali.

Salah satu alasan untuk ingin melakukan ini adalah agar penambahan dapat dibuat .bashrc, yang tidak memerlukan login, dan juga lebih berguna pada sistem yang menggunakan (misalnya) lightdm, yang tidak pernah menelepon .profile.

Saya menyadari pertanyaan yang berhubungan dengan cara membersihkan duplikat dari $ PATH, tetapi saya tidak ingin menghapus duplikat . Saya ingin cara menambahkan jalur hanya jika belum ada.



Goldi, saya tidak tahu mengapa, tetapi saya telah melihat komentar pertama Anda bahkan dengan kosong. Tapi ya, awalan nama juga berfungsi, jangan khawatir! Menutup cara lain juga baik-baik saja.
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

Oke, selama Anda mendapat pesan saya. Terkadang melakukan pembalikan seperti ini menyebabkan sedikit kekacauan, saya kira kita akan melihat apa yang terjadi.
goldilocks

Jawaban:


35

Misalkan jalur baru yang ingin kita tambahkan adalah:

new=/opt/bin

Kemudian, menggunakan shell POSIX apa pun, kita dapat menguji untuk melihat apakah newsudah ada di path dan menambahkannya jika belum:

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

Perhatikan penggunaan titik dua. Tanpa titik dua, kita mungkin berpikir bahwa, katakanlah, new=/binsudah berada di jalan karena polanya cocok /usr/bin. Walaupun PATH biasanya memiliki banyak elemen, kasus khusus nol dan satu elemen dalam PATH juga ditangani. Kasus PATH awalnya tidak memiliki elemen (kosong) ditangani dengan penggunaan ${PATH:=$new}yang ditugaskan PATHuntuk$new jika kosong. Mengatur nilai default untuk parameter dengan cara ini adalah fitur dari semua shell POSIX: lihat bagian 2.6.2 dari dokumen POSIX .)

Fungsi yang bisa dipanggil

Untuk kenyamanan, kode di atas dapat dimasukkan ke dalam suatu fungsi. Fungsi ini dapat didefinisikan pada baris perintah atau, agar tersedia secara permanen, dimasukkan ke dalam skrip inisialisasi shell Anda (Untuk pengguna bash, itu adalah ~/.bashrc):

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

Untuk menggunakan fungsi pembaruan jalur ini untuk menambahkan direktori ke PATH saat ini:

pupdate /new/path

@hammar OK. Saya menambahkan case untuk itu.
John1024

1
Anda dapat menyimpan 2 perbedaan kasus - lih. unix.stackexchange.com/a/40973/1131 .
maxschlepzig

3
Jika PATHkosong, ini akan menambah entri kosong (yaitu direktori saat ini) ke PATH. Saya pikir Anda perlu kasus lain.
CB Bailey

2
@CharlesBailey Bukan yang lain case. Lakukan saja case "${PATH:=$new}". Lihat jawaban saya sendiri untuk fallback serupa.
mikeserv

1
@ MC0E Saya menambahkan contoh tentang bagaimana menggunakan fungsi shell untuk menyembunyikan "line-noise."
John1024

9

Buat file dengan /etc/profile.dnama, misalnya, mypath.sh(atau apa pun yang Anda inginkan). Jika Anda menggunakan lightdm, pastikan itu layak atau menggunakan /etc/bashrcfile lain. Tambahkan ke bahwa fungsi-fungsi berikut:

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
}

Hal-hal di awal (disesuaikan dengan) $ PATH didahulukan dari yang berikut, dan sebaliknya, hal-hal di akhir (ditambahkan) akan digantikan oleh apa yang datang sebelumnya. Ini berarti jika $ PATH Anda /usr/local/bin:/usr/bindan ada yang dapat dieksekusi gotchadi kedua direktori, yang di/usr/local/bin akan digunakan secara default.

Anda sekarang dapat - dalam file yang sama ini, di file konfigurasi shell lain, atau dari commandline - gunakan:

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

Jika ini dalam a .bashrc, itu akan mencegah nilai muncul lebih dari sekali ketika Anda memulai shell baru. Ada batasan dalam hal itu jika Anda ingin menambahkan sesuatu yang sebelumnya (misalnya memindahkan jalur dalam $ PATH) atau sebaliknya, Anda harus melakukannya sendiri.


Memecah $PATHdengan IFS=:akhirnya lebih fleksibel daripada case.
mikeserv

@ mikeserv Tidak diragukan lagi. Ini sejenis hack yang digunakan untuk case, IMO. Saya membayangkan awkbisa dimanfaatkan dengan baik di sini juga.
goldilocks

Itu poin yang bagus. Dan, seperti yang saya pikirkan, gawkbisa langsung ditugaskan $PATH.
mikeserv

5

Anda bisa melakukannya dengan cara ini:

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

Catatan: jika Anda membuat PATH dari variabel lain, periksa apakah mereka tidak kosong, karena banyak shell mengartikan "" seperti "." .


+1 Menurut halaman manual -qdiperlukan oleh POSIX untuk grep, tapi saya tidak tahu apakah itu berarti masih ada beberapa (non POSIX) greps yang tidak memilikinya.
goldilocks

1
perhatikan bahwa pola grep terlalu luas. Pertimbangkan untuk menggunakan egrep -q "(^ |:) / my / bin (: | \ $)" daripada grep / my / bin> / dev / null. Dengan modifikasi itu solusi Anda sudah benar, dan saya pikir ini adalah solusi yang lebih mudah dibaca daripada jawaban yang saat ini disukai dari @ john1024. Perhatikan bahwa saya telah menggunakan tanda kutip ganda sehingga Anda menggunakan substitusi variabel sebagai pengganti/my/bin
mc0e

5

Bagian penting dari kode adalah untuk memeriksa apakah PATHmengandung jalur tertentu:

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

Yaitu, pastikan bahwa setiap jalur di PATHdibatasi pada kedua sisi oleh PATHpemisah ( :), lalu periksa ( -q) apakah string literal ( -F) yang terdiri dari PATHpemisah, jalur Anda, dan PATHpemisah lain ada di sana. Jika tidak, Anda dapat dengan aman menambahkan jalur:

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

Ini harus kompatibel POSIX , dan harus bekerja dengan lintasan apa pun yang tidak mengandung karakter baris baru. Ini lebih kompleks jika Anda ingin bekerja dengan jalur yang berisi baris baru saat POSIX kompatibel, tetapi jika Anda memiliki grepyang mendukung -zAnda dapat menggunakannya.


4

Saya telah membawa fungsi kecil ini bersamaku di berbagai ~/.profilefile selama bertahun-tahun. Saya pikir itu ditulis oleh sysadmin di laboratorium tempat saya dulu bekerja tetapi saya tidak yakin. Bagaimanapun, ini mirip dengan pendekatan Goldilock tetapi sedikit berbeda:

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

Jadi, untuk menambahkan direktori baru ke awal PATH:

pathmunge /new/path

dan sampai akhir:

pathmunge /new/path after

Ini bekerja untuk saya! Tapi saya bertukar logika untuk meletakkannya setelah secara default, dan menimpa dengan "sebelum". :)
Kevin Pauli

pathmunge adalah bagian dari distribusi centos linux / etc / profile, ia memiliki parameter sebelum dan sesudah. Saya tidak melihatnya di ubuntu 16 terbaru saya.
Kemin Zhou

Tampaknya berfungsi dengan baik pada macOS 10.12 setelah /bin/grep->grep
Ben Creasy

4

MEMPERBARUI:

Saya perhatikan jawaban Anda sendiri memiliki fungsi terpisah masing-masing untuk menambahkan atau menambahkan ke $PATH. Saya menyukai ide itu. Jadi saya menambahkan sedikit penanganan argumen. Saya juga _menempatkan nama dengan benar :

_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

KELUARAN:

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

Secara default ia akan -Amembayar $PATH, tetapi Anda dapat mengubah perilaku ini untuk -Prepend dengan menambahkan -Pdi mana saja di daftar argumen Anda. Anda dapat mengubahnya kembali ke -Appending dengan menyerahkannya a-A lagi.

EVAL AMAN

Dalam kebanyakan kasus, saya menyarankan agar orang menghindari penggunaan eval. Tapi ini, saya pikir, menonjol sebagai contoh penggunaannya untuk kebaikan. Dalam hal ini satu-satunya pernyataan yang eval bisa dilihat adalah P=atau A=. Nilai-nilai argumennya diuji secara ketat sebelum dipanggil. Ini untuk apa 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
}

Ini akan menerima argumen sebanyak yang Anda berikan dan tambahkan masing-masing $PATHhanya satu kali dan hanya jika belum ada $PATH. Itu menggunakan hanya shell-script POSIX sepenuhnya portabel, hanya bergantung pada built-in shell, dan sangat cepat.

% 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 'lihat pembaruan di sini - Anda menginspirasi saya.
mikeserv

+1 Karena penasaran (mungkin ini adalah T&J terpisah yang bagus), dari mana ide bahwa _awalan fungsi shell membuat mereka "benar-benar diberi nama"? Dalam bahasa lain, biasanya akan menunjukkan fungsi global internal (yaitu, yang perlu global, tetapi tidak dimaksudkan untuk digunakan secara eksternal sebagai bagian dari API). Nama saya tentu saja bukan pilihan yang bagus, tetapi bagi saya sepertinya hanya menggunakan _tidak menyelesaikan masalah tabrakan sama sekali - akan lebih baik untuk memakukan pada namespace yang sebenarnya, misalnya. mikeserv_path_assign().
goldilocks

@ TAFKA'goldilocks '- akan lebih baik untuk mendapatkan lebih spesifik dengannya, tetapi semakin lama namanya semakin tidak nyaman penggunaannya. Tetapi jika Anda memiliki awalan biner yang dapat dieksekusi yang tepat _maka Anda perlu mengganti manajer paket. Bagaimanapun, ini, pada dasarnya, hanyalah sebuah "global, internal, fungsi" - itu global untuk setiap shell yang dipanggil dari shell di mana ia dinyatakan, dan itu hanya sedikit naskah bahasa yang ditafsirkan nongkrong dalam memori juru bahasa . unix.stackexchange.com/questions/120528/…
mikeserv

Tidak bisakah Anda unset a(atau yang setara) di akhir profil?
sourcejedi

0

Melihat! 12-line kekuatan-industri ... fungsi bash- dan zsh-portable shell yang secara khusus mencintai skrip startup ~/.bashrcatau ~/.zshrcpilihan Anda:

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

Persiapkan dirimu untuk kemuliaan sesaat. Kemudian, daripada melakukan ini dan berharap yang terbaik:

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

Lakukan ini sebagai gantinya dan dijamin mendapatkan yang terbaik, baik Anda benar-benar menginginkannya atau tidak:

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

Baiklah, Tentukan "Terbaik."

Aman menambahkan dan menambahkan ke arus ${PATH}bukanlah masalah sepele seperti yang biasa terjadi. Sementara nyaman dan tampaknya masuk akal, one-liners formulir export PATH=$PATH:~/opt/binmengundang komplikasi jahat dengan:

  • Dirnames relatif tidak sengaja (misalnya, export PATH=$PATH:opt/bin). Sementara bashdan zshdiam - diam menerima dan kebanyakan mengabaikan nama relatif dalam kebanyakan kasus, nama relatif relatif diawali oleh salah satu hatau t(dan mungkin karakter jahat lainnya) menyebabkan keduanya memutihkan diri mereka sendiri secara memalukan diri sendiri karya mahakaryanya pada tahun 1962 karya Masaki Kobayashi karya Harakiri :

    # 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.
  • Duplikasi nama direktori secara tidak sengaja. Meskipun nama ganda duplikat ${PATH}sebagian besar tidak berbahaya, mereka juga tidak diinginkan, rumit, sedikit tidak efisien, menghambat debuggability, dan mempromosikan drive wear - agak seperti jawaban ini. Sementara SSD gaya NAND ( tentu saja ) kebal terhadap keausan baca, HDD tidak. Akses sistem file yang tidak perlu pada setiap perintah yang dicoba menyiratkan keausan baca yang tidak perlu pada tempo yang sama. Duplikat khususnya tidak berbahaya ketika menggunakan cangkang bersarang di subproses bersarang, di mana titik yang tampaknya tidak berbahaya satu-garis seperti export PATH=$PATH:~/watdengan cepat meledak ke dalam Lingkaran ${PATH}Neraka Ke Tujuh seperti PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat. Hanya Beelzebubba yang dapat membantu Anda jika Anda kemudian menambahkan nama pengguna tambahan ke dalamnya. (Jangan biarkan ini terjadi pada anak-anakmu yang berharga. )

  • Dirnames yang hilang secara tidak sengaja. Sekali lagi, meskipun nama yang hilang ${PATH}sebagian besar tidak berbahaya, mereka juga biasanya tidak diinginkan, rumit, sedikit tidak efisien, menghambat debuggability, dan mempromosikan drive wear.

Ergo, otomatisasi ramah seperti fungsi shell yang didefinisikan di atas. Kita harus menyelamatkan diri kita sendiri.

Tapi ... Kenapa "+ path.append ()"? Kenapa Tidak Cukup append_path ()?

Untuk disambiguitas (misalnya, dengan perintah eksternal dalam ${PATH}fungsi shell saat ini atau di seluruh sistem yang didefinisikan di tempat lain), fungsi shell yang ditentukan pengguna idealnya diawali atau diakhiri dengan substring unik yang didukung oleh bashdan zshtetapi sebaliknya dilarang untuk nama-nama perintah standar - seperti, misalnya +,.

Hei. Berhasil. Jangan menilai saya.

Tapi ... Kenapa "+ path.append ()"? Mengapa Tidak "+ path.prepend ()"?

Karena menambahkan ke arus ${PATH}lebih aman daripada bergantung pada arus ${PATH}, semua hal dianggap sama, yang tidak pernah ada. Mengganti perintah di seluruh sistem dengan perintah khusus pengguna dapat menjadi tidak bersih paling baik dan paling tidak membuat gila. Di Linux, misalnya, aplikasi hilir umumnya mengharapkan varian perintah GNU coreutils daripada turunan atau alternatif non-standar khusus.

Yang mengatakan, benar-benar ada kasus penggunaan yang valid untuk melakukannya. Menentukan +path.prepend()fungsi yang setara itu sepele. Sans prolix nebulosity, untuk kewarasannya bersama:

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

Tapi ... Kenapa Tidak Gilles?

Gilles ' jawaban diterima di tempat lain optimal mengesankan dalam kasus umum sebagai 'shell agnostik idempoten append' . Dalam kasus umum bashdan zshdengan tidak ada symlink yang tidak diinginkan, bagaimanapun, hukuman kinerja yang diperlukan untuk melakukannya sedih yang ricer Gentoo dalam diriku. Bahkan di hadapan symlink yang tidak diinginkan, masih diperdebatkan apakah forking satu subshell per add_to_PATH()argumen bernilai potensi penyisipan duplikat symlink.

Untuk kasus penggunaan yang ketat yang menuntut duplikat symlink dihilangkan, zshvarian khusus ini melakukannya melalui builtin yang efisien dan bukannya garpu yang tidak efisien:

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

Perhatikan *":${dirname:A}:"*bukan *":${dirname}:"*yang asli. :Aadalah zsh-ism yang luar biasa sayangnya tidak ada di bawah sebagian besar kerang lainnya - termasuk bash. Mengutip man zshexpn:

J : Ubah nama file menjadi jalur absolut seperti yang dilakukan apengubah, dan kemudian berikan hasilnya melalui realpath(3)fungsi pustaka untuk menyelesaikan tautan simbolik. Catatan: pada sistem yang tidak memiliki realpath(3)fungsi pustaka, tautan simbolik tidak terselesaikan, demikian juga pada sistem tersebut adan Asetara.

Tidak ada pertanyaan lebih lanjut.

Sama-sama. Nikmati penembakan yang aman. Anda sekarang layak menerimanya.


0

Ini versi fungsional-gaya pemrograman saya.

  • Berfungsi untuk *PATHvariabel yang dibatasi oleh titik dua , tidak hanya PATH.
  • Tidak mengakses negara global
  • Hanya bekerja dengan / pada input tetap yang diberikan
  • Menghasilkan satu output
  • Tidak ada efek samping
  • Memoizable (pada prinsipnya)

Juga patut diperhatikan:

  • Agnostik tentang exporting; yang tersisa untuk penelepon (lihat contoh)
  • Murni bash; tidak bercabang
path_add () {
  # $ 1: Elemen untuk memastikan ada di string jalur yang diberikan tepat satu kali
  # $ 2: Nilai string jalur yang ada ("$ PATH", bukan "PATH")
  # $ 3 (opsional, apa saja): Jika diberikan, tambahkan $ 1; jika tidak, tambahkan
  #
  # Contoh:
  # $ export PATH = $ (path_add '/ opt / bin' "$ PATH")
  # $ CDPATH = $ (path_add '/ Music' "$ CDPATH" at_end)

  local -r already_present = "(^ |:) $ {1} ($ | :)"
  if [["$ 2" = ~ $ already_present]]; kemudian
    gema "$ 2"
  elif [[$ # == 3]]; kemudian
    gema "$ {2}: $ {1}"
  lain
    gema "$ {1}: $ {2}"
  fi
}

0

Script ini memungkinkan Anda untuk menambahkan di akhir $PATH:

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

Atau tambahkan di awal $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
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.