相对shebang:如何编写运行附带的便携式解释器的可执行脚本

2024-01-26

假设我们有一个程序/包,它带有自己的解释器和一组应该在执行时调用它的脚本(使用 shebang)。

假设我们希望保持它的可移植性,因此即使简单地复制到不同的位置(不同的机器)而不调用设置/安装或修改环境(PATH),它仍然可以正常运行。这些脚本不应混合使用系统解释器。

给定的约束排除了两种已知的方法,例如带有绝对路径的 shebang:

#!/usr/bin/python 

并在环境中搜索

#!/usr/bin/env python

单独的发射器看起来很难看并且不可接受。

我发现了对 shebang 限制的很好的总结,它描述了为什么 shebang 中的相对路径毫无用处,并且解释器不能有多个参数:http://www.in-ulm.de/~mascheck/various/shebang/ http://www.in-ulm.de/~mascheck/various/shebang/

而且我还发现实用的解决方案 http://rosettacode.org/wiki/Multiline_shebang对于大多数具有“多行 shebang”技巧的语言。它允许编写这样的脚本:

#!/bin/sh
"exec" "`dirname $0`/python2.7" "$0" "$@"
print copyright

但有时,我们不想使用这种方法来扩展/修补依赖 shebang 的现有脚本以及解释器的绝对路径。例如。 Python的setup.py支持--executable选项基本上允许为其生成的脚本指定 shebang 内容:

python setup.py build --executable=/opt/local/bin/python

因此,特别是可以指定什么--executable=为了实现所需的可移植性?或者换句话说,因为我想让这个问题不太具体到Python......

问题

如何编写一个 shebang 来指定一个解释器,其路径相对于正在执行的脚本的位置?


缺失的“妙语”安东的回答 https://stackoverflow.com/a/33225083/6778374:

随着更新版本env,我们现在可以实现最初的想法了:

#!/usr/bin/env -S /bin/sh -c '"$(dirname "$0")/python3" "$0" "$@"'

请注意,我切换到python3,但这个问题实际上是关于 shebang - 而不是 python - 所以你可以在你想要的任何脚本环境中使用这个解决方案。您还可以更换/bin/sh只用sh如果你更喜欢。

这里发生了很多事情,包括一些引用地狱的话,乍一看并不清楚发生了什么。我认为在没有解释的情况下仅仅说“这是如何做到的”没有什么价值,所以让我们来解开它。

它是这样分解的:

  1. shebang 被解释为运行/usr/bin/env具有以下参数:

    1. -S /bin/sh -c '"$(dirname "$0")/python3" "$0" "$@"'
    2. 脚本文件的完整路径(本地路径或绝对路径)
    3. 从此之后,任何额外的命令行参数
  2. env找到-S在第一个参数的开头,并根据(简化的)shell 规则将其拆分。在这种情况下,只有单引号是相关的 - 所有其他花哨的语法都在单引号内,因此它会被忽略。新的论据env become:

    1. /bin/sh
    2. -c
    3. "$(dirname "$0")/python3" "$0" "$@"
    4. 脚本文件的完整路径(本地路径或绝对路径)
    5. 从此,(可能)额外的参数
  3. It runs /bin/sh- 默认 shell - 带有参数:

    1. -c
    2. "$(dirname "$0")/python3" "$0" "$@"
    3. 脚本文件的完整路径
    4. 从此,(可能)额外的参数
  4. 当 shell 运行时-c,运行在第二工作模式此处定义 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html(并且所有 shell 的不同手册页也多次重新描述,例如dash https://www.man7.org/linux/man-pages/man1/dash.1.html,这更加平易近人)。在我们的例子中,我们可以忽略所有额外的选项,语法是:

     sh -c command_string command_name [argument ...]
    

    在我们的例子中:

    1. 命令字符串是"$(dirname "$0")/python3" "$0" "$@"
    2. command_name 是脚本路径,例如./path to/script dir/script file.py
    3. argument(s) 是任何额外的参数(可能有零个参数)
  5. 如上所述,shell 想要运行 command_string ("$(dirname "$0")/python3" "$0" "$@")作为命令,所以现在我们转向外壳命令语言 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html:

    • 参数扩展 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02执行于"$0" and "$@",两者都是特殊参数 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_05_02:

      • "$@"扩展到参数。如果没有争论,它就会“膨胀”成无。由于这种特殊的行为,我链接的规范中对其进行了可怕的解释,但是破折号的手册页 https://www.man7.org/linux/man-pages/man1/dash.1.html解释得更好。
      • $0扩展为 command_name - 我们的脚本文件。每次出现$0位于双引号内,因此不会被拆分,即路径中的空格不会将其分解为多个参数。
    • 命令替换 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_03被应用,替换$(dirname "$0")运行命令的标准输出dirname "./path to/script dir/script file.py",即我们的脚本文件所在的文件夹:./path to/script dir.

    在所有替换和扩展之后,命令变为,例如:

     "./path to/script dir/python3" "./path to/script dir/script file.py" "first argument" "second argument" ...
    
  6. 最后,shell运行扩展后的命令,并执行我们本地的python3将我们的脚本文件作为参数,后跟我们传递给它的任何其他参数。

Phew!


以下基本上是我的尝试证明这些步骤正在发生。它可能不值得你花时间,但我已经写了它,我不认为它有多糟糕以至于应该将其删除。如果没有别的事,如果有人想查看如何对此类事物进行逆向工程的示例,这可能会对他们有用。它不包括额外的参数,这些参数是在伊曼纽尔评论后添加的。

最后还有一个很烂的笑话..


首先让我们从更简单的开始。看看下面的“脚本”,替换env with echo:

$ cat "/home/neatnit/Projects/SO question 33225082/my script.py"
#!/usr/bin/echo  -S  /bin/sh  -c  '"$(  dirname  "$0"  )/python2.7"  "$0"'

print("This is python")

这几乎不是一个脚本——shebang 调用echo它只会打印给出的任何参数。我特意放了单词之间有两个空格,这样我们就可以看到它们是如何被保存的。顺便说一句,我特意将脚本放在包含空格的路径中,以表明它们已正确处理。

让我们运行一下:

$ "/home/neatnit/Projects/SO question 33225082/my script.py"
-S  /bin/sh  -c  '"$(  dirname  "$0"  )/python2.7"  "$0"' /home/neatnit/Projects/SO question 33225082/my script.py

我们看到那个shebang,echo使用两个参数运行:

  1. -S /bin/sh -c '"$( dirname "$0" )/python2.7" "$0"'
  2. /home/neatnit/Projects/SO question 33225082/my script.py

这些是字面参数echo看到 - 没有引用或转义。

现在,让我们得到env返回但使用printf https://unix.stackexchange.com/a/105580[1] 领先于sh探索如何env处理这些参数:

$ cat "/home/neatnit/Projects/SO question 33225082/my script.py"
#!/usr/bin/env  -S  printf  %s\n  /bin/sh  -c  '"$(  dirname  "$0"  )/python2.7"  "$0"'

print("This is python")

并运行它:

$ "/home/neatnit/Projects/SO question 33225082/my script.py"
/bin/sh
-c
"$(  dirname  "$0"  )/python2.7"  "$0"
/home/neatnit/Projects/SO question 33225082/my script.py

env之后分割字符串-S[2] 根据普通(但简化)的 shell 规则。在这种情况下,所有$符号在单引号内,所以env没有扩展它们。然后它将附加参数(脚本文件)附加到末尾。

When sh获取这些参数,之后的第一个参数-c(在这种情况下:"$( dirname "$0" )/python2.7" "$0") 被解释为 shell 命令,下一个参数充当该命令中的第一个参数 ($0).

推动printf更深一层:

$ cat "/home/neatnit/Projects/SO question 33225082/my script.py"
#!/usr/bin/env  -S  /bin/sh  -c  'printf  %s\\\n  "$(  dirname  "$0"  )/python2.7"  "$0"'

print("This is python")

并运行它:

$ "/home/neatnit/Projects/SO question 33225082/my script.py"
/home/neatnit/Projects/SO question 33225082/python2.7
/home/neatnit/Projects/SO question 33225082/my script.py

最后 - 它开始看起来像我们正在寻找的命令!本地python2.7和我们的脚本作为参数!

sh扩大$0 into /home/[ ... ]/my script.py,给出这个命令:

"$(  dirname  "/home/[ ... ]/my script.py"  )/python2.7"  "/home/[ ... ]/my script.py"

dirname剪掉路径的最后一部分以获取包含的文件夹,给出以下命令:

"/home/[ ... ]/SO question 33225082/python2.7"  "/home/[ ... ]/my script.py"

为了强调一个常见的陷阱,如果我们不使用双引号并且路径包含空格,就会发生以下情况:

$ cat "/home/neatnit/Projects/SO question 33225082/my script.py"
#!/usr/bin/env  -S  /bin/sh  -c  'printf  %s\\\n  $(  dirname  $0  )/python2.7  $0'

print("This is python")
$ "/home/neatnit/Projects/SO question 33225082/my script.py"
/home/neatnit/Projects
.
33225082
./python2.7
/home/neatnit/Projects/SO
question
33225082/my
script.py

不用说,将其作为命令运行不会给出所需的结果。弄清楚这里到底发生了什么就留给读者作为练习:)

最后,我们将引号放回原来的位置并去掉printf,我们终于可以运行我们的脚本了:

$ "/home/neatnit/Projects/SO question 33225082/my script.py"
/home/neatnit/Projects/SO question 33225082/my script.py: 1: /home/neatnit/Projects/SO question 33225082/python2.7: not found

等等,呃,让我解决这个问题

$ ln --symbolic $(which python3) "/home/neatnit/Projects/SO question 33225082/python2.7"
$ "/home/neatnit/Projects/SO question 33225082/my script.py"
This is python

Rejoice!


[1] 这样我们就可以在单独的行中看到每个参数,并且我们不必因空格分隔的参数而感到困惑。

[2] 后面不需要有空格-S,我只是更喜欢它的外观。-Sprintf听起来真的很累。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

相对shebang:如何编写运行附带的便携式解释器的可执行脚本 的相关文章

随机推荐