我有一个脚本,我希望能够在两台机器上运行。这两台机器从同一个 git 存储库获取脚本的副本。该脚本需要使用正确的解释器运行(例如zsh
)。
很遗憾,两个都 env
并zsh
位于本地和远程计算机的不同位置:
远程机
$ which env
/bin/env
$ which zsh
/some/long/path/to/the/right/zsh
本地机
$ which env
/usr/bin/env
$which zsh
/usr/local/bin/zsh
如何设置 shebang 以便始终/path/to/script.sh
使用Zsh
可用的脚本运行脚本PATH
?
答案1
你不能直接通过 shebang 解决这个问题,因为 shebang 是纯粹静态的。您可以做的是在 shebang 中添加一些“最不常见的乘数”(从 shell 角度来看),并使用正确的 shell 重新执行您的脚本(如果该 LCM 不是 zsh)。换句话说:让您的脚本由在所有系统上找到的 shell 执行,测试zsh
仅 - 功能,如果测试结果为 false,则让脚本exec
带有zsh
,测试将成功,您只需继续。
zsh
例如,中的一个独特特征是$ZSH_VERSION
变量的存在:
#!/bin/sh -
[ -z "$ZSH_VERSION" ] && exec zsh - "$0" ${1+"$@"}
# zsh-specific stuff following here
echo "$ZSH_VERSION"
在这个简单的例子中,脚本首先由/bin/sh
(所有 80 年代后的类 Unix 系统都理解#!
并具有/bin/sh
Bourne 或 POSIX 但我们的语法与两者兼容)执行。如果$ZSH_VERSION
是不是设置,脚本exec
本身通过zsh
。如果$ZSH_VERSION
设置了(或者脚本已经运行过zsh
),则简单地跳过测试。瞧。
zsh
仅当根本不存在时,此操作才会失败$PATH
。
编辑:为了确保您只exec
在zsh
平常的地方,您可以使用类似的东西
for sh in /bin/zsh \
/usr/bin/zsh \
/usr/local/bin/zsh; do
[ -x "$sh" ] && exec "$sh" - "$0" ${1+"$@"}
done
这可以避免你意外地exec
得到一些$PATH
不是zsh
你所期望的东西。
答案2
多年来,我一直使用类似的方法来处理需要运行脚本的系统上的 Bash 的各个位置。
Bash/Zsh/等。
#!/bin/sh
# Determines which OS and then reruns this script with approp. shell interp.
LIN_BASH="/bin/sh";
SOL_BASH="/packages/utilities/bin/sun5/bash";
OS_TYPE=`uname -s`;
if [ $OS_TYPE = "SunOS" ]; then
$SOL_BASH -c "`sed -n '/\#\#\# BEGIN/,$p' $0`" $0 $*;
elif [ $OS_TYPE = "Linux" ]; then
$LIN_BASH -c "`sed -n '/\#\#\# BEGIN/,$p' $0`" $0 $*;
else
echo "UNKNOWN OS_TYPE, $OS_TYPE";
exit 1;
fi
exit 0;
### BEGIN
...script goes here...
上述内容可以很容易地适应各种口译员。关键是该脚本最初作为 Bourne shell 运行。然后,它第二次递归地调用自身,但### BEGIN
使用解析注释上方的所有内容sed
。
珀尔
Perl 也有类似的技巧:
#!/bin/sh
LIN_PERL="/usr/bin/perl";
SOL_PERL="/packages/perl/bin/perl";
OS_TYPE=`uname -s`;
if [ $OS_TYPE = "SunOS" ]; then
eval 'exec $SOL_PERL -x -S $0 ${1+"$@"}';
elif [ $OS_TYPE = "Linux" ]; then
eval 'exec $LIN_PERL -x -S $0 ${1+"$@"}';
else
echo "$OS_TYPE: UNSUPORRTED OS/PLATFORM";
exit 0;
fi
exit 0;
#!perl
...perl script goes here...
此方法利用 Perl 的功能,当给定要运行的文件时,将解析该文件并跳过该行之前的所有行#! perl
。
答案3
注意:@jw013 进行以下操作不支持的反对意见见以下评论:
否决是因为自修改代码通常被认为是不好的做法。在过去的小型汇编程序时代,这是减少条件分支和提高性能的聪明方法,但现在安全风险超过了优点。如果运行脚本的用户没有脚本的写入权限,您的方法将不起作用。
我回答了他的安全反对意见,指出任何特殊权限是只需要一次每安装更新采取行动,以便安装更新这自安装脚本 - 我个人认为它非常安全。我还向他指出了一个man sh
指通过相似的手段实现相似的目标。当时我并没有费心去指出无论什么安全缺陷或其他方面通常不建议的做法这些可能会或可能不会出现在我的答案中,但它们更有可能植根于问题本身,而不是我的答案:
如何设置 shebang 以便将脚本运行为 /path/to/script.sh 始终使用 PATH 中可用的 Zsh?
@jw013 不满意,继续反对,进一步推进他的尚未支持论证至少有几个错误的陈述:
您使用单个文件,而不是两个文件。这[
man sh
参考] 包中有一个文件修改另一个文件。您有一个正在修改自身的文件。这两种情况有明显的区别。接受输入并产生输出的文件就可以了。可执行文件在运行时会自行更改通常是一个坏主意。你提到的例子并没有做到这一点。
首先:
唯一的可执行文件任意代码可执行文件shell 脚本就是它#!
本身
(虽然甚至#!
是官方未指定)
{ cat >|./file
chmod +x ./file
./file
} <<-\FILE
#!/usr/bin/sh
{ ${l=lsof -p} $$
echo "$l \$$" | sh
} | grep \
"COMMAND\|^..*sh\| [0-9]*[wru] "
#END
FILE
##OUTPUT
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
file 8900 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash
file 8900 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33)
file 8900 mikeserv 0r REG 0,35 108 15496912 /tmp/zshUTTARQ (deleted)
file 8900 mikeserv 1u CHR 136,2 0t0 5 /dev/pts/2
file 8900 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2
file 8900 mikeserv 255r REG 0,33 108 2134129 /home/mikeserv/file
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sh 8906 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash
sh 8906 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33)
sh 8906 mikeserv 0r FIFO 0,8 0t0 15500515 pipe
sh 8906 mikeserv 1w FIFO 0,8 0t0 15500514 pipe
sh 8906 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2
{ sed -i \
'1c#!/home/mikeserv/file' ./file
./file
sh -c './file ; echo'
grep '#!' ./file
}
##OUTPUT
zsh: too many levels of symbolic links: ./file
sh: ./file: /home/mikeserv/file: bad interpreter: Too many levels of symbolic links
#!/home/mikeserv/file
shell 脚本只是一个文本文件 - 为了使其发挥作用,它必须是读通过另一个可执行文件,其指令然后解释的由另一个可执行文件,最后在另一个可执行文件之前执行其解释shell 脚本的。这是不可能用于执行 shell 脚本文件涉及少于两个文件。自己的编译器可能有一个例外zsh
,但我对此没有什么经验,并且这里没有以任何方式表示。
shell 脚本的 hashbang必须指出其意图口译员或因无关而被丢弃。
贝壳的令牌识别/执行行为是由标准定义的
shell 有两种解析和解释其输入的基本模式:要么其当前输入正在定义 a <<here_document
,要么它正在定义 a { ( command |&&|| list ) ; } &
- 换句话说,shell 要么解释 a代币作为命令的分隔符,一旦读入命令,它就应该执行;或者作为创建文件并将其映射到另一个命令的文件描述符的指令。就是这样。
当解释命令来执行 shell 时,会在一组上分隔标记保留字。当 shell 遇到开始标记时,它必须继续读取命令列表,直到该列表由结束标记(例如换行符(如果适用))或结束标记(如执行前的结束标记)})
分隔({
。
shell 区分简单的命令和一个复合命令。这复合命令是执行前必须读入的命令集,但 shell 不会$expansion
对其任何组成部分执行简单的命令直到它单独执行每一个。
因此,在下面的示例中,;semicolon
保留字划定个人简单的命令而非转义\newline
字符在两者之间分隔复合命令:
{ cat >|./file
chmod +x ./file
./file
} <<-\FILE
#!/usr/bin/sh
echo "simple command ${sc=1}" ;\
: > $0 ;\
echo "simple command $((sc+2))" ;\
sh -c "./file && echo hooray"
sh -c "./file && echo hooray"
#END
FILE
##OUTPUT
simple command 1
simple command 3
hooray
这是指南的简化。当你考虑时,事情会变得更加复杂shell 内置命令、子 shell、当前环境等等,但是,对于我在这里的目的来说,这已经足够了。
说到内置的和命令列表,afunction() { declaration ; }
只是分配 a 的一种方式复合命令到一个简单的命令。shell 不得$expansions
对声明语句本身执行任何操作(包括<<redirections>
),而是必须将定义存储为单个文字字符串,并在调用时将其作为内置的特殊 shell 执行。
因此,在可执行 shell 脚本中声明的 shell 函数以其文字字符串形式存储在解释 shell 的内存中 - 未展开以包含附加的此处文档作为输入 - 并在每次作为内置 shell 调用时独立于其源文件执行 -只要 shell 的当前环境持续存在。
A<<HERE-DOCUMENT
是一个内联文件
重定向运算符
<<
和<<-
都允许重定向 shell 输入文件中包含的行,称为此处文档,到命令的输入。这此处文档应被视为单个单词,从下一个单词开始
\newline
,一直持续到有一行仅包含分隔符和 a ,中间\newline
没有s。[:blank:]
然后下一个此处文档开始,如果有的话。格式如下:
[n]<<word
here-document
delimiter
...其中可选
n
表示文件描述符编号。如果省略该数字,则此处文档指的是标准输入(文件描述符0)。
for shell in dash zsh bash sh ; do sudo $shell -c '
{ readlink /proc/self/fd/3
cat <&3
} 3<<-FILE
$0
FILE
' ; done
#OUTPUT
pipe:[16582351]
dash
/tmp/zshqs0lKX (deleted)
zsh
/tmp/sh-thd-955082504 (deleted)
bash
/tmp/sh-thd-955082612 (deleted)
sh
你看?对于上面的每个 shell,该 shell 创建一个文件并将其映射到文件描述符。在zsh, (ba)sh
shell 中创建一个常规文件/tmp
,转储输出,将其映射到描述符,然后删除该/tmp
文件,这样内核的描述符副本就剩下了。dash
避免了所有这些废话,只是将其输出处理放入|pipe
针对重定向<<
目标的匿名文件中。
这使得dash
:
cmd <<HEREDOC
$(cmd)
HEREDOC
功能上等同于bash
:
cmd <(cmd)
whiledash
的实现至少是 POSIXly 可移植的。
这使得一些文件
所以当我这样做时,在下面的答案中:
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_fn() { printf '#!' ; command -v zsh ; cat
} <<SCRIPT >$0
[SCRIPT BODY]
SCRIPT
_fn ; exec $0
FILE
发生以下情况:
我首先将
cat
shell 创建的任何文件的内容FILE
放入./file
,使其可执行,然后执行它。内核解释
#!
并/usr/bin/sh
调用<read
文件描述符分配给./file
。sh
将字符串映射到内存中,其中包含复合命令开始于_fn()
并结束于SCRIPT
。_fn
调用时,sh
必须首先解释然后映射到文件中定义的描述符<<SCRIPT...SCRIPT
前_fn
作为特殊的内置实用程序调用,因为SCRIPT
is_fn
的<input.
printf
和输出的字符串command
被写到_fn
s标准输出>&1
- 重定向到当前 shell 的ARGV0
- 或$0
.cat
连接其<&0
标准输入文件描述符 -SCRIPT
->
截断的当前 shell 的ARGV0
参数,或$0
.完成已读入的电流复合命令,
sh exec
s 可执行文件和新重写的$0
参数。
从调用时间./file
到其包含的指令指定应exec
再次调用它,sh
以单个方式读取它复合命令在它执行它们的时候,而./file
它自己什么也不做除了愉快地接受它的新内容。实际工作的文件是/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.
毕竟谢谢
因此,当 @jw013 指定时:
接受输入并产生输出的文件就很好......
...在他对这个答案的错误批评中,他实际上无意中宽恕了这里使用的唯一方法,该方法基本上只是:
cat <new_file >old_file
回答
这里的所有答案都很好,但没有一个是完全正确的。每个人似乎都声称您无法动态且永久地路径您的#!bang
.下面是设置路径独立 shebang 的演示:
演示版
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_rewrite_me() { printf '#!' ; command -v zsh
${out+cat} ; ${out+:} . /dev/fd/0 >&2
} <<\SCRIPT >|${out-/dev/null}
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
SCRIPT
_rewrite_me ; out=$0 _rewrite_me ; exec $0
FILE
输出
$0 : ./file
lines : 13
!bang : #!/usr/bin/sh
shell : /usr/bin/sh
1 > #!/usr/bin/sh
2 > _rewrite_me() { printf '#!' ; command -v zsh
...
12 > SCRIPT
13 > _rewrite_me ; out=$0 _rewrite_me ; exec $0
$0 : /home/mikeserv/file
lines : 8
!bang : #!/usr/bin/zsh
shell : /usr/bin/zsh
1 > #!/usr/bin/zsh
2 > printf "
...
7 > sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
8 > sed -e 'N;s/\n/ >\t/' -e 4a\\...
你看?我们只是让脚本覆盖自身。而且它只会在git
同步后发生一次。从那时起,#!bang 行中的路径就正确了。
现在几乎所有的东西都只是绒毛。为了安全地执行此操作,您需要:
在顶部定义并在底部调用的函数来进行写入。通过这种方式,我们将需要的所有内容存储在内存中,并确保在开始写入之前读取整个文件。
确定路径应该是什么的某种方法。
command -v
非常适合这一点。Heredocs 确实很有帮助,因为它们是实际文件。同时他们会存储您的脚本。您也可以使用字符串,但是...
您必须确保 shell 读取的命令会在与执行脚本的命令列表相同的命令列表中覆盖您的脚本。
看:
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_rewrite_me() { printf '#!' ; command -v zsh
${out+cat} ; ${out+:} . /dev/fd/0 >&2
} <<\SCRIPT >|${out-/dev/null}
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
SCRIPT
_rewrite_me ; out=$0 _rewrite_me
exec $0
FILE
请注意,我只将exec
命令向下移动了一行。现在:
#OUTPUT
$0 : ./file
lines : 14
!bang : #!/usr/bin/sh
shell : /usr/bin/sh
1 > #!/usr/bin/sh
2 > _rewrite_me() { printf '#!' ; command -v zsh
...
13 > _rewrite_me ; out=$0 _rewrite_me
14 > exec $0
我没有得到输出的后半部分,因为脚本无法读取下一个命令。尽管如此,因为唯一缺少的命令是最后一个:
cat ./file
#!/usr/bin/zsh
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
该脚本按其应有的方式完成 - 主要是因为它全部都在heredoc中 - 但如果你没有正确计划,你可以截断你的文件流,这就是我上面发生的事情。
答案4
这是一种使用自修改脚本来修复其 shebang 的方法。该代码应添加到您的实际脚本之前。
#!/bin/sh
# unpatched
PATH=`PATH=/bin:/usr/bin:$PATH getconf PATH`
if [ "`awk 'NR==2 {print $2;exit;}' $0`" = unpatched ]; then
[ -z "`PATH=\`getconf PATH\`:/usr/local/bin:/some/long/path/to/the/right:$PATH command -v zsh`" ] && { echo "zsh not found"; exit 1; }
cp -- "$0" "$0.org" || exit 1
mv -- "$0" "$0.old" || exit 1
(
echo "#!`PATH=\`getconf PATH\`:$PATH command -v zsh`"
sed -n '/^##/,$p' $0.old
) > $0 || exit
chmod +x $0
rm $0.old
sync
exit
fi
## Original script starts here
一些评论:
它应该由有权在脚本所在目录中创建和删除文件的人运行一次。
它仅使用旧版 bourne shell 语法,尽管人们普遍认为它
/bin/sh
不能保证是 POSIX shell,即使是兼容 POSIX 的操作系统也是如此。它将 PATH 设置为符合 POSIX 标准的路径,后跟一系列可能的 zsh 位置,以避免选择“虚假”zsh。
如果由于某种原因,自修改脚本不受欢迎,那么分发两个脚本而不是一个脚本将是微不足道的,第一个脚本是您要修补的脚本,第二个脚本是我建议稍微修改以处理前者的脚本。