注意:@ jw013在以下注释中提出了以下不受支持的反对意见:
不好的理由是因为自修改代码通常被认为是不好的做法。在过去的小型汇编程序中,这是减少条件分支和提高性能的一种聪明方法,但是如今,安全风险胜过了优点。如果运行脚本的用户对该脚本没有写权限,则您的方法将行不通。
我通过指出回答了他的安全反对任何特殊权限只需要一次,每次安装/更新行动,以安装/更新的自安装的,我会亲自打电话很安全-脚本。我还指出他man sh
提到了通过类似手段实现类似目标的情况。当时,我没有费心指出,无论安全缺陷或其他通常不建议的做法可能会或可能不会出现在我的答案中,它们更可能植根于问题本身而不是我的答案:
如何设置shebang,以便将脚本作为/path/to/script.sh运行始终使用PATH中可用的Zsh?
不满意,@ jw013继续反对,至少提出了两个错误的陈述,以进一步推论其尚不被支持的论点:
您使用一个文件,而不是两个文件。的[ man sh
引用]
封装具有一个文件修改另一个文件。您有一个修改自身的文件。这两种情况之间有明显的区别。接受输入并产生输出的文件就可以了。可执行文件在运行时会自行更改通常是一个坏主意。您所指向的示例没有这样做。
首先:
THE ONLY EXECUTABLE代码将任何EXECUTABLE 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脚本文件的执行,以涉及少于两个文件。zsh
自己的编译器中可能存在例外,但是由于这一点,我的经验很少,在这里没有任何代表。
Shell脚本的哈希值必须指向其预期的解释器,否则将被丢弃。
Shell具有解析和解释其输入的两种基本模式:其当前输入定义a <<here_document
或定义a- { ( command |&&|| list ) ; } &
换句话说,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-builtins,子shell,当前环境等时,它会变得更加复杂,但是就我在这里的目的而言,这已经足够了。
和发言的内置插件和命令列表,一个function() { declaration ; }
仅仅是分配的一种手段复合命令到一个简单的命令。Shell不能$expansions
对声明语句本身执行任何操作(包括)<<redirections>
,而必须将定义存储为单个文字字符串,并在调用时作为特殊的内置Shell执行。
因此,在可执行Shell脚本中声明的Shell函数将以其原义字符串形式存储在解释Shell的内存中(未扩展为包含附加的此处文档作为输入),并且在每次称为Shell build-时都独立于其源文件执行。只要shell的当前环境持续存在就可以。
重定向操作符<<
和<<-
两者都允许将外壳程序输入文件(称为here-document)中包含的行重定向到命令的输入。
在这里,文件将被视为后的下一个开头一个字\newline
,并继续,直到有一个只包含一个行分隔符和\newline
,没有[:blank:]
在s之间。然后,下一个此处文档开始(如果有)。格式如下:
[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)
而dash
的实现至少是POSIXly可移植的。
这使得几个 FILES
所以在下面的答案中,当我这样做时:
{ 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
是_fn
的<input.
输出通过串printf
并command
写出到_fn
的标准出来 >&1
-这被重定向到当前shell的ARGV0
-或$0
。
cat
在截断的当前shell的参数或之上连接其<&0
标准输入文件描述符- 。SCRIPT
>
ARGV0
$0
完成其已经读入的当前复合命令,即可sh exec
执行文件和新重写的$0
参数。
从./file
调用时间开始,直到其包含的指令指定应exec
再次执行d,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确实有帮助,因为它们是实际文件。他们将同时存储您的脚本。您也可以使用字符串,但是...
您必须确保外壳程序在与执行脚本的命令相同的命令列表中读入覆盖脚本的命令。
看:
{ 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中-但是如果您不正确地计划它,则可以截断您的文件流,这就是我上面遇到的情况。
env
同时不在/ bin和/ usr / bin中吗?尝试which -a env
确认。