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 shreferê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 shreferenciado] 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 zshpró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_documentou 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 $expansionnenhum 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 \newlinecaractere 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 $expansionsdeclaraçã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 \newlinee 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 nrepresenta 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)shshell, cria um arquivo regular /tmp, despeja a saída, mapeia-o para um descritor e exclui o /tmparquivo para que a cópia do descritor do kernel seja o que resta. dashevita todo esse absurdo e simplesmente coloca seu processamento de saída em um |pipearquivo anônimo destinado ao <<destino de redirecionamento .
Isso faz com que dash:
cmd <<HEREDOC
$(cmd)
HEREDOC
funcionalmente equivalente a bash's:
cmd <(cmd)
enquanto dasha 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 cato conteúdo de qualquer arquivo do shell criado para FILEdentro ./file, torná-lo executável, em seguida, executá-lo.
O kernel interpreta #!e chama /usr/bin/shcom um <read descritor de arquivo atribuído ./file.
shmapeia uma string na memória que consiste no comando composto começando em _fn()e terminando em SCRIPT.
Quando _fné chamado, shdeve primeiro interpretar, em seguida, mapear para um descritor de arquivo definido no <<SCRIPT...SCRIPT antes invocando _fncomo um especial built-in utilitário porque SCRIPTé _fn's<input.
A saída de cordas por printfe commandestão escritas para _fn's padrão-out >&1 - que é redirecionada para o atual shell de ARGV0- ou $0.
catconcatena 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 $0argumento executável - e recentemente reescrito - .
Desde o momento em que ./fileé chamado até que suas instruções contidas especifiquem que ele deve ser execd novamente, o shlê em um único comando composto de cada vez enquanto os executa, enquanto ./fileele 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 gitsincronizaçã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 execcomando 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.
envnão está em / bin e / usr / bin? Tentewhich -a envconfirmar.