$ 0 จะรวมเส้นทางไปยังสคริปต์หรือไม่


11

ฉันต้องการ grep สคริปต์ปัจจุบันเพื่อให้ฉันสามารถพิมพ์ความช่วยเหลือและข้อมูลรุ่นจากส่วนความเห็นที่ด้านบน

ฉันกำลังคิดถึงสิ่งนี้:

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

แต่ฉันสงสัยว่าจะเกิดอะไรขึ้นถ้าสคริปต์นั้นอยู่ในไดเรกทอรีที่อยู่ใน PATH และถูกเรียกโดยไม่ระบุไดเรกทอรีอย่างชัดเจน

ฉันค้นหาคำอธิบายของตัวแปรพิเศษและพบคำอธิบายต่อไปนี้ของ$0:

  • ชื่อของเชลล์หรือโปรแกรมปัจจุบัน

  • ชื่อไฟล์ของสคริปต์ปัจจุบัน

  • ชื่อของสคริปต์เอง

  • คำสั่งในขณะที่มันถูกเรียกใช้

สิ่งเหล่านี้ไม่ทำให้ชัดเจนว่าค่าของ$0จะรวมถึงไดเรกทอรีหรือไม่หากสคริปต์ถูกเรียกใช้โดยไม่มี อันสุดท้ายบ่งบอกถึงความจริงที่ว่ามันจะไม่เกิดขึ้น

ทดสอบบนระบบของฉัน (Bash 4.1)

ฉันสร้างไฟล์เรียกทำงานได้ใน / usr / local / bin ชื่อscriptnameด้วยหนึ่งบรรทัดecho $0และเรียกใช้จากตำแหน่งอื่น

นี่คือผลลัพธ์ของฉัน:

> 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

ในการทดสอบเหล่านี้ค่าของ$0จะเป็นวิธีที่สคริปต์ถูกเรียกใช้เสมอยกเว้นว่ามีการเรียกใช้โดยไม่มีคอมโพเนนต์พา ธ ใด ๆ ในกรณีที่ค่าของการ$0เป็นเส้นทางที่แน่นอน เพื่อให้ดูเหมือนว่าจะปลอดภัยในการส่งผ่านไปยังคำสั่งอื่น

แต่แล้วฉันก็ได้พบกับข้อคิดเห็นเกี่ยวกับStack Overflowที่ทำให้ฉันสับสน คำตอบแนะนำให้ใช้$(dirname $0)เพื่อรับไดเรกทอรีของสคริปต์ปัจจุบัน ความคิดเห็น (เพิ่มขึ้น 7 ครั้ง) กล่าวว่า "จะไม่ทำงานหากสคริปต์อยู่ในเส้นทางของคุณ"

คำถาม

  • ความคิดเห็นนั้นถูกต้องหรือไม่
  • พฤติกรรมนั้นแตกต่างจากระบบอื่นหรือไม่?
  • มีสถานการณ์ที่$0จะไม่รวมไดเรกทอรีหรือไม่

ผู้คนได้ตอบเกี่ยวกับสถานการณ์ที่$0มีบางอย่างที่ไม่ใช่สคริปต์ซึ่งจะตอบคำถามชื่อ อย่างไรก็ตามฉันยังสนใจในสถานการณ์ที่$0ตัวของสคริปต์เอง แต่ไม่รวมไดเรกทอรี โดยเฉพาะฉันพยายามเข้าใจความคิดเห็นที่ทำไว้ในคำตอบ SO
toxalot

คำตอบ:


17

ในกรณีที่พบบ่อยที่สุด$0จะมีเส้นทางแน่นอนหรือสัมพันธ์กับสคริปต์ดังนั้น

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

(สมมติว่ามีreadlinkคำสั่งและสนับสนุน-e) โดยทั่วไปเป็นวิธีที่ดีพอที่จะรับเส้นทางสัมบูรณ์ซึ่งเป็นที่ยอมรับไปยังสคริปต์

$0 ถูกกำหนดจากอาร์กิวเมนต์ที่ระบุสคริปต์ตามที่ส่งไปยังล่าม

ตัวอย่างเช่นใน:

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

$0the/scriptได้รับ

เมื่อคุณทำงาน:

the/script its args

เปลือกของคุณจะทำ:

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

หากสคริปต์มีตัวอย่าง#! /bin/sh -บางอย่างระบบจะแปลงให้เป็น:

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

(ถ้ามันไม่มี she-bang หรือมากกว่านั้นโดยทั่วไปถ้าระบบส่งคืนข้อผิดพลาด ENOEXEC ดังนั้นเชลล์ของคุณจะทำสิ่งเดียวกัน)

มีข้อยกเว้นสำหรับสคริปต์ setuid / setgid ในบางระบบที่ระบบจะเปิดสคริปต์ในบางfd xและเรียกใช้แทน:

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

เพื่อหลีกเลี่ยงสภาพการแข่งขัน (ในกรณีนี้$0จะมี/dev/fd/x)

ตอนนี้คุณอาจยืนยันว่า/dev/fd/x เป็นเส้นทางไปยังสคริปต์นั้น อย่างไรก็ตามโปรดทราบว่าถ้าคุณอ่านจาก$0คุณจะทำลายสคริปต์ในขณะที่คุณใช้การป้อนข้อมูล

ตอนนี้มีความแตกต่างถ้าชื่อคำสั่งสคริปต์ตามที่เรียกใช้ไม่ได้มีเครื่องหมายทับ ใน:

the-script its args

เปลือกของคุณจะมีลักษณะขึ้นในthe-script อาจมีพา ธ สัมบูรณ์หรือสัมพัทธ์ (รวมถึงสตริงว่าง) ไปยังบางไดเรกทอรี ตัวอย่างเช่นหากมีและพบได้ในไดเรกทอรีปัจจุบันเปลือกจะทำ:$PATH$PATH$PATH/bin:/usr/bin:the-script

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

ซึ่งจะกลายเป็น:

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

หรือหากพบใน/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")

ในทุกกรณีข้างต้นยกเว้นกรณีมุม setuid $0จะมีเส้นทาง (สัมบูรณ์หรือญาติ) ไปยังสคริปต์

ตอนนี้สคริปต์สามารถถูกเรียกเป็น:

the-interpreter the-script its args

เมื่อthe-scriptดังกล่าวข้างต้นไม่มีอักขระทับ, พฤติกรรมแตกต่างกันเล็กน้อยจากเปลือกหอยเพื่อเปลือก

การใช้งาน AT&T แบบเก่าkshนั้นค้นหาสคริปต์โดยไม่มีเงื่อนไข$PATH(ซึ่งจริงๆแล้วเป็นข้อผิดพลาดและช่องโหว่ด้านความปลอดภัยสำหรับสคริปต์ setuid) ดังนั้น$0จริง ๆ แล้วไม่ได้มีเส้นทางไปยังสคริปต์เว้นแต่การ$PATHค้นหาจะเกิดขึ้นจริงthe-scriptในไดเรกทอรีปัจจุบัน

ใหม่กว่า AT&T kshจะพยายามตีความthe-scriptในไดเรกทอรีปัจจุบันหากสามารถอ่านได้ ถ้าไม่ได้ก็จะค้นหาสำหรับอ่านและปฏิบัติการ ในthe-script$PATH

สำหรับbashมันตรวจสอบว่าthe-scriptอยู่ในไดเรกทอรีปัจจุบัน (และไม่ได้เป็น symlink หัก) และหากไม่ได้ค้นหาหาอ่านได้ (ไม่จำเป็นต้องปฏิบัติการ) ในthe-script$PATH

zshในการshจำลองจะทำbashยกเว้นว่าถ้าthe-scriptเป็น symlink ที่ขาดในไดเรกทอรีปัจจุบันมันจะไม่ค้นหาthe-scriptใน$PATHและจะรายงานข้อผิดพลาดแทน

อื่น ๆ ทั้งหมดที่บอร์นเหมือนเปลือกหอยไม่ได้ดูในthe-script$PATH

สำหรับเชลล์เหล่านั้นถ้าคุณพบว่า$0มันไม่มี/และไม่สามารถอ่านได้มันก็อาจจะถูก$PATHค้นหา จากนั้นเป็นไฟล์ใน$PATHมีแนวโน้มที่จะปฏิบัติการได้ก็อาจจะประมาณปลอดภัยที่จะใช้command -v -- "$0"ในการค้นหาเส้นทางของมัน ( แต่ที่จะไม่ทำงานถ้า$0เกิดขึ้นยังเป็นชื่อของ builtin เปลือกหรือคำหลัก (ในเปลือกหอยมากที่สุด))

ดังนั้นหากคุณต้องการที่จะครอบคลุมสำหรับกรณีที่คุณสามารถเขียนมัน:

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

(สิ่งที่""แนบมา$PATHคือเพื่อรักษาองค์ประกอบว่างท้ายด้วยเปลือกหอยซึ่ง$IFSทำหน้าที่เป็นตัวคั่นแทนตัวคั่น )

ตอนนี้มีวิธีที่ลึกลับมากกว่าในการเรียกใช้สคริปต์ เราสามารถทำได้:

the-shell < the-script

หรือ:

cat the-script | the-shell

ในกรณีนั้น$0จะเป็นอาร์กิวเมนต์แรก ( argv[0]) ที่ล่ามได้รับ (ด้านบนthe-shellแต่อาจเป็นอะไรก็ได้โดยทั่วไปแล้วทั้งชื่อฐานหรือเส้นทางเดียวกับล่ามนั้น)

การตรวจจับว่าคุณอยู่ในสถานการณ์นั้นตามค่าของ$0ไม่น่าเชื่อถือ คุณสามารถดูผลลัพธ์ของps -o args= -p "$$"การรับเบาะแส ในกรณีไปป์ไม่มีทางจริงที่คุณสามารถกลับไปที่เส้นทางไปยังสคริปต์ได้

เราสามารถทำได้เช่นกัน:

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

แล้วยกเว้นในzsh(และบางการดำเนินงานเก่าของเปลือกบอร์น) จะเป็น$0 blahอีกครั้งยากที่จะได้รับเส้นทางของสคริปต์ในเปลือกเหล่านั้น

หรือ:

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

เป็นต้น

เพื่อให้แน่ใจว่าคุณมีสิทธิ์$prognameคุณสามารถค้นหาสตริงที่ต้องการใน:

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

แต่อีกครั้งฉันไม่คิดว่ามันคุ้มค่ากับความพยายาม


สเตฟานฉันไม่เข้าใจการใช้งานของคุณ"-"ในตัวอย่างด้านบน จากประสบการณ์ของผมexec("the-script", ["the-script", "its", "args"])จะกลายเป็นexec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"])มีแน่นอนเป็นไปได้ของตัวเลือกล่าม
jrw32982 รองรับ Monica

@ jrw32982 #! /bin/sh -เป็น"มักจะใช้cmd -- somethingถ้าคุณไม่สามารถรับประกันได้ว่าsomethingจะไม่เริ่มต้นด้วย-"สุภาษิตการปฏิบัติที่ดีที่นี่นำไปใช้กับ/bin/sh(ที่-เป็นเครื่องหมายสิ้นสุดของตัวเลือกแบบพกพามากกว่า--) ด้วยsomethingการเป็นเส้นทาง / ชื่อของ ต้นฉบับ หากคุณไม่ได้ใช้ว่าสำหรับสคริปต์ setuid (ในระบบที่สนับสนุนพวกเขา แต่ไม่ได้มี / dev / FD / x วิธีที่กล่าวถึงในคำตอบ) จากนั้นหนึ่งจะได้รับเปลือกรากโดยการสร้าง symlink ให้สคริปต์ของคุณเรียกว่า-iหรือ-sสำหรับ ตัวอย่าง.
Stéphane Chazelas

ขอบคุณStéphane ฉันพลาดยัติภังค์เดี่ยวต่อท้ายในตัวอย่างของคุณ ฉันจะต้องตามล่าหาที่มันเป็นเอกสารที่ยัติภังค์เดียวจะเทียบเท่ากับยัติภังค์คู่pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html
jrw32982 รองรับ Monica

มันง่ายเกินไปที่จะลืมเครื่องหมายยัติภังค์เดี่ยว / คู่ในบรรทัด shebang สำหรับสคริปต์ setuid; อย่างใดระบบควรดูแลให้คุณ ไม่อนุญาตสคริปต์ setuid ทั้งหมดหรือ/bin/shปิดใช้งานการประมวลผลตัวเลือกของตัวเองหากตรวจพบว่ากำลังใช้งาน setuid ฉันไม่เห็นวิธีการใช้ / dev / fd / x ด้วยตัวเองแก้ไขได้ ฉันยังคงต้องการยัติภังค์เดี่ยว / คู่
jrw32982 รองรับ Monica

@ jrw32982, /dev/fd/xเริ่มต้นด้วยไม่ได้/ -เป้าหมายหลักคือการลบสภาพการแข่งขันระหว่างทั้งสองexecve()ว่า (ในระหว่างexecve("the-script")ที่ยกระดับสิทธิ์และต่อมาexecve("interpreter", "thescript")ที่interpreterเปิดวิทยานิพนธ์ในภายหลัง (ซึ่งอาจได้รับการแทนที่ด้วย symlink กับสิ่งอื่นในระหว่างนี้) ระบบที่ใช้ สคริปต์ suid ทำหน้าที่execve("interpreter", "/dev/fd/n")แทนโดยที่ n ถูกเปิดเป็นส่วนหนึ่งของ execve () ครั้งแรก
Stéphane Chazelas

6

ต่อไปนี้เป็นสองสถานการณ์ที่ไม่รวมไดเรกทอรีนี้:

> bash scriptname
scriptname

> bash <scriptname
bash

ในทั้งสองกรณีไดเรกทอรีปัจจุบันจะต้องมีไดเรกทอรีที่scriptnameตั้งอยู่

ในกรณีแรกค่าของ$0ยังสามารถส่งผ่านไปได้grepเนื่องจากถือว่าอาร์กิวเมนต์ FILE สัมพันธ์กับไดเรกทอรีปัจจุบัน

ในกรณีที่สองหากข้อมูลความช่วยเหลือและรุ่นถูกพิมพ์เพื่อตอบสนองต่อตัวเลือกบรรทัดคำสั่งเฉพาะเท่านั้นไม่น่าจะมีปัญหา ฉันไม่แน่ใจว่าทำไมทุกคนถึงเรียกใช้สคริปต์เพื่อพิมพ์ข้อมูลช่วยเหลือหรือรุ่น

คำเตือน

  • หากสคริปต์เปลี่ยนไดเรกทอรีปัจจุบันคุณจะไม่ต้องการใช้พา ธ สัมพัทธ์

  • หากสคริปต์มีที่มาค่าของ$0มักจะเป็นสคริปต์ผู้โทรมากกว่าสคริปต์ที่มา


3

คุณสามารถระบุอาร์กิวเมนต์อาร์บิทอลได้เมื่อใช้-cตัวเลือกเชลล์ส่วนใหญ่ (all?) เช่น:

sh -c 'echo $0' argv0

จากman bash(เลือกอย่างหมดจดเพราะนี่มีคำอธิบายที่ดีกว่าman shการใช้ของฉันเหมือนกันโดยไม่คำนึงถึง):

-ค

หากอ็อพชัน -c มีอยู่คำสั่งจะถูกอ่านจากอาร์กิวเมนต์ไม่ใช่ตัวเลือกแรก command_string หากมีข้อโต้แย้งหลัง command_string พวกเขาจะถูกกำหนดให้กับพารามิเตอร์ตำแหน่งเริ่มต้นด้วย $ 0


ฉันคิดว่าสิ่งนี้ทำงานได้เพราะ -c 'command' เป็นตัวถูกดำเนินการและargv0เป็นอาร์กิวเมนต์บรรทัดคำสั่งที่ไม่ได้ทำงานครั้งแรก
mikeserv

@ ไมค์ถูกต้องฉันได้อัปเดตด้วยข้อมูลโค้ดของผู้ชาย
แกรม

3

หมายเหตุ: คนอื่น ๆ ได้อธิบายกลไกแล้ว$0ดังนั้นฉันจะข้ามสิ่งนั้นไป

readlink -f $0ผมขั้นตอนด้านโดยทั่วไปปัญหานี้ทั้งหมดและเพียงแค่ใช้คำสั่ง สิ่งนี้จะทำให้คุณกลับไปสู่เส้นทางที่เต็มไปด้วยอะไรก็ตามที่คุณให้ไว้เพื่อเป็นข้อโต้แย้ง

ตัวอย่าง

บอกว่าฉันมาที่นี่เพื่อเริ่มต้นด้วย:

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

ทำไดเรกทอรี + ไฟล์:

$ mkdir adir
$ touch afile
$ cd adir/

ตอนนี้เริ่มแสดง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

กลอุบายเพิ่มเติม

ขณะนี้มีผลลัพธ์ที่สอดคล้องกันที่ถูกส่งคืนเมื่อเราสอบปากคำ$0ผ่านreadlinkเราสามารถใช้เพียงdirname $(readlink -f $0)เพื่อให้ได้เส้นทางที่แน่นอนไปยังสคริปต์ - หรือ - basename $(readlink -f $0)เพื่อรับชื่อจริงของสคริปต์


0

manหน้าของฉันพูดว่า:

$0: ขยายไปnameของหรือshellshell script

ดูเหมือนว่าแปลนี้argv[0]ของเปลือกปัจจุบัน - หรือแรกnonoperandอาร์กิวเมนต์ commandline เปลือกปัจจุบันการตีความเป็นอาหารเมื่อเรียก ผมเคยระบุว่าsh ./somescriptเส้นทางหากว่าของตัวแปรที่จะแต่ตอนนี้ไม่ถูกต้องเพราะเป็นกระบวนการใหม่ของตัวเองและเรียกด้วยใหม่$0 $ENV shshell$ENV

ด้วยวิธีนี้sh ./somescript.shแตกต่างจาก. ./somescript.shที่ทำงานในสภาพแวดล้อมปัจจุบันและ$0มีการตั้งค่าแล้ว

คุณสามารถตรวจสอบโดยการเปรียบเทียบ การ$0/proc/$$/status

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

ขอบคุณสำหรับการแก้ไข @toxalot ฉันเรียนรู้บางสิ่ง


ในระบบของฉันถ้ามันมีที่มา ( . ./myscript.shหรือsource ./myscript.sh) จากนั้น$0เป็นเปลือก แต่ถ้ามันถูกส่งผ่านเป็นอาร์กิวเมนต์ไปยังเชลล์ ( sh ./myscript.sh) ก็$0เป็นเส้นทางไปยังสคริปต์ แน่นอนshในระบบของฉันคือทุบตี ดังนั้นฉันไม่รู้ว่ามันสร้างความแตกต่างหรือไม่
toxalot

มันมี hashbang หรือไม่? ฉันคิดว่าความแตกต่างมีความสำคัญ - และฉันสามารถเพิ่มได้โดยที่ไม่ควรจะใช้execมันและไม่ควรจะหาแหล่งที่มาแทน แต่ด้วยความexecเหมาะสม
mikeserv

ไม่ว่าจะมีหรือไม่มีแฮชแบงก์ฉันก็ได้ผลลัพธ์เหมือนกัน มีหรือไม่มีการปฏิบัติการฉันได้รับผลลัพธ์เดียวกัน
toxalot

ฉันด้วย! ฉันคิดว่ามันเป็นเพราะมันเป็นเปลือกในตัว ฉันกำลังตรวจสอบ ...
mikeserv

เส้นบางถูกตีความโดยเคอร์เนล หากคุณ exec ./somescriptกับ bangline ก็จะเทียบเท่ากับการทำงาน#!/bin/sh /bin/sh ./somescriptไม่อย่างนั้นมันก็ไม่ได้ทำให้เชลล์แตกต่างกัน
แกรม

0

ฉันต้องการ grep สคริปต์ปัจจุบันเพื่อให้ฉันสามารถพิมพ์ความช่วยเหลือและข้อมูลรุ่นจากส่วนความเห็นที่ด้านบน

ในขณะที่$0มีชื่อสคริปต์มันอาจจะมีเส้นทางที่นำหน้าขึ้นอยู่กับวิธีสคริปต์ที่เรียกว่าผมได้ใช้เสมอในการพิมพ์ชื่อสคริปต์ในการส่งออกช่วยเหลือที่เอาเส้นทางชั้นนำจาก${0##*/}$0

นำมาจากคู่มือการสคริปต์การทุบตีขั้นสูง - การทดแทนพารามิเตอร์ส่วนที่ 10.2

${var#Pattern}

นำออกจาก$varส่วนที่สั้นที่สุดของที่ตรงกับปลายด้านหน้าของ$Pattern$var

${var##Pattern}

นำออกจาก$varส่วนที่ยาวที่สุดของที่ตรงกับปลายด้านหน้าของ$Pattern$var

ดังนั้นส่วนที่ยาวที่สุดของการ$0จับคู่*/นั้นจะเป็นคำนำหน้าพา ธ ทั้งหมดโดยส่งกลับเฉพาะชื่อสคริปต์


ใช่ฉันก็ทำเช่นกันสำหรับชื่อของสคริปต์ที่ใช้ในข้อความช่วยเหลือ แต่ฉันกำลังพูดถึงgreppingสคริปต์ดังนั้นฉันต้องมีเส้นทางญาติอย่างน้อย ฉันต้องการที่จะใส่ข้อความช่วยเหลือที่ด้านบนในความคิดเห็นและไม่ต้องทำซ้ำข้อความช่วยเหลือในภายหลังเมื่อพิมพ์
toxalot

0

สำหรับสิ่งที่คล้ายกันฉันใช้:

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

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

rPath มีค่าเท่ากันเสมอ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.