Выберите интерпретатор после запуска скрипта, например, если / еще внутри hashbang

16

Есть ли способ динамически выбрать интерпретатор, который выполняет сценарий? У меня есть скрипт, который я запускаю на двух разных системах, и интерпретатор, который я хочу использовать, находится в разных местах на двух системах. В конечном итоге мне приходится менять строку hashbang каждый раз, когда я переключаюсь. Я хотел бы сделать что-то, что является логическим эквивалентом этого (я понимаю, что эта точная конструкция невозможна):

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

#Rest of script goes here

Или еще лучше, если он попытается использовать первый интерпретатор, а если не найдет, использует второй:

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

#Rest of script goes here

Очевидно, что вместо этого я могу выполнить его как /path/to/python/on/systemA myscript.py или в /path/on/systemB myscript.py зависимости от того, где я нахожусь, но на самом деле у меня есть скрипт-обертка, который запускается myscript.py, поэтому я хотел бы указать путь к интерпретатору python программно, а не вручную.

ДКВ
источник
3
Передача «остальной части сценария» в виде файла в интерпретатор без Шебанга, и использование ifусловия не вариант для вас? как,if something; then /bin/sh restofscript.sh elif...
МАЗС
Это вариант, я тоже его обдумал, но несколько грязнее, чем хотелось бы. Поскольку логика в строке hashbang невозможна, я думаю, что я действительно пойду этим путем.
dkv
Мне нравится широкий спектр разных ответов на этот вопрос.
Оскар Ског

Ответы:

27

Нет, это не сработает. Эти два символа обязательно #!должны быть первыми двумя символами в файле (как бы вы в любом случае указали, что интерпретирует оператор if?). Это составляет «магическое число», которое exec()обнаруживает семейство функций, когда они определяют, является ли файл, который они собираются выполнить, сценарием (который требует интерпретатора) или двоичным файлом (который не выполняет).

Формат линии Шебанга довольно строг. У него должен быть абсолютный путь к интерпретатору и не более одного аргумента.

Что вы можете сделать, это использоватьenv :

#!/usr/bin/env interpreter

Теперь путь к envявляется обычно /usr/bin/env , но технически это не гарантия.

Это позволяет вам настроить PATHпеременную среды в каждой системе так, чтобы interpreter(будь тоbash , pythonили , perlили то , что у вас есть) не найдено.

Недостатком этого подхода является то, что невозможно будет передать аргумент интерпретатору.

Это означает, что

#!/usr/bin/env awk -f

и

#!/usr/bin/env sed -f

вряд ли будет работать на некоторых системах.

Другой очевидный подход - использовать автоинструменты GNU (или более простую систему шаблонов), чтобы найти интерпретатор и поместить правильный путь в файл в ./configure шаг, который будет запускаться после установки скрипта в каждой системе.

Можно также прибегнуть к запуску сценария с явным интерпретатором, но это, очевидно, то, чего вы пытаетесь избежать:

$ sed -f script.sed
Кусалананда
источник
Правильно, я понимаю, что это #!должно произойти с самого начала, так как эта строка обрабатывается не оболочкой. Мне было интересно , если есть способ поставить логику внутри в hashbang линии , которая была бы эквивалентна если / другое. Я также надеялся избежать возни с моим, PATHно я думаю, что это мои единственные варианты.
dkv
1
Когда вы используете #!/usr/bin/awk, вы можете предоставить ровно один аргумент, как #!/usr/bin/awk -f. Если двоичный файл, на который вы указываете, является envаргументом двоичного файла, который вы хотите envнайти, как в #!/usr/bin/env awk.
DopeGhoti
2
@dkv Это не так. Он использует интерпретатор с двумя аргументами и может работать в некоторых системах, но определенно не во всех.
Кусалананда
3
@dkv в Linux работает /usr/bin/envс одним аргументом awk -f.
ilkkachu
1
@Kusalananda, нет, в этом была суть. Если у вас есть скрипт, вызываемый foo.awkс помощью строки hashbang, #!/usr/bin/env awk -fи вы вызываете его с помощью ./foo.awkзатем, то в Linux вы увидите envдва параметра awk -fи ./foo.awk. На самом деле идет поиск /usr/bin/awk -f(и т. Д.) С пробелом.
ilkkachu
27

Вы всегда можете создать скрипт-обертку, чтобы найти правильный интерпретатор для конкретной программы:

#!/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" "$@"

Сохраните обертку в пользовательских PATH какprogram и отложите реальную программу в сторону или под другим именем.

Я использовал #!/bin/bashв hashbang из-за flagsмассива. Если вам не нужно хранить переменное количество флагов или что-то подобное, и вы можете обойтись без него, скрипт должен работать переносимо #!/bin/sh.

ilkkachu
источник
2
Я exec "$interpreter" "${flags[@]}" "$script" "$@"также видел, чтобы сохранить дерево процессов чище. Также распространяется код выхода.
rrauenza
@rrauenza, ах да, естественно с exec.
ilkkachu
1
Не было #!/bin/shбы лучше, чем #!/bin/bash? Даже если /bin/shэто символическая ссылка на другую оболочку, она должна существовать в большинстве (если не во всех) системах * nix, плюс это заставит автора сценария создавать переносимый сценарий, а не впадать в bashisms.
Сергей Колодяжный
@SergiyKolodyazhnyy, хе, я думал упомянуть об этом раньше, но не сделал, тогда. Используемый массив flagsявляется нестандартной функцией, но он достаточно полезен для хранения переменного количества флагов, поэтому я решил оставить его.
ilkkachu
Или используйте / бен / ш и просто вызовите интерпретатор непосредственно в каждой отрасли: script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". Вы также можете добавить проверку ошибок для ошибок exec.
jrw32982 поддерживает Монику
11

Вы также можете написать полиглот (объединить два языка). / bin / sh гарантированно существует.

У этого есть обратная сторона уродливого кода, и, возможно, некоторые /bin/shмогут запутаться. Но это может быть использовано, когдаenv он не существует или существует где-то еще, кроме / usr / bin / env. Его также можно использовать, если вы хотите сделать какой-то довольно необычный выбор.

Первая часть скрипта определяет, какой интерпретатор использовать при запуске с / bin / sh в качестве интерпретатора, но игнорируется при запуске правильным интерпретатором. Используйте execдля предотвращения запуска оболочки больше, чем первая часть.

Пример 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
Я думаю, что я видел один из них раньше, но идея все еще одинаково ужасна ... Но вы, вероятно, хотите exec "$interpreter" "$0" "$@"передать имя самого скрипта и самому интерпретатору. (А потом надеюсь, никто не соврал при настройке $0.)
ilkkachu
6
Scala фактически поддерживает сценарии полиглотов в своем синтаксисе: если сценарий Scala начинается с #!, Scala игнорирует все до совпадения !#; это позволяет вам разместить произвольно сложный код скрипта на произвольном языке, а затем execмеханизм исполнения Scala со скриптом.
Йорг Миттаг
1
@ Jörg W Mittag: +1 для Scala
jrw32982 поддерживает Монику
2

Я предпочитаю ответы Кусалананды и Илккачу, но вот альтернативный ответ, который более прямо делает то, о чем просил вопрос, просто потому, что он был задан.

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

if True:
  print("hello world!")

Обратите внимание, что вы можете сделать это только тогда, когда интерпретатор разрешает написание кода в первом аргументе. Здесь -eи все после него дословно воспринимается как 1 аргумент для ruby. Насколько я могу судить, вы не можете использовать bash для кода shebang, потому bash -cчто код должен быть в отдельном аргументе.

Я попытался сделать то же самое с Python для кода 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

но получается слишком долго и linux (по крайней мере на моей машине) усекает шебанг до 127 символов. Пожалуйста, извините за использование execвставки новых строк, так как python не разрешает try-excepts илиimport s без новых строк.

Я не уверен, насколько это переносимо, и я бы не стал делать это с кодом, предназначенным для распространения. Тем не менее, это выполнимо. Может быть, кто-то найдет это полезным для быстрой и грязной отладки или что-то в этом роде.

Йол
источник
2

Хотя это не выбирает интерпретатор в сценарии оболочки (он выбирает его для каждой машины), это более простая альтернатива, если у вас есть административный доступ ко всем машинам, на которых вы пытаетесь запустить скрипт.

Создайте символическую ссылку (или жесткую ссылку, если необходимо), чтобы указать на нужный путь интерпретатора. Например, в моей системе perl и python находятся в / usr / bin:

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

создаст символическую ссылку, чтобы разрешить хэш-банг для / bin / perl и т. д. Это также сохраняет возможность передавать параметры в сценарии.

обкрадывать
источник
1
+1 Это так просто. Как вы заметили, он не совсем отвечает на вопрос, но, похоже, делает именно то, что хочет ОП. Хотя я полагаю, что использование env позволяет обойти права root при каждой проблеме с машиной.
Джо
0

Сегодня я столкнулся с подобной проблемой ( python3указывающей на версию python, которая была слишком старой в одной системе), и предложил подход, несколько отличающийся от обсуждаемого здесь: используйте «неправильную» версию Python для загрузки в «правильный». Ограничение состоит в том, что некоторая версия python должна быть надежно достижимой, но обычно это может быть достигнуто, например,#!/usr/bin/env python3 .

Итак, что я делаю, это начинаю свой скрипт с:

#!/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

Что это делает:

  • Проверьте версию интерпретатора для некоторого критерия приемлемости
  • Если это неприемлемо, просмотрите список возможных вариантов и повторите его, используя первую из доступных версий.
microtherion
источник