NOTA: @ jw013 faz a seguinte objeção sem suporte nos comentários abaixo:
O voto negativo é porque o código de modificação automática é geralmente considerado uma prática ruim. Nos velhos tempos de pequenos programas de montagem, era uma maneira inteligente de reduzir filiais condicionais e melhorar o desempenho, mas hoje em dia os riscos de segurança superam as vantagens. Sua abordagem não funcionaria se o usuário que executou o script não tivesse privilégios de gravação no script.
Eu respondi suas objeções de segurança, salientando que quaisquer permissões especiais são exigido somente uma vez por instalação / atualização de ação, a fim de instalar / atualizar o auto-instalação roteiro - que eu pessoalmente muito chamada segura. Eu também o apontei para uma man sh
referência para alcançar objetivos semelhantes por meios semelhantes. Na época, eu não me incomodei em apontar que quaisquer falhas de segurança ou práticas geralmente desaconselhadas que possam ou não ser representadas na minha resposta, elas provavelmente estão mais enraizadas na própria pergunta do que na minha resposta:
Como posso configurar o shebang para que a execução do script como /path/to/script.sh sempre use o Zsh disponível no PATH?
Não satisfeito, @ jw013 continuou a objetar, promovendo seu argumento ainda não suportado com pelo menos algumas declarações erradas:
Você usa um único arquivo, não dois. O
pacote [ man sh
referenciado] possui um arquivo, modifica outro arquivo. Você tem um arquivo se modificando. Há uma diferença distinta entre esses dois casos. Um arquivo que recebe entrada e produz saída é bom. Um arquivo executável que muda automaticamente enquanto é executado geralmente é uma má ideia. O exemplo que você apontou não faz isso.
Em primeiro lugar:
A ÚNICA EXECUTABLE código em qualquer EXECUTABLE Shell Script é o #!
SE
(embora mesmo não #!
seja oficialmente especificado )
{ 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
Um script de shell é apenas um arquivo de texto - para que ele tenha algum efeito, ele deve ser lido por outro arquivo executável, suas instruções então interpretadas por esse outro arquivo executável, antes de finalmente o outro arquivo executável executar sua interpretação do script de shell. Não é possível para a execução de um arquivo de script de shell envolver menos de dois arquivos. Existe uma possível exceção no zsh
próprio compilador, mas com isso eu tenho pouca experiência e não é de forma alguma representada aqui.
O hashbang de um script de shell deve apontar para o intérprete pretendido ou ser descartado como irrelevante.
O shell possui dois modos básicos de analisar e interpretar sua entrada: ou sua entrada atual está definindo um <<here_document
ou está definindo um { ( command |&&|| list ) ; } &
- em outras palavras, o shell interpreta um token como um delimitador para um comando que ele deve executar depois de lê-lo em ou como instruções para criar um arquivo e mapeá-lo para um descritor de arquivo para outro comando. É isso aí.
Ao interpretar comandos para executar, o shell delimita os tokens em um conjunto de palavras reservadas. Quando o shell encontra uma abertura simbólica deve continuar a ler em uma lista de comandos até que a lista é ou delimitado por um fechamento simbólico como uma nova linha - quando aplicável - ou o fechamento token de como })
para ({
antes da execução.
O shell distingue entre um comando simples e um comando composto. O comando composto é o conjunto de comandos que devem ser lidos antes da execução, mas o shell não executa $expansion
nenhum dos seus comandos simples constituintes até que ele execute individualmente cada um.
Portanto, no exemplo a seguir, as ;semicolon
palavras reservadas delimitam comandos simples individuais, enquanto o \newline
caractere não escapado delimita entre os dois comandos compostos:
{ 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
Essa é uma simplificação das diretrizes. Fica muito mais complicado quando você considera shell-builtins, subshells, ambiente atual e etc, mas, para meus propósitos aqui, é suficiente.
E por falar em built-ins e listas de comandos, a function() { declaration ; }
é apenas um meio de atribuir um comando composto a um comando simples. O shell não deve executar nenhuma $expansions
declaração na declaração em si - para incluir <<redirections>
-, mas deve armazenar a definição como uma única cadeia literal e executá-la como um shell especial embutido quando solicitado.
Portanto, uma função de shell declarada em um script de shell executável é armazenada na memória do shell de interpretação em sua forma de string literal - não expandida para incluir documentos aqui anexados como entrada - e executada independentemente do seu arquivo de origem toda vez que é chamada de shell. enquanto o ambiente atual do shell durar.
Os operadores de redirecionamento <<
e os <<-
dois permitem o redirecionamento de linhas contidas em um arquivo de entrada do shell, conhecido como documento aqui, para a entrada de um comando.
O documento aqui deve ser tratado como uma única palavra que começa após a próxima \newline
e continua até que exista uma linha contendo apenas o delimitador e a \newline
, sem [:blank:]
s no meio. Então, o próximo documento aqui começa, se houver um. O formato é o seguinte:
[n]<<word
here-document
delimiter
... em que o opcional n
representa o número do descritor de arquivo. Se o número for omitido, o documento aqui se refere à entrada padrão (descritor de arquivo 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
Entende? Para cada shell acima do shell, cria um arquivo e o mapeia para um descritor de arquivo. No zsh, (ba)sh
shell, cria um arquivo regular /tmp
, despeja a saída, mapeia-o para um descritor e exclui o /tmp
arquivo para que a cópia do descritor do kernel seja o que resta. dash
evita todo esse absurdo e simplesmente coloca seu processamento de saída em um |pipe
arquivo anônimo destinado ao <<
destino de redirecionamento .
Isso faz com que dash
:
cmd <<HEREDOC
$(cmd)
HEREDOC
funcionalmente equivalente a bash
's:
cmd <(cmd)
enquanto dash
a implementação é pelo menos POSIXly portátil.
QUE FAZ DIVERSOS ARQUIVOS
Então, na resposta abaixo quando eu faço:
{ 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
O seguinte acontece:
A primeira vez que cat
o conteúdo de qualquer arquivo do shell criado para FILE
dentro ./file
, torná-lo executável, em seguida, executá-lo.
O kernel interpreta #!
e chama /usr/bin/sh
com um <read
descritor de arquivo atribuído ./file
.
sh
mapeia uma string na memória que consiste no comando composto começando em _fn()
e terminando em SCRIPT
.
Quando _fn
é chamado, sh
deve primeiro interpretar, em seguida, mapear para um descritor de arquivo definido no <<SCRIPT...SCRIPT
antes invocando _fn
como um especial built-in utilitário porque SCRIPT
é _fn
's<input.
A saída de cordas por printf
e command
estão escritas para _fn
's padrão-out >&1
- que é redirecionada para o atual shell de ARGV0
- ou $0
.
cat
concatena seu descritor de arquivo de <&0
entrada padrão - SCRIPT
- sobre o argumento >
do shell atual truncado ARGV0
, ou $0
.
A conclusão do comando composto atual já lido , sh exec
é o $0
argumento executável - e recentemente reescrito - .
Desde o momento em que ./file
é chamado até que suas instruções contidas especifiquem que ele deve ser exec
d novamente, o sh
lê em um único comando composto de cada vez enquanto os executa, enquanto ./file
ele próprio não faz nada, exceto aceitar alegremente seu novo conteúdo. Os arquivos que estão realmente no trabalho são/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.
OBRIGADO, DEPOIS DE TUDO
Então, quando @ jw013 especifica que:
Um arquivo que recebe entrada e produz saída é bom ...
... entre suas críticas errôneas a essa resposta, ele está, na verdade, sem querer, desculpando o único método usado aqui, que basicamente funciona apenas para:
cat <new_file >old_file
RESPONDA
Todas as respostas aqui são boas, mas nenhuma delas está totalmente correta. Todo mundo parece reivindicar que você não pode seguir seu caminho de forma dinâmica e permanente #!bang
. Aqui está uma demonstração de como configurar um caminho independente de caminho:
DEMO
{ 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
SAÍDA
$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\\...
Entende? Acabamos de fazer o script se sobrescrever. E isso só acontece uma vez após uma git
sincronização. A partir desse ponto, ele tem o caminho certo na linha #! Bang.
Agora, quase tudo isso lá em cima é apenas fofo. Para fazer isso com segurança, você precisa:
Uma função definida na parte superior e chamada na parte inferior que faz a escrita. Dessa forma, armazenamos tudo o que precisamos na memória e garantimos que todo o arquivo seja lido antes de começarmos a escrevê-lo.
Alguma maneira de determinar qual deve ser o caminho. command -v
é muito bom para isso.
Heredocs realmente ajudam porque são arquivos reais. Enquanto isso, eles armazenam seu script. Você pode usar cordas também, mas ...
Você precisa garantir que o shell leia o comando que sobrescreve seu script na mesma lista de comandos que a executada.
Veja:
{ 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
Observe que eu mudei o exec
comando apenas uma linha. Agora:
#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
Não recebo a segunda metade da saída porque o script não pode ler no próximo comando. Ainda assim, porque o único comando que faltava era o último:
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\\...
O script surgiu como deveria - principalmente porque estava no heredoc - mas se você não planejar corretamente, poderá interromper o fluxo de arquivos, o que aconteceu comigo acima.
env
não está em / bin e / usr / bin? Tentewhich -a env
confirmar.