脚本启动后选择解释器,例如if / else在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
在没有shebang的if情况下将“脚本的其余部分”作为文件传递给解释器,而使用条件不是您的选择吗?就像,if something; then /bin/sh restofscript.sh elif...
mazs

我也考虑过,这是一个选择,但是比我想要的更混乱。由于hashbang行中的逻辑是不可能的,我想我确实会走这条路线。
dvv

我喜欢这个问题产生的各种各样的答案。
奥斯卡·斯科格

Answers:


27

不,那行不通。这两个字符#!绝对必须是文件中的前两个字符(无论如何,您将如何指定解释if语句的内容?)。这构成了exec()函数族在确定要执行的文件是脚本(需要解释器)还是二进制文件(不需要)时检测到的“幻数” 。

shebang行的格式非常严格。它需要有一个通往解释器的绝对路径,并且最多只能有一个论点。

可以做的是使用env

#!/usr/bin/env interpreter

现在,路径env一般 /usr/bin/env,但在技术上这是没有保证。

这使您可以调整PATH每个系统上的环境变量,以便找到interpreter(无论是bashpython还是perl您所拥有的)。

这种方法的缺点是不可能将参数传递给解释器。

这意味着

#!/usr/bin/env awk -f

#!/usr/bin/env sed -f

在某些系统上不太可能工作。

另一种显而易见的方法是使用GNU自动工具(或一些更简单的模板系统)查找解释器,并一./configure步步将正确的路径放入文件中,该步骤将在每个系统上安装脚本时运行。

也可以使用显式解释器来运行脚本,但这显然是您要避免的事情:

$ sed -f script.sed

是的,我意识到这#!需要一开始,因为不是处理该行的shell。我在想,如果有把逻辑的方式里面的hashbang线,这将是相当于的if / else。我也希望避免弄乱自己,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" "$@"

将包装器另存为用户的名称PATHprogram然后将实际程序放在一边或使用其他名称。

#!/bin/bash由于flags数组,我在hashbang中使用过。如果您不需要存储可变数量的标记或类似的标记并且可以不使用它,则该脚本应与一起移植#!/bin/sh


2
我还看到过exec "$interpreter" "${flags[@]}" "$script" "$@"用来保持进程树清洁的方法。它还传播退出代码。
rrauenza

@rrauenza,啊,是的,自然而然exec
ilkkachu

1
会不会 #!/bin/sh更好#!/bin/bash呢?即使/bin/sh是与其他shell的符号链接,它也应存在于大多数(如果不是全部)* nix系统上,此外,它还会迫使脚本作者制作可移植的脚本,而不是陷入bashisms。
Sergiy Kolodyazhnyy

@SergiyKolodyazhnyy,呵呵,我想提早提一下,但那时没有。用于的数组flags是非标准功能,但是它对于存储可变数量的标志足够有用,因此我决定保留它。
ilkkachu

或使用/ bin / sh并直接在每个分支中直接调用解释器:script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@"。您还可以添加错误检查以检查exec故障。
jrw32982支持Monica's

11

您还可以编写多种语言(将两种语言结合起来)。/ bin / sh被保证存在。

这有丑陋的代码的缺点,也许有些/bin/sh可能会引起混淆。但是当env/ usr / bin / env不存在或不存在它时,可以使用它。如果您想进行一些漂亮的选择,也可以使用它。

脚本的第一部分确定以/ bin / sh作为解释器运行时使用哪个解释器,但由正确的解释器运行时将被忽略。使用exec以防止shell比第一部分运转更。

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执行引擎放入其中。
约尔格W¯¯米塔格

1
@
JörgW

2

我更喜欢Kusalananda和ilkkachu的答案,但这是一个替代答案,它更直接地执行所要提出的问题,仅仅是因为被问到了。

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

if True:
  print("hello world!")

请注意,只有在解释器允许在第一个参数中编写代码时,您才能执行此操作。在这里,-e之后的所有内容都被逐字逐句地作为对红宝石的一种论点。据我所知,您不能对shebang代码使用bash,因为它bash -c要求代码位于单独的参数中。

我尝试对shebang代码使用python做同样的事情:

#!/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(至少在我的机器上)将shebang截断为127个字符。请原谅使用exec插入换行符,因为python不允许import在没有换行符的情况下使用try-excepts或s。

我不确定它的可移植性,也不会在打算分发的代码上使用。不过,这是可行的。也许有人会发现它对于快速,肮脏的调试很有用。


2

尽管这没有在shell脚本中选择解释器(而是在每台计算机上选择解释器),但是如果您对尝试在其上运行脚本的所有计算机都具有管理访问权限,则这是一种更简单的选择。

创建一个符号链接(或一个硬链接,如果需要的话)以指向所需的解释器路径。例如,在我的系统上,perl和python在/ usr / bin中:

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

将创建一个符号链接,以允许hashbang解析为/ bin / perl等。这也保留了将参数传递给脚本的功能。


1
+1这很简单。正如您所注意到的,它并不能完全回答问题,但似乎完全可以满足OP的要求。虽然我猜想使用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

这是什么:

  • 检查口译版本,了解一些验收标准
  • 如果不可接受,请查看候选版本列表,然后使用可用的第一个版本重新执行
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.