Como ler o script de shell inteiro antes de executá-lo?

35

Geralmente, se você editar um scrpit, todos os usos em execução do script estarão sujeitos a erros.

Pelo que entendi, o bash (outras conchas também?) Lê o script de forma incremental; portanto, se você modificou o arquivo de script externamente, ele começa a ler as coisas erradas. Existe alguma maneira de evitá-lo?

Exemplo:

sleep 20

echo test

Se você executar esse script, o bash lerá a primeira linha (digamos 10 bytes) e entrará no modo de suspensão. Quando ele é reiniciado, pode haver conteúdos diferentes no script, começando no décimo-th byte. Eu posso estar no meio de uma linha no novo script. Assim, o script em execução será quebrado.

VasyaNovikov
fonte
O que você quer dizer com "modificar o script externamente"?
maulinglawns
1
Talvez haja uma maneira de agrupar todo o conteúdo em uma função ou algo assim, para que o shell leia primeiro o script inteiro? Mas e a última linha em que você invoca a função, ela será lida até EOF? Talvez omitir o último resolvesse o problema \n? Talvez um subshell ()faça? Não tenho muita experiência com isso, por favor, ajude!
VasyaNovikov
@maulinglawns Se o script tiver conteúdo como sleep 20 ;\n echo test ;\n sleep 20e eu começar a editá-lo, ele poderá se comportar mal. Por exemplo, o bash pode ler os 10 primeiros bytes do script, entender o sleepcomando e dormir. Após o reinício, haveria conteúdos diferentes no arquivo, iniciando em 10 bytes.
VasyaNovikov
1
Então, o que você está dizendo é que está editando um script que está sendo executado? Pare o script primeiro, faça suas edições e inicie-o novamente.
maulinglawns
@ maulinglawns sim, é basicamente isso. O problema é que não é conveniente interromper os scripts e é difícil sempre me lembrar de fazer isso. Talvez haja uma maneira de forçar o bash lendo o script inteiro primeiro?
VasyaNovikov

Respostas:

43

Sim, os shells e, bashem particular, são cuidadosos ao ler o arquivo uma linha de cada vez, para que funcione da mesma maneira que quando você o usa interativamente.

Você notará que, quando o arquivo não é procurável (como um pipe), ele bashlê um byte de cada vez para ter certeza de não ler além do \ncaractere. Quando o arquivo é procurável, ele otimiza lendo blocos inteiros de cada vez, mas procura novamente após o arquivo \n.

Isso significa que você pode fazer coisas como:

bash << \EOF
read var
var's content
echo "$var"
EOF

Ou escreva scripts que se atualizem. O que você não seria capaz de fazer se não lhe desse essa garantia.

Agora, é raro que você queira fazer coisas assim e, como você descobriu, esse recurso tende a atrapalhar com mais frequência do que é útil.

Para evitá-lo, você pode tentar garantir que não modifique o arquivo no local (por exemplo, modifique uma cópia e mova a cópia no local (como sed -iou perl -pialguns editores fazem, por exemplo)).

Ou você pode escrever seu script como:

{
  sleep 20
  echo test
}; exit

(observe que é importante que exitesteja na mesma linha que }; embora você também possa colocá-lo dentro do aparelho antes do fechamento).

ou:

main() {
  sleep 20
  echo test
}
main "$@"; exit

O shell precisará ler o script até exitantes de começar a fazer qualquer coisa. Isso garante que o shell não leia novamente o script.

Isso significa que o script inteiro será armazenado na memória.

Isso também pode afetar a análise do script.

Por exemplo, em bash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Saída que U + 00E9 codificado em UTF-8. No entanto, se você alterar para:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

O \ue9será expandido no conjunto de caracteres que estava em vigor no momento em que o comando foi analisado, o que, neste caso, é antes do exportcomando ser executado.

Observe também que, se o comando sourceaka .for usado, com algumas conchas, você terá o mesmo tipo de problema para os arquivos de origem.

Não é esse o caso de bashcujo sourcecomando lê o arquivo completamente antes de interpretá-lo. Se você escrever bashespecificamente, poderá usar isso adicionando no início do script:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Eu não confiaria nisso, pois, como você pode imaginar, as versões futuras bashpodem mudar esse comportamento que atualmente pode ser visto como uma limitação (o bash e o AT&T ksh são os únicos shells do tipo POSIX que se comportam dessa maneira até onde se sabe) e o already_sourcedtruque é um pouco frágil, pois supõe que a variável não está no ambiente, sem mencionar que afeta o conteúdo da variável BASH_SOURCE)

Stéphane Chazelas
fonte
@VasyaNovikov, parece haver algo errado com o SE no momento (ou pelo menos para mim). Havia apenas algumas respostas quando adicionei a minha, e seu comentário parece ter aparecido agora, mesmo que ele tenha sido publicado há 16 minutos (ou talvez seja apenas eu perdendo minhas bolas de gude). De qualquer forma, observe a "saída" extra necessária aqui para evitar problemas quando o tamanho do arquivo aumentar (conforme observado no comentário que adicionei à sua resposta).
Stéphane Chazelas
Stéphane, acho que encontrei outra solução. É para usar }; exec true. Dessa forma, não há necessidade de novas linhas no final do arquivo, o que é amigável para alguns editores (como o emacs). Todos os testes com os quais eu consegui pensar funcionavam corretamente}; exec true
VasyaNovikov 23/11
@VasyaNovikov, não sei o que você quer dizer. Como é melhor que isso }; exit? Você também está perdendo o status de saída.
Stéphane Chazelas
Como mencionado em uma pergunta diferente: é comum primeiro analisar o arquivo inteiro e depois executar a instrução composta caso o comando dot ( . script) seja usado.
schily
@ Schily, sim, menciono isso nesta resposta como uma limitação da AT&T ksh e bash. Outras conchas do tipo POSIX não têm essa limitação.
Stéphane Chazelas
12

Você simplesmente precisa excluir o arquivo (ou seja, copiá-lo, excluí-lo, renomear a cópia novamente para o nome original). De fato, muitos editores podem ser configurados para fazer isso por você. Quando você edita um arquivo e salva um buffer alterado, em vez de substituí-lo, ele renomeia o arquivo antigo, cria um novo e coloca o novo conteúdo no novo arquivo. Portanto, qualquer script em execução deve continuar sem problemas.

Ao usar um sistema de controle de versão simples como o RCS, que está prontamente disponível para vim e emacs, você obtém a dupla vantagem de ter um histórico de suas alterações, e o sistema de checkout deve, por padrão, remover o arquivo atual e recriá-lo com os modos corretos. (Evite vincular esses arquivos, é claro).

meuh
fonte
"excluir" não faz parte do processo. Se você deseja torná-lo adequadamente atômico, renomeie o arquivo de destino - se você tiver uma etapa de exclusão, existe o risco de o processo morrer após a exclusão, mas antes da renomeação, sem deixar nenhum arquivo no lugar ( ou um leitor tenta acessar o arquivo nessa janela e não encontra versões antigas nem novas disponíveis).
Charles Duffy
11

Solução mais simples:

{
  ... your code ...

  exit
}

Dessa forma, o bash lerá todo o {}bloco antes de executá-lo e a exitdiretiva garantirá que nada seja lido fora do bloco de código.

Se você não deseja "executar" o script, mas sim "originá-lo", precisa de uma solução diferente. Isso deve funcionar então:

{
  ... your code ...

  return 2>/dev/null || exit
}

Ou se você deseja controle direto sobre o código de saída:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Voilà! Este script é seguro para editar, fonte e executar. Você ainda precisa ter certeza de que não o modifica nesses milissegundos quando está sendo lido inicialmente.

VasyaNovikov
fonte
1
O que descobri é que ele não vê o EOF e para de ler o arquivo, mas fica preso no processo de "fluxo em buffer" e acaba procurando além do final do arquivo, e é por isso que parece bom se o tamanho de o arquivo aumenta em pouco, mas fica ruim quando você o torna mais do que o dobro do tamanho anterior. Em breve, reportarei um bug aos mantenedores do bash.
Stéphane Chazelas
1
bug relatado agora , veja também patch .
Stéphane Chazelas
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
terdon
5

Prova de conceito. Aqui está um script que se modifica:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

vemos a versão alterada imprimir

Isso ocorre porque o bash load mantém um identificador de arquivo para abrir o script, portanto as alterações no arquivo serão vistas imediatamente.

Se você não deseja atualizar a cópia na memória, desvincule o arquivo original e substitua-o.

Uma maneira de fazer isso é usando sed -i.

sed -i '' filename

prova de conceito

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

Se você estiver usando um editor para alterar o script, talvez seja necessário ativar o recurso "manter uma cópia de backup" para fazer com que o editor grave a versão alterada em um novo arquivo, em vez de substituir o existente.

Jasen
fonte
2
Não, bashnão abre o arquivo com mmap(). É apenas cuidadoso ler uma linha de cada vez, conforme necessário, assim como quando está recebendo os comandos de um dispositivo terminal quando interativo.
Stéphane Chazelas
2

O agrupamento do script em um bloco {}provavelmente é a melhor opção, mas requer a alteração dos scripts.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

seria a segunda melhor opção (assumindo tmpfs ) a desvantagem é que ele quebra $ 0 se seus scripts o usarem.

usar algo como F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashé menos ideal porque ele precisa manter o arquivo inteiro na memória e quebra $ 0.

tocar no arquivo original deve ser evitado para que, na hora da última modificação, bloqueios de leitura e links físicos não sejam perturbados. Dessa forma, você pode deixar um editor aberto enquanto executa o arquivo e o rsync não faz a soma de verificação desnecessária do arquivo para backups e links físicos, conforme o esperado.

substituir o arquivo na edição funcionaria, mas é menos robusto, porque não é aplicável a outros scripts / usuários / ou pode-se esquecer. E, novamente, ele quebraria os links físicos.

user1133275
fonte
qualquer coisa que faça uma cópia funcionaria. tac test.sh | tac | bash
Jasen