Chọn trình thông dịch sau khi tập lệnh bắt đầu, ví dụ: if / other bên trong hashbang


16

Có cách nào để tự động chọn trình thông dịch đang thực thi một tập lệnh không? Tôi có một tập lệnh mà tôi đang chạy trên hai hệ thống khác nhau và trình thông dịch tôi muốn sử dụng được đặt ở các vị trí khác nhau trên hai hệ thống. Điều cuối cùng tôi phải làm là thay đổi dòng hashbang mỗi khi tôi chuyển qua. Tôi muốn làm một cái gì đó tương đương logic với điều này (tôi nhận ra rằng cấu trúc chính xác này là không thể):

if running on system A:
    #!/path/to/python/on/systemA
elif running on system B:
    #!/path/on/systemB

#Rest of script goes here

Hoặc thậm chí tốt hơn sẽ là cái này, để nó cố gắng sử dụng trình thông dịch đầu tiên, và nếu nó không tìm thấy thì nó sử dụng cái thứ hai:

try:
    #!/path/to/python/on/systemA
except: 
    #!path/on/systemB

#Rest of script goes here

Rõ ràng, tôi thay vì có thể thực hiện nó như /path/to/python/on/systemA myscript.py hoặc /path/on/systemB myscript.py tùy thuộc vào nơi tôi, nhưng tôi thực sự có một kịch bản wrapper mà ra mắt myscript.py, vì vậy tôi muốn chỉ định đường dẫn đến thông dịch viên python lập trình chứ không phải bằng tay.


3
chuyển 'phần còn lại của tập lệnh' dưới dạng tệp cho trình thông dịch mà không có shebang và sử dụng ifđiều kiện này không phải là một lựa chọn cho bạn? thích,if something; then /bin/sh restofscript.sh elif...
mazs

Đó là một lựa chọn, tôi cũng đã xem xét nó, nhưng hơi lộn xộn hơn tôi muốn. Vì logic trong dòng hashbang là không thể, tôi nghĩ rằng tôi thực sự sẽ đi theo con đường đó.
dkv

Tôi thích một loạt các câu trả lời khác nhau mà câu hỏi này đã tạo ra.
Oskar Skog

Câu trả lời:


27

Không, nó sẽ không hoạt động. Hai ký tự #!hoàn toàn cần phải là hai ký tự đầu tiên trong tệp (làm thế nào bạn sẽ chỉ định những gì diễn giải câu lệnh if?). Điều này tạo thành "số ma thuật" mà exec()họ các hàm phát hiện khi họ xác định xem một tệp mà họ sắp thực hiện là một tập lệnh (cần một trình thông dịch) hay một tệp nhị phân (không có).

Định dạng của dòng shebang khá nghiêm ngặt. Nó cần phải có một đường dẫn tuyệt đối đến một trình thông dịch và nhiều nhất là một đối số với nó.

Những gì bạn có thể làm là sử dụng env:

#!/usr/bin/env interpreter

Bây giờ, con đường dẫn đến envthường /usr/bin/env , nhưng về mặt kỹ thuật mà không có bảo đảm.

Điều này cho phép bạn điều chỉnh PATHbiến môi trường trên mỗi hệ thống do đó interpreter(có thể là bash, pythonhoặc perlhoặc bất cứ điều gì bạn có) được tìm thấy.

Một nhược điểm của phương pháp này là sẽ không thể chuyển một đối số cho người phiên dịch.

Điều này có nghĩa rằng

#!/usr/bin/env awk -f

#!/usr/bin/env sed -f

không có khả năng làm việc trên một số hệ thống.

Một cách tiếp cận rõ ràng khác là sử dụng GNU autotools (hoặc một số hệ thống tạo khuôn đơn giản hơn) để tìm trình thông dịch và đặt đường dẫn chính xác vào tệp trong một ./configurebước, sẽ được chạy khi cài đặt tập lệnh trên mỗi hệ thống.

Người ta cũng có thể dùng đến việc chạy tập lệnh với một trình thông dịch rõ ràng, nhưng đó rõ ràng là điều bạn đang cố tránh:

$ sed -f script.sed

Phải, tôi nhận ra rằng #!cần phải đến ngay từ đầu, vì nó không phải là vỏ xử lý dòng đó. Tôi đã tự hỏi liệu có cách nào để đưa logic vào trong dòng hashbang tương đương với if / other không. Tôi cũng hy vọng tránh làm phiền tôi PATHnhưng tôi đoán đó là những lựa chọn duy nhất của tôi.
dkv

1
Khi bạn sử dụng #!/usr/bin/awk, bạn có thể cung cấp chính xác một đối số, như #!/usr/bin/awk -f. Nếu nhị phân bạn đang trỏ đến env, đối số là nhị phân bạn yêu cầu envtìm, như trong #!/usr/bin/env awk.
DopeGhoti

2
@dkv Không phải. Nó sử dụng một trình thông dịch với hai đối số và nó có thể hoạt động trên một số hệ thống, nhưng chắc chắn không phải trên tất cả.
Kusalananda

3
@dkv trên Linux nó chạy /usr/bin/envvới đối số duy nhất awk -f.
ilkkachu

1
@Kusalananda, không, đó là điểm chính. Nếu bạn có một tập lệnh được gọi foo.awkvới dòng hashbang #!/usr/bin/env awk -f./foo.awksau đó gọi nó với Linux, envthì hai tham số awk -f./foo.awk. Nó thực sự đi tìm /usr/bin/awk -f(vv) với một không gian.
ilkkachu

27

Bạn luôn có thể tạo tập lệnh bao bọc để tìm trình thông dịch chính xác cho chương trình thực tế:

#!/bin/bash
if something ; then
    interpreter=this
    script=/some/path/to/program.real
    flags=()
else
    interpreter=that
    script=/other/path/to/program.real
    flags=(-x -y)
fi
exec "$interpreter" "${flags[@]}" "$script" "$@"

Save the wrapper trong những người sử dụng PATHnhư programvà đưa chương trình thực tế sang một bên hoặc với một tên khác.

Tôi đã sử dụng #!/bin/bashtrong hashbang vì flagsmảng. Nếu bạn không cần lưu trữ một số lượng cờ khác nhau hoặc có thể làm mà không cần đến nó, tập lệnh sẽ hoạt động tốt #!/bin/sh.


2
Tôi đã thấy exec "$interpreter" "${flags[@]}" "$script" "$@"cũng được sử dụng để giữ cho cây quá trình sạch hơn. Nó cũng tuyên truyền mã thoát.
rrauenza

@rrauenza, à vâng, tự nhiên với exec.
ilkkachu

1
Sẽ không #!/bin/shtốt hơn thay vì #!/bin/bash? Ngay cả khi /bin/shlà một liên kết tượng trưng đến một trình bao khác, nó vẫn tồn tại trên hầu hết các hệ thống (nếu không phải tất cả) * nix, cộng với việc nó sẽ buộc tác giả kịch bản tạo một tập lệnh di động thay vì rơi vào bashism.
Sergiy Kolodyazhnyy

@SergiyKolodyazhnyy, heh, tôi đã nghĩ đến việc đề cập đến điều đó sớm hơn, nhưng sau đó thì không. Mảng được sử dụng flagslà một tính năng không chuẩn, nhưng nó đủ hữu ích để lưu trữ một số lượng cờ khác nhau nên tôi quyết định giữ nó.
ilkkachu

Hoặc sử dụng / bin / sh và chỉ cần gọi trình thông dịch trực tiếp trong mỗi nhánh : script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". Bạn cũng có thể thêm kiểm tra lỗi cho các lỗi thực thi.
jrw32982 hỗ trợ Monica

11

Bạn cũng có thể viết một polyglot (kết hợp hai ngôn ngữ). / bin / sh được đảm bảo tồn tại.

Điều này có nhược điểm của mã xấu và có lẽ một số /bin/shcó khả năng bị nhầm lẫn. Nhưng nó có thể được sử dụng khi envkhông tồn tại hoặc tồn tại ở nơi nào khác ngoài / usr / bin / env. Nó cũng có thể được sử dụng nếu bạn muốn thực hiện một số lựa chọn khá lạ mắt.

Phần đầu tiên của tập lệnh xác định trình thông dịch nào sẽ sử dụng khi chạy với / bin / sh làm trình thông dịch, nhưng bị bỏ qua khi được chạy bởi trình thông dịch chính xác. Sử dụng execđể ngăn vỏ chạy nhiều hơn phần đầu tiên.

Ví dụ về Python:

#!/bin/sh
'''
' 2>/dev/null
# Python thinks this is a string, docstring unfortunately.
# The shell has just tried running the <newline> program.
find_best_python ()
{
    for candidate in pypy3 pypy python3 python; do
        if [ -n "$(which $candidate)" ]; then
            echo $candidate
            return
        fi
    done
    echo "Can't find any Python" >/dev/stderr
    exit 1
}
interpreter="$(find_best_python)"   # Replace with something fancier.
# Run the rest of the script
exec "$interpreter" "$0" "$@"
'''

3
Tôi nghĩ rằng tôi đã thấy một trong những điều này trước đây, nhưng ý tưởng vẫn khủng khiếp không kém ... Nhưng, có lẽ bạn cũng muốn exec "$interpreter" "$0" "$@"nhận tên của kịch bản cho người phiên dịch thực tế. (Và sau đó hy vọng không ai nói dối khi thiết lập $0.)
ilkkachu

6
Scala thực sự có hỗ trợ cho các tập lệnh polyglot theo cú pháp của nó: nếu tập lệnh Scala bắt đầu bằng #!, Scala bỏ qua mọi thứ cho đến khớp !#; điều này cho phép bạn đặt mã tập lệnh phức tạp tùy ý vào một ngôn ngữ tùy ý trong đó, và sau đó execlà công cụ thực thi Scala với tập lệnh.
Jörg W Mittag

1
@ Jorg W Mittag: +1 cho Scala
jrw32982 hỗ trợ Monica

2

Tôi thích câu trả lời của Kusalananda và ilkkachu, nhưng đây là một câu trả lời thay thế trực tiếp hơn những gì câu hỏi đang hỏi, đơn giản chỉ vì nó được hỏi.

#!/usr/bin/ruby -e exec "non-existing-interpreter", ARGV[0] rescue exec "python", ARGV[0]

if True:
  print("hello world!")

Lưu ý rằng bạn chỉ có thể làm điều này khi trình thông dịch cho phép viết mã trong đối số đầu tiên. Ở đây, -evà mọi thứ sau khi nó được lấy nguyên văn thành 1 đối số cho ruby. Theo như tôi có thể nói, bạn không thể sử dụng bash cho mã shebang, bởi vì bash -cyêu cầu mã phải nằm trong một đối số riêng.

Tôi đã thử làm điều tương tự với python cho mã shebang:

#!/usr/bin/python -cexec("import sys,os\ntry: os.execlp('non-existing-interpreter', 'non-existing-interpreter', sys.argv[1])\nexcept: os.execlp('ruby', 'ruby', sys.argv[1])")

if true
  puts "hello world!"
end

nhưng hóa ra quá dài và linux (ít nhất là trên máy của tôi) đã cắt shebang thành 127 ký tự. Vui lòng loại trừ việc sử dụng execđể chèn dòng mới vì python không cho phép thử hoặc importkhông có dòng mới.

Tôi không chắc nó có khả năng di động như thế nào và tôi sẽ không làm điều đó với mã được phân phối. Tuy nhiên, điều đó là có thể. Có lẽ ai đó sẽ thấy nó hữu ích cho việc gỡ lỗi nhanh và bẩn hoặc một cái gì đó.


2

Mặc dù điều này không chọn trình thông dịch trong tập lệnh shell (nó chọn nó trên mỗi máy), nhưng nó là một lựa chọn dễ dàng hơn nếu bạn có quyền truy cập quản trị vào tất cả các máy bạn đang cố chạy tập lệnh.

Tạo một liên kết tượng trưng (hoặc một liên kết cứng nếu muốn) để trỏ đến đường dẫn trình thông dịch mong muốn. Ví dụ: trên hệ thống của tôi perl và python nằm trong / usr / bin:

cd /bin
ln -s /usr/bin/perl perl
ln -s /usr/bin/python python

sẽ tạo một liên kết tượng trưng để cho phép hashbang giải quyết / bin / perl, v.v. Điều này bảo tồn khả năng truyền tham số cho các tập lệnh.


1
+1 Điều này thật đơn giản. Như bạn lưu ý, nó không hoàn toàn trả lời câu hỏi, nhưng dường như nó thực hiện chính xác những gì OP muốn. Mặc dù tôi đoán sử dụng env có được quyền truy cập root trên mỗi vấn đề của máy.
Joe

0

Tôi đã phải đối mặt với một vấn đề tương tự như hôm nay ( python3chỉ vào một phiên bản trăn quá cũ trên một hệ thống), và đưa ra một cách tiếp cận hơi khác so với những gì được thảo luận ở đây: Sử dụng phiên bản "sai" của python để bootstrap vào "bên phải". Hạn chế là một số phiên bản của trăn cần có thể truy cập một cách đáng tin cậy, nhưng điều đó thường có thể đạt được bằng cách ví dụ #!/usr/bin/env python3.

Vì vậy, những gì tôi làm là bắt đầu kịch bản của tôi với:

#!/usr/bin/env python3
import sys
import os

# On one of our systems, python3 is pointing to python3.3
# which is too old for our purposes. 'Upgrade' if needed
if sys.version_info[1] < 4:
    for py_version in ['python3.7', 'python3.6', 'python3.5', 'python3.4']:
        try:
            os.execlp(py_version, py_version, *sys.argv)
        except:
            pass # Deliberately ignore errors, pick first available version

Điều này làm là:

  • Kiểm tra phiên bản thông dịch cho một số tiêu chí chấp nhận
  • Nếu không được chấp nhận, hãy xem qua danh sách các phiên bản ứng cử viên và tự thực hiện lại với phiên bản đầu tiên có sẵn
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.