$ 0 sẽ luôn bao gồm đường dẫn đến tập lệnh?


11

Tôi muốn grep tập lệnh hiện tại để tôi có thể in thông tin trợ giúp và phiên bản từ phần bình luận ở trên cùng.

Tôi đã nghĩ về một cái gì đó như thế này:

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

Nhưng sau đó tôi tự hỏi điều gì sẽ xảy ra nếu tập lệnh được đặt trong một thư mục nằm trong PATH và được gọi mà không chỉ định rõ ràng thư mục.

Tôi đã tìm kiếm một lời giải thích về các biến đặc biệt và tìm thấy các mô tả sau về $0:

  • Tên của chương trình hoặc trình bao hiện tại

  • tên tệp của tập lệnh hiện tại

  • tên của chính kịch bản

  • lệnh khi nó được chạy

Không ai trong số này làm rõ liệu giá trị của $0sẽ bao gồm thư mục hay không nếu tập lệnh được gọi mà không có nó. Điều cuối cùng thực sự ngụ ý với tôi rằng nó sẽ không.

Kiểm tra trên hệ thống của tôi (Bash 4.1)

Tôi đã tạo một tệp thực thi trong / usr / local / bin được gọi là scriptname với một dòng echo $0và gọi nó từ các vị trí khác nhau.

Đây là kết quả của tôi:

> 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

Trong các thử nghiệm này, giá trị của $0luôn luôn chính xác là cách tập lệnh được gọi, ngoại trừ nếu nó được gọi mà không có bất kỳ thành phần đường dẫn nào. Trong trường hợp đó, giá trị của $0đường dẫn tuyệt đối . Vì vậy, có vẻ như nó sẽ an toàn để chuyển sang một lệnh khác.

Nhưng sau đó tôi bắt gặp một bình luận về Stack Overflow khiến tôi bối rối. Câu trả lời gợi ý sử dụng $(dirname $0)để có được thư mục của tập lệnh hiện tại. Nhận xét (được trích dẫn 7 lần) cho biết "sẽ không hoạt động nếu tập lệnh nằm trong đường dẫn của bạn".

Câu hỏi

  • Nhận xét đó có đúng không?
  • Là hành vi khác nhau trên các hệ thống khác?
  • Có những tình huống $0sẽ không bao gồm các thư mục?

Mọi người đã trả lời về các tình huống ở đâu $0đó ngoài kịch bản, câu trả lời cho tiêu đề câu hỏi. Tuy nhiên, tôi cũng quan tâm đến các tình huống trong đó $0là chính kịch bản, nhưng không bao gồm thư mục. Cụ thể, tôi đang cố gắng hiểu nhận xét về câu trả lời SO.
độc tố

Câu trả lời:


17

Trong các trường hợp phổ biến nhất, $0sẽ chứa một đường dẫn, tuyệt đối hoặc liên quan đến tập lệnh, vì vậy

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

(giả sử có một readlinklệnh và nó hỗ trợ -e) nói chung là một cách đủ tốt để có được đường dẫn tuyệt đối chính tắc đến tập lệnh.

$0 được gán từ đối số chỉ định tập lệnh được truyền cho trình thông dịch.

Ví dụ: trong:

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

$0được the/script.

Khi bạn chạy:

the/script its args

Vỏ của bạn sẽ làm một:

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

Nếu tập lệnh chứa một #! /bin/sh -she-bang chẳng hạn, hệ thống sẽ chuyển đổi nó thành:

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

(nếu nó không chứa tiếng nổ hoặc nói chung là nếu hệ thống trả về lỗi ENOEXEC, thì vỏ của bạn sẽ làm điều tương tự)

Có một ngoại lệ cho tập lệnh setuid / setgid trên một số hệ thống, trong đó hệ thống sẽ mở tập lệnh trên một số fd xvà chạy thay thế:

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

để tránh các điều kiện chủng tộc (trong trường hợp $0này sẽ chứa /dev/fd/x).

Bây giờ, bạn có thể lập luận rằng đó /dev/fd/x một đường dẫn đến kịch bản đó. Tuy nhiên, lưu ý rằng nếu bạn đọc từ $0, bạn sẽ phá vỡ tập lệnh khi bạn sử dụng đầu vào.

Bây giờ, có một sự khác biệt nếu tên lệnh script được gọi không chứa dấu gạch chéo. Trong:

the-script its args

Vỏ của bạn sẽ nhìn lên the-scripttrong $PATH. $PATHcó thể chứa đường dẫn tuyệt đối hoặc tương đối (bao gồm cả chuỗi rỗng) đến một số thư mục. Chẳng hạn, nếu $PATHchứa /bin:/usr/bin:the-scriptđược tìm thấy trong thư mục hiện tại, shell sẽ thực hiện:

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

mà sẽ trở thành:

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

Hoặc nếu nó được tìm thấy trong /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")

Trong tất cả các trường hợp trên trừ trường hợp góc setuid, $0sẽ chứa đường dẫn (tuyệt đối hoặc tương đối) đến tập lệnh.

Bây giờ, một tập lệnh cũng có thể được gọi là:

the-interpreter the-script its args

Khi the-scriptnhư trên không chứa các ký tự gạch chéo, hành vi thay đổi một chút từ vỏ này sang vỏ khác.

Các kshtriển khai AT & T cũ thực sự đang tìm kiếm tập lệnh vô điều kiện $PATH(đây thực sự là một lỗi và lỗ hổng bảo mật cho tập lệnh setuid), vì vậy $0thực sự không có đường dẫn đến tập lệnh trừ khi việc $PATHtìm kiếm thực sự tìm thấy the-scripttrong thư mục hiện tại.

AT & T mới hơn kshsẽ thử và diễn giải the-scripttrong thư mục hiện tại nếu có thể đọc được. Nếu không nó sẽ tìm kiếm một có thể đọc và thực thi the-script trong $PATH.

Đối với bash, nó kiểm tra nếu the-scriptlà trong thư mục hiện tại (và không phải là một liên kết tượng trưng tấm) và nếu không, tra cứu cho một thể đọc được (không nhất thiết phải thực thi) the-scripttrong $PATH.

zshtrong shthi đua sẽ làm như bashtrừ rằng nếu the-scriptlà một liên kết tượng trưng vỡ trong thư mục hiện, nó sẽ không tìm kiếm một the-scripttrong $PATHvà thay vào đó sẽ báo cáo một lỗi.

Tất cả các vỏ giống như Bourne khác không nhìn the-scriptlên $PATH.

Đối với tất cả các shell đó, nếu bạn thấy rằng $0nó không chứa a /và không thể đọc được, thì có lẽ nó đã được tra cứu $PATH. Sau đó, vì các tệp trong $PATHcó khả năng được thực thi, nên có thể sử dụng xấp xỉ an toàn command -v -- "$0"để tìm đường dẫn của nó (mặc dù điều đó sẽ không hoạt động nếu $0cũng là tên của shell dựng sẵn hoặc từ khóa (trong hầu hết các shell)).

Vì vậy, nếu bạn thực sự muốn bao gồm cho trường hợp đó, bạn có thể viết nó:

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

(phần ""bổ sung $PATHlà để bảo toàn phần tử rỗng có dấu với các lớp vỏ có $IFSvai trò là dấu phân cách thay vì dấu phân cách ).

Bây giờ, có nhiều cách bí truyền hơn để gọi một kịch bản. Người ta có thể làm:

the-shell < the-script

Hoặc là:

cat the-script | the-shell

Trong trường hợp đó, $0sẽ là đối số đầu tiên ( argv[0]) mà trình thông dịch nhận được (ở trên the-shell, nhưng đó có thể là bất cứ thứ gì mặc dù nói chung là tên cơ sở hoặc một đường dẫn đến trình thông dịch đó).

Phát hiện ra rằng bạn đang ở trong tình huống đó dựa trên giá trị $0không đáng tin cậy. Bạn có thể nhìn vào đầu ra ps -o args= -p "$$"để có được manh mối. Trong trường hợp ống, không có cách nào thực sự bạn có thể quay lại đường dẫn đến kịch bản.

Người ta cũng có thể làm:

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

Sau đó, ngoại trừ trong zsh(và một số triển khai cũ của vỏ Bourne), $0sẽ là blah. Một lần nữa, khó có thể đi đến đường dẫn của kịch bản trong các shell đó.

Hoặc là:

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

Vân vân.

Để đảm bảo bạn có quyền $progname, bạn có thể tìm kiếm một chuỗi cụ thể trong đó như:

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

Nhưng một lần nữa tôi không nghĩ rằng nó đáng để nỗ lực.


Stéphane, tôi không hiểu việc bạn sử dụng "-"trong các ví dụ trên. Theo kinh nghiệm của tôi, exec("the-script", ["the-script", "its", "args"])trở nên exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), với tất nhiên khả năng là một lựa chọn thông dịch viên.
jrw32982 hỗ trợ Monica

@ jrw32982, #! /bin/sh -"luôn luôn sử dụng cmd -- somethingnếu bạn không thể đảm bảo rằng somethingsẽ không bắt đầu bằng -" câu ngạn ngữ thực hành tốt ở đây được áp dụng cho /bin/sh(khi -điểm đánh dấu tùy chọn dễ mang theo hơn --) với somethingđường dẫn / tên của kịch bản. Nếu bạn không sử dụng điều đó cho các tập lệnh setuid (trên các hệ thống hỗ trợ chúng nhưng không sử dụng phương thức / dev / fd / x được đề cập trong câu trả lời), thì người ta có thể lấy shell gốc bằng cách tạo liên kết tượng trưng cho tập lệnh của bạn được gọi -ihoặc -scho ví dụ
Stéphane Chazelas

Cảm ơn, Stéphane. Tôi đã bỏ lỡ dấu gạch nối đơn trong dòng shebang ví dụ của bạn. Tôi đã phải tìm kiếm nơi ghi lại rằng một dấu gạch nối đơn tương đương với một dấu gạch nối kép pubs.opengroup.org/onlinepub/9699919799/utilities/sh.html .
jrw32982 hỗ trợ Monica

Thật quá dễ dàng để quên dấu gạch nối đơn / đôi trong dòng shebang cho tập lệnh setuid; bằng cách nào đó hệ thống sẽ chăm sóc nó cho bạn. Không cho phép các tập lệnh setuid hoàn toàn hoặc bằng cách nào đó /bin/shnên vô hiệu hóa xử lý tùy chọn của chính nó nếu nó có thể phát hiện ra rằng nó đang chạy setuid. Tôi không thấy cách sử dụng / dev / fd / x tự khắc phục điều đó. Bạn vẫn cần gạch nối đơn / đôi, tôi nghĩ vậy.
jrw32982 hỗ trợ Monica

@ jrw32982, /dev/fd/xbắt đầu bằng /, không -. Mục tiêu chính là loại bỏ điều kiện cuộc đua giữa hai người execve()(ở giữa execve("the-script")đó nâng cao các đặc quyền và sau execve("interpreter", "thescript")đó interpretermở ra bản thảo sau này (có thể rất tốt đã được thay thế bằng một liên kết tượng trưng cho một thứ khác trong thời gian đó). kịch bản suid đúng cách execve("interpreter", "/dev/fd/n")thay vào đó n đã được mở như là một phần của lệnh thực thi đầu tiên ().
Stéphane Chazelas

6

Đây là hai tình huống mà thư mục sẽ không được bao gồm:

> bash scriptname
scriptname

> bash <scriptname
bash

Trong cả hai trường hợp, thư mục hiện hành sẽ phải là thư mục nơi mà Tập lệnh đã được đặt.

Trong trường hợp đầu tiên, giá trị của $0vẫn có thể được chuyển đến grepvì nó giả sử đối số FILE có liên quan đến thư mục hiện tại.

Trong trường hợp thứ hai, nếu thông tin trợ giúp và phiên bản chỉ được in để đáp ứng với tùy chọn dòng lệnh cụ thể, thì đó không phải là vấn đề. Tôi không chắc tại sao mọi người sẽ gọi một tập lệnh theo cách đó để in thông tin trợ giúp hoặc phiên bản.

Hãy cẩn thận

  • Nếu tập lệnh thay đổi thư mục hiện tại, bạn sẽ không muốn sử dụng các đường dẫn tương đối.

  • Nếu tập lệnh có nguồn gốc, giá trị của $0thường sẽ là tập lệnh người gọi thay vì tập lệnh có nguồn gốc.


3

Một đối số tùy ý có thể được chỉ định khi sử dụng -ctùy chọn cho hầu hết (tất cả?) Shell. Ví dụ:

sh -c 'echo $0' argv0

Từ man bash(được chọn hoàn toàn vì điều này có mô tả tốt hơn so với tôi man sh- cách sử dụng là như nhau bất kể):

-c

Nếu tùy chọn -c có mặt, thì các lệnh được đọc từ đối số lệnh không phải tùy chọn đầu tiên. Nếu có các đối số sau lệnh_ chuỗi, chúng được gán cho các tham số vị trí, bắt đầu bằng $ 0.


Tôi nghĩ rằng điều này hoạt động vì -c 'lệnh' là toán hạng và argv0là đối số dòng lệnh không hoạt động đầu tiên của nó.
mikeerv

@mike đúng, tôi đã cập nhật với một đoạn người đàn ông.
Graeme

3

LƯU Ý: Những người khác đã giải thích các cơ chế của $0vì vậy tôi sẽ bỏ qua tất cả.

Tôi thường bước bên toàn bộ vấn đề này và chỉ cần sử dụng lệnh readlink -f $0. Điều này sẽ luôn cung cấp cho bạn đường dẫn đầy đủ của bất cứ điều gì bạn đưa ra cho nó như là một đối số.

Ví dụ

Nói rằng tôi ở đây để bắt đầu với:

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

Tạo một thư mục + tập tin:

$ mkdir adir
$ touch afile
$ cd adir/

Bây giờ bắt đầu hiển thị 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

Thủ đoạn bổ sung

Bây giờ với một kết quả nhất quán được trả về khi chúng ta thẩm vấn $0thông qua readlinkchúng ta có thể sử dụng một cách đơn giản dirname $(readlink -f $0)để có được đường dẫn tuyệt đối đến tập lệnh - hoặc - basename $(readlink -f $0)để có được tên thực của tập lệnh.


0

manTrang của tôi nói:

$0: Mở rộng đến namecác shellhoặc shell script.

Có vẻ như điều này chuyển thành argv[0]shell hiện tại - hoặc đối số dòng lệnh không hoạt động đầu tiên mà shell hiện dịch đang được cung cấp khi được gọi. Trước đây tôi đã nói rằng sh ./somescriptsẽ chuyển biến của nó sang nhưng điều này không chính xác bởi vì đây là một quá trình mới của chính nó và được gọi với một cái mới .$0 $ENV shshell$ENV

Theo cách này sh ./somescript.shkhác với . ./somescript.shchạy trong môi trường hiện tại$0đã được thiết lập.

Bạn có thể kiểm tra điều này bằng cách so sánh $0với /proc/$$/status.

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

Cảm ơn đã sửa chữa, @toxalot. Tôi đã học được điều gì đó.


Trên hệ thống của tôi, nếu nó có nguồn gốc ( . ./myscript.shhoặc source ./myscript.sh), thì đó $0là vỏ. Nhưng nếu nó được truyền dưới dạng đối số cho shell ( sh ./myscript.sh), thì đó $0là đường dẫn đến tập lệnh. Tất nhiên, shtrên hệ thống của tôi là Bash. Vì vậy, tôi không biết nếu điều đó làm cho một sự khác biệt hay không.
toxalot

Nó có một hashbang không? Tôi nghĩ rằng sự khác biệt là vấn đề - và tôi có thể thêm điều đó - nếu không có nó thì không nên execthay vào đó, nhưng với nó, nó nên execnhư vậy.
mikeerv

Có hoặc không có hashbang, tôi nhận được kết quả tương tự. Có hoặc không được thực thi, tôi nhận được kết quả tương tự.
toxalot

Tôi cũng vậy! Tôi nghĩ đó là bởi vì nó là một vỏ dựng sẵn. Tôi đang kiểm tra ...
mikeerv

Các dòng Bang được giải thích bởi kernel. Nếu bạn thực hiện ./somescriptvới bangline #!/bin/sh, nó sẽ tương đương với việc chạy /bin/sh ./somescript. Nếu không, chúng không tạo ra sự khác biệt cho vỏ.
Graeme

0

Tôi muốn grep tập lệnh hiện tại để tôi có thể in thông tin trợ giúp và phiên bản từ phần bình luận ở trên cùng.

Mặc dù $0chứa tên tập lệnh, nó có thể chứa đường dẫn có tiền tố dựa trên cách gọi tập lệnh, tôi luôn sử dụng ${0##*/}để in tên tập lệnh trong đầu ra trợ giúp loại bỏ bất kỳ đường dẫn nào $0.

Lấy từ hướng dẫn Script Bash nâng cao - thay thế tham số Mục 10.2

${var#Pattern}

Xóa khỏi $varphần ngắn nhất $Patternphù hợp với mặt trước của $var.

${var##Pattern}

Xóa khỏi $varphần dài nhất $Patternphù hợp với mặt trước của $var.

Vì vậy, phần dài nhất của các $0kết quả khớp đó */sẽ là toàn bộ tiền tố đường dẫn, chỉ trả về tên tập lệnh.


Có, tôi cũng làm vậy với tên của tập lệnh được sử dụng trong thông báo trợ giúp. Nhưng tôi đang nói về việc gặt kịch bản nên tôi cần có ít nhất một đường dẫn tương đối. Tôi muốn đặt thông điệp trợ giúp lên hàng đầu trong các bình luận và không phải lặp lại thông báo trợ giúp sau khi in.
độc tố

0

Đối với một cái gì đó tương tự tôi sử dụng:

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

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

rPath luôn có cùng giá trị.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.