缺失的“妙语”安东的回答 https://stackoverflow.com/a/33225083/6778374:
随着更新版本env
,我们现在可以实现最初的想法了:
#!/usr/bin/env -S /bin/sh -c '"$(dirname "$0")/python3" "$0" "$@"'
请注意,我切换到python3
,但这个问题实际上是关于 shebang - 而不是 python - 所以你可以在你想要的任何脚本环境中使用这个解决方案。您还可以更换/bin/sh
只用sh
如果你更喜欢。
这里发生了很多事情,包括一些引用地狱的话,乍一看并不清楚发生了什么。我认为在没有解释的情况下仅仅说“这是如何做到的”没有什么价值,所以让我们来解开它。
它是这样分解的:
-
shebang 被解释为运行/usr/bin/env
具有以下参数:
-S /bin/sh -c '"$(dirname "$0")/python3" "$0" "$@"'
- 脚本文件的完整路径(本地路径或绝对路径)
- 从此之后,任何额外的命令行参数
-
env
找到-S
在第一个参数的开头,并根据(简化的)shell 规则将其拆分。在这种情况下,只有单引号是相关的 - 所有其他花哨的语法都在单引号内,因此它会被忽略。新的论据env
become:
/bin/sh
-c
"$(dirname "$0")/python3" "$0" "$@"
- 脚本文件的完整路径(本地路径或绝对路径)
- 从此,(可能)额外的参数
-
It runs /bin/sh
- 默认 shell - 带有参数:
-c
"$(dirname "$0")/python3" "$0" "$@"
- 脚本文件的完整路径
- 从此,(可能)额外的参数
-
当 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 ...]
在我们的例子中:
- 命令字符串是
"$(dirname "$0")/python3" "$0" "$@"
- command_name 是脚本路径,例如
./path to/script dir/script file.py
- argument(s) 是任何额外的参数(可能有零个参数)
-
如上所述,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" ...
-
最后,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
使用两个参数运行:
-S /bin/sh -c '"$( dirname "$0" )/python2.7" "$0"'
/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
听起来真的很累。