Apakah $ 0 akan selalu menyertakan path ke script?

11

Saya ingin membaca skrip saat ini sehingga saya dapat mencetak informasi bantuan dan versi dari bagian komentar di bagian atas.

Saya sedang memikirkan sesuatu seperti ini:

grep '^#h ' -- "$0" | sed -e 's/#h //'

Tapi kemudian saya bertanya-tanya apa yang akan terjadi jika skrip tersebut berada di direktori yang ada di PATH dan dipanggil tanpa secara eksplisit menentukan direktori.

Saya mencari penjelasan tentang variabel-variabel khusus dan menemukan deskripsi berikut $0:

  • nama shell atau program saat ini

  • nama file dari skrip saat ini

  • nama skrip itu sendiri

  • perintah saat dijalankan

Tak satu pun dari ini memperjelas apakah nilai $0akan menyertakan direktori jika skrip dipanggil tanpa itu. Yang terakhir sebenarnya menyiratkan kepada saya bahwa itu tidak akan terjadi.

Menguji Sistem Saya (Bash 4.1)

Saya membuat file yang dapat dieksekusi di / usr / local / bin bernama scriptname dengan satu baris echo $0dan memintanya dari lokasi yang berbeda.

Ini hasil saya:

> cd /usr/local/bin/test
> ../scriptname
../scriptname

> cd /usr/local/bin
> ./scriptname
./scriptname

> cd /usr/local
> bin/scriptname
bin/scriptname

> cd /tmp
> /usr/local/bin/scriptname
/usr/local/bin/scriptname

> scriptname
/usr/local/bin/scriptname

Dalam pengujian ini, nilai $0selalu persis bagaimana skrip dipanggil, kecuali jika dipanggil tanpa komponen path. Dalam hal ini, nilai $0adalah jalur absolut . Sehingga sepertinya aman untuk meneruskan ke perintah lain.

Tapi kemudian saya menemukan komentar tentang Stack Overflow yang membingungkan saya. Jawabannya menyarankan menggunakan $(dirname $0)untuk mendapatkan direktori skrip saat ini. Komentar (7 kali dipilih) mengatakan "itu tidak akan berfungsi jika skrip berada di jalur Anda".

Pertanyaan

  • Apakah komentar itu benar?
  • Apakah perilaku berbeda pada sistem lain?
  • Apakah ada situasi di mana $0tidak termasuk direktori?
toxalot
sumber
Orang-orang telah menjawab tentang situasi di mana $0ada sesuatu selain naskah, yang memang menjawab judul pertanyaan. Namun, saya juga tertarik pada situasi di mana $0skrip itu sendiri, tetapi tidak termasuk direktori. Secara khusus, saya mencoba memahami komentar yang dibuat pada jawaban SO.
toxalot

Jawaban:

17

Dalam kasus yang paling umum, $0akan berisi path, absolut atau relatif terhadap skrip, jadi

script_path=$(readlink -e -- "$0")

(dengan asumsi ada readlinkperintah dan itu mendukung -e) umumnya adalah cara yang cukup baik untuk mendapatkan path absolut kanonik ke skrip.

$0 ditugaskan dari argumen yang menentukan skrip yang diteruskan ke penerjemah.

Misalnya, di:

the-shell -shell-options the/script its args

$0dapatkan the/script.

Ketika Anda menjalankan:

the/script its args

Shell Anda akan melakukan:

exec("the/script", ["the/script", "its", "args"])

Jika skrip berisi #! /bin/sh -she-bang misalnya, sistem akan mengubahnya menjadi:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "the/script", "its", "args"])

(jika tidak mengandung she-bang, atau lebih umum lagi jika sistem mengembalikan kesalahan ENOEXEC, maka shell Anda yang akan melakukan hal yang sama)

Ada pengecualian untuk skrip setuid / setgid pada beberapa sistem, di mana sistem akan membuka skrip pada beberapa fd xdan menjalankannya sebagai gantinya:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "/dev/fd/x", "its", "args"])

untuk menghindari kondisi balapan (dalam hal ini $0akan berisi /dev/fd/x).

Sekarang, Anda mungkin berpendapat bahwa itu /dev/fd/x adalah jalan menuju skrip itu. Namun perhatikan bahwa jika Anda membaca dari $0, Anda akan merusak skrip saat Anda mengkonsumsi input.

Sekarang, ada perbedaan jika nama perintah skrip yang dipanggil tidak mengandung garis miring. Di:

the-script its args

Shell Anda akan mencari the-scriptdi $PATH. $PATHmungkin berisi jalur absolut atau relatif (termasuk string kosong) ke beberapa direktori. Misalnya, jika $PATHberisi /bin:/usr/bin:dan the-scriptditemukan di direktori saat ini, shell akan melakukan:

exec("the-script", ["the-script", "its", "args"])

yang akan menjadi:

exec("/bin/sh", ["/bin/sh" or "the-script", "-", "the-script", "its", "args"]

Atau jika ditemukan di /usr/bin:

exec("/usr/bin/the-script", ["the-script", "its", "args"])
exec("/bin/sh", ["/bin/sh" or "the-script" or "/usr/bin/the-script",
     "-", "/usr/bin/the-script", "its", "args")

Dalam semua kasus di atas kecuali kasus sudut setuid, $0akan berisi path (absolut atau relatif) ke skrip.

Sekarang, skrip juga bisa disebut sebagai:

the-interpreter the-script its args

Ketika the-scriptseperti di atas tidak mengandung karakter garis miring, perilaku sedikit berbeda dari shell ke shell.

kshImplementasi AT&T lama benar-benar mencari skrip tanpa syarat di $PATH(yang sebenarnya adalah bug dan lubang keamanan untuk skrip setuid), jadi $0sebenarnya tidak mengandung jalur ke skrip kecuali $PATHpencarian benar-benar ditemukan the-scriptdi direktori saat ini.

AT&T yang lebih baru kshakan mencoba dan menafsirkan the-scriptdalam direktori saat ini jika itu dapat dibaca. Jika tidak akan mencari yang dapat dibaca dan dieksekusi the-script di $PATH.

Karena bash, ia memeriksa apakah the-scriptada di direktori saat ini (dan bukan merupakan symlink yang rusak) dan jika tidak, cari yang dapat dibaca (belum tentu dapat dieksekusi) the-scriptdi $PATH.

zshdalam shemulasi akan melakukan seperti bashkecuali bahwa jika the-scriptsymlink rusak di direktori saat ini, itu tidak akan mencari the-scriptin $PATHdan malah akan melaporkan kesalahan.

Semua kerang mirip Bourne lainnya tidak terlihat the-scriptmasuk $PATH.

Untuk semua kerang itu, jika Anda menemukan yang $0tidak mengandung a /dan tidak dapat dibaca, maka itu mungkin telah dicari $PATH. Kemudian, karena file-file di $PATHdalamnya kemungkinan dapat dieksekusi, mungkin ini perkiraan yang aman untuk digunakan command -v -- "$0"untuk menemukan jalurnya (meskipun itu tidak akan berhasil jika $0kebetulan juga merupakan nama dari shell builtin atau kata kunci (dalam kebanyakan shell)).

Jadi, jika Anda benar-benar ingin menutupi kasus itu, Anda bisa menulisnya:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}""; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

(yang ""ditambahkan $PATHadalah untuk melestarikan elemen kosong yang tertinggal dengan cangkang yang $IFSbertindak sebagai pembatas bukannya pemisah ).

Sekarang, ada lebih banyak cara esoteris untuk menjalankan skrip. Orang bisa melakukan:

the-shell < the-script

Atau:

cat the-script | the-shell

Dalam hal ini, $0akan menjadi argumen pertama ( argv[0]) yang diterima penerjemah (di atas the-shell, tetapi bisa berupa apa saja meskipun umumnya baik nama dasar atau satu jalur ke penerjemah itu).

Mendeteksi bahwa Anda berada dalam situasi itu berdasarkan nilai $0tidak dapat diandalkan. Anda bisa melihat output ps -o args= -p "$$"untuk mendapatkan petunjuk. Dalam kasus pipa, tidak ada cara nyata Anda bisa kembali ke jalur ke skrip.

Orang juga bisa melakukan:

the-shell -c '. the-script' blah blih

Kemudian, kecuali dalam zsh(dan beberapa implementasi lama dari Bourne shell), $0akan blah. Sekali lagi, sulit untuk mendapatkan jalan naskah di shell tersebut.

Atau:

the-shell -c "$(cat the-script)" blah blih

dll.

Untuk memastikan Anda memiliki hak $progname, Anda dapat mencari string tertentu di dalamnya seperti:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}:; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

[ -f "$progname" ] && grep -q 7YQLVVD3UIUDTA32LSE8U9UOHH < "$progname" ||
  progname=unknown

Tetapi sekali lagi saya tidak berpikir itu sepadan dengan usaha.

Stéphane Chazelas
sumber
Stéphane, saya tidak mengerti penggunaan Anda "-"dalam contoh di atas. Dalam pengalaman saya, exec("the-script", ["the-script", "its", "args"])menjadi exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), tentu saja dengan opsi interpreter.
jrw32982 mendukung Monica
@ jrw32982, #! /bin/sh -adalah "selalu digunakan cmd -- somethingjika Anda tidak dapat menjamin bahwa somethingtidak akan mulai dengan -" pepatah praktik yang baik di sini diterapkan untuk /bin/sh(di mana -penanda akhir opsi lebih portabel daripada --) dengan somethingmenjadi path / nama dari naskah. Jika Anda tidak menggunakannya untuk skrip setuid (pada sistem yang mendukungnya tetapi tidak dengan metode / dev / fd / x yang disebutkan dalam jawaban), maka seseorang dapat memperoleh shell root dengan membuat symlink ke skrip yang dipanggil -iatau -suntuk contoh.
Stéphane Chazelas
Terima kasih, Stéphane. Saya telah melewatkan tanda hubung tunggal di baris shebang contoh Anda. Saya harus mencari di mana didokumentasikan bahwa satu tanda hubung sama dengan tanda hubung ganda pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
jrw32982 mendukung Monica
Terlalu mudah untuk melupakan tanda hubung tunggal / ganda pada baris shebang untuk skrip setuid; entah bagaimana sistem harus mengurusnya untuk Anda. Larang skrip setuid sama sekali atau entah bagaimana /bin/shharus menonaktifkan pemrosesan opsinya sendiri jika dapat mendeteksi bahwa skrip itu menjalankan setuid. Saya tidak melihat bagaimana menggunakan / dev / fd / x dengan sendirinya memperbaiki itu. Anda masih membutuhkan tanda hubung tunggal / ganda, saya pikir.
jrw32982 mendukung Monica
@ jrw32982, /dev/fd/xdimulai dengan /, bukan -. Tujuan utamanya adalah untuk menghapus kondisi balapan antara keduanya execve(), meskipun (di antaranya execve("the-script")yang meningkatkan hak istimewa dan berikutnya di execve("interpreter", "thescript")mana interpretermembuka skrip nanti (yang mungkin telah diganti dengan symlink ke sesuatu yang lain sementara itu). Sistem yang menerapkan skrip suid dengan benar melakukan execve("interpreter", "/dev/fd/n")gantinya di mana n telah dibuka sebagai bagian dari execve pertama ().
Stéphane Chazelas
6

Berikut adalah dua situasi di mana direktori tidak akan dimasukkan:

> bash scriptname
scriptname

> bash <scriptname
bash

Dalam kedua kasus, direktori saat ini harus menjadi direktori tempat scriptname berada.

Dalam kasus pertama, nilai $0masih bisa diteruskan ke grepkarena mengasumsikan argumen FILE relatif terhadap direktori saat ini.

Dalam kasus kedua, jika informasi bantuan dan versi hanya dicetak sebagai respons terhadap opsi baris perintah tertentu, itu seharusnya tidak menjadi masalah. Saya tidak yakin mengapa ada orang yang meminta skrip seperti itu untuk mencetak bantuan atau informasi versi.

Peringatan

  • Jika skrip mengubah direktori saat ini, Anda tidak ingin menggunakan jalur relatif.

  • Jika skrip bersumber, nilai $0biasanya akan menjadi skrip penelepon daripada skrip bersumber.

toxalot
sumber
3

Argumen nol arbitrer dapat ditentukan saat menggunakan -copsi untuk sebagian besar (semua?) Shell. Misalnya:

sh -c 'echo $0' argv0

Dari man bash(dipilih semata-mata karena ini memiliki deskripsi yang lebih baik daripada saya man sh- penggunaannya tetap sama):

-c

Jika opsi -c hadir, maka perintah dibaca dari argumen non-opsi pertama command_string. Jika ada argumen setelah command_string, mereka ditugaskan ke parameter posisi, dimulai dengan $ 0.

Graeme
sumber
Saya pikir ini berfungsi karena -c 'command' adalah operan, dan argv0argumen commandline nonoperand pertamanya.
mikeserv
@ Mike benar, saya telah memperbarui dengan cuplikan pria.
Graeme
3

CATATAN: Lainnya sudah menjelaskan mekanisme $0jadi saya akan melewatkan semua itu.

Saya biasanya memihak seluruh masalah ini dan cukup menggunakan perintah readlink -f $0. Ini akan selalu memberi Anda kembali jalan penuh dari apa pun yang Anda berikan sebagai argumen.

Contohnya

Katakan saya di sini untuk memulai dengan:

$ pwd
/home/saml/tst/119929/adir

Buat file direktori +:

$ mkdir adir
$ touch afile
$ cd adir/

Sekarang mulai pamer readlink:

$ readlink -f ../adir
/home/saml/tst/119929/adir

$ readlink -f ../
/home/saml/tst/119929

$ readlink -f ../afile 
/home/saml/tst/119929/afile

$ readlink -f .
/home/saml/tst/119929/adir

Tipu daya tambahan

Sekarang dengan hasil yang konsisten dikembalikan ketika kita menginterogasi $0melalui readlinkkita dapat menggunakan hanya dirname $(readlink -f $0)untuk mendapatkan path absolut ke skrip -atau- basename $(readlink -f $0)untuk mendapatkan nama sebenarnya dari skrip.

slm
sumber
0

manHalaman saya mengatakan:

$0: Memperluas ke namedari shellatau shell script.

Tampaknya ini diterjemahkan ke argv[0]dari shell saat ini - atau argumen commandline nonoperand pertama yang diumpankan shell diumpankan saat dipanggil. Saya sebelumnya menyatakan bahwa sh ./somescriptakan path variabelnya ke tetapi ini tidak benar karena ini adalah proses baru sendiri dan dipanggil dengan yang baru .$0 $ENV shshell$ENV

Dengan cara ini sh ./somescript.shberbeda dari . ./somescript.shyang berjalan di lingkungan saat ini dan $0sudah diatur.

Anda dapat memeriksa ini dengan membandingkan $0ke /proc/$$/status.

echo 'script="/proc/$$/status"
    echo $0
    cat "$script"' \
    > ./script.sh
sh ./script.sh ; . ./script.sh

Terima kasih atas koreksinya, @toxalot. Saya belajar sesuatu.

mikeserv
sumber
Di sistem saya, jika bersumber ( . ./myscript.shatau source ./myscript.sh), maka $0shell. Tetapi jika itu diteruskan sebagai argumen ke shell ( sh ./myscript.sh), maka $0adalah jalan menuju skrip. Tentu saja, shpada sistem saya adalah Bash. Jadi saya tidak tahu apakah itu membuat perbedaan atau tidak.
toxalot
Apakah ada hashbang? Saya pikir perbedaan itu penting - dan saya dapat menambahkan itu - tanpa itu seharusnya tidak seharusnya execdan sebaliknya sumber, tetapi dengan itu, seharusnya exec.
mikeserv
Dengan atau tanpa hashbang, saya mendapatkan hasil yang sama. Dengan atau tanpa dapat dieksekusi, saya mendapatkan hasil yang sama.
toxalot
Saya juga! Saya pikir itu karena shell builtin. Saya sedang memeriksa ...
mikeserv
Garis bang ditafsirkan oleh kernel. Jika Anda ./somescriptmengeksekusi dengan bangline #!/bin/sh, itu akan setara dengan berjalan /bin/sh ./somescript. Kalau tidak, mereka tidak membuat perbedaan pada shell.
Graeme
0

Saya ingin membaca skrip saat ini sehingga saya dapat mencetak informasi bantuan dan versi dari bagian komentar di bagian atas.

Meskipun $0berisi nama skrip yang mungkin berisi lintasan awalan berdasarkan pada cara skrip dipanggil, saya selalu digunakan ${0##*/}untuk mencetak nama skrip dalam output bantuan yang menghilangkan lintasan utama apa pun dari $0.

Diambil dari panduan Advanced Bash Scripting - Bagian 10.2 penggantian parameter

${var#Pattern}

Hapus dari $varbagian terpendek $Patternyang cocok dengan ujung depan $var.

${var##Pattern}

Hapus dari $varbagian terpanjang $Patternyang cocok dengan ujung depan $var.

Jadi bagian terpanjang dari $0kecocokan itu */akan menjadi awalan seluruh jalur, hanya mengembalikan nama skrip.

sambler
sumber
Ya, saya juga melakukannya untuk nama skrip yang digunakan dalam pesan bantuan. Tapi saya bicarakan grepping script jadi saya harus memiliki setidaknya path relatif. Saya ingin meletakkan pesan bantuan di bagian atas di komentar dan tidak harus mengulangi pesan bantuan nanti ketika mencetaknya.
toxalot
0

Untuk hal serupa saya gunakan:

rPath="$(dirname $(realpath $0))"
echo $rPath 

rPath=$(dirname $(readlink -e -- "$0"))
echo $rPath 

rPath selalu memiliki nilai yang sama.

Anonim
sumber