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

33

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

Até onde eu entendi, bash (outras shells também?) lê o script de forma incremental, então se você modificou o arquivo de script externamente, ele começa a ler as coisas erradas. Existe alguma maneira de evitar isso?

Exemplo:

sleep 20

echo test

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

    
por VasyaNovikov 21.12.2016 / 08:55

5 respostas

42

Sim, shells e bash em particular, têm o cuidado de ler o arquivo uma linha de cada vez, por isso funciona da mesma forma que quando você o utiliza interativamente.

Você notará que quando o arquivo não é pesquisável (como um canal), bash até mesmo lê um byte de cada vez para ter certeza de não ler além do caractere \n . Quando o arquivo é procurado, ele otimiza lendo blocos inteiros de cada vez, mas procure novamente após o \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 desse garantia.

Agora, é raro que você queira fazer coisas desse tipo e, como você descobriu, esse recurso tende a atrapalhar mais do que é útil.

Para evitar isso, você pode tentar e não modificar o arquivo no local (por exemplo, modificar uma cópia e mover a cópia no lugar (como sed -i ou perl -pi e alguns editores não por exemplo)).

Ou você pode escrever seu roteiro como:

{
  sleep 20
  echo test
}; exit

(note que é importante que o exit esteja na mesma linha que } ; embora você também possa colocá-lo dentro das chaves um pouco antes do fechamento).

ou:

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

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

Isso significa que todo o script 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'

A saída seria U + 00E9 codificada em UTF-8. No entanto, se você alterá-lo para:

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

O \ue9 será expandido no conjunto de caracteres que estava em vigor no momento em que o comando foi analisado, o que neste caso é anterior ao comando export .

Observe também que, se o comando source aka . for usado, com alguns shells, você terá o mesmo tipo de problema para os arquivos originados.

Esse não é o caso de bash , cujo comando source lê o arquivo totalmente antes de interpretá-lo. Se escrever para bash especificamente, você poderia realmente fazer uso disso, adicionando no início do script:

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

(Eu não confiaria nisso, como você poderia imaginar versões futuras de bash poderiam mudar esse comportamento que pode ser visto atualmente como uma limitação (bash e AT & T ksh são os únicos shells parecidos com POSIX que se comportam) assim, tanto quanto posso dizer) e o truque already_sourced é um pouco frágil, uma vez que assume que a variável não está no ambiente, para não mencionar que ela afeta o conteúdo da variável BASH_SOURCE

    
por 21.12.2016 / 13:50
12

Você simplesmente precisa excluir o arquivo (ou seja, copiá-lo, excluí-lo, renomear a cópia de volta para o nome original). Na verdade, muitos editores podem ser configurados para fazer isso por você. Quando você edita um arquivo e salva um buffer alterado nele, em vez de sobrescrever o arquivo, 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.

Usando um sistema de controle de versão simples como o RCS que está prontamente disponível para o vim e o emacs, você tem a dupla vantagem de ter um histórico de suas alterações, e o sistema de checkout deve remover o arquivo atual e recriá-lo com o modos corretos. (Cuidado com hard-linking desses arquivos, claro).

    
por 21.12.2016 / 09:52
11

Solução mais simples:

{
  ... your code ...

  exit
}

Desta forma, o bash lerá todo o bloco {} antes de executá-lo, e a diretiva exit fará com que nada seja lido fora do bloco de código.

Se você não quiser "executar" o script, mas sim "fonte", você precisa de uma solução diferente. Isso deve funcionar então:

{
  ... your code ...

  return 2>/dev/null || exit
}

Ou se você quiser 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, gerar e executar. Você ainda precisa ter certeza de que não irá modificá-lo nesses milésimos de segundo quando estiver sendo lido inicialmente.

    
por 21.12.2016 / 11:33
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 modificada da impressão

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

Se você não quiser 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, ativar o recurso "manter uma cópia de backup" pode ser tudo o que é necessário para fazer com que o editor grave a versão alterada em um novo arquivo, em vez de sobrescrever o existente.

    
por 21.12.2016 / 09:39
2

Encapsular seu script em um bloco {} é provavelmente a melhor opção, mas requer a alteração de seus scripts.

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

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

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

tocar no arquivo original deve ser evitado para que, pela última vez, leia os bloqueios e os links físicos não sejam afetados. Dessa forma, você pode deixar um editor aberto durante a execução do arquivo, e o rsync não irá, desnecessariamente, verificar o arquivo para backups e hard links funcionar como esperado.

substituir o arquivo na edição funcionaria, mas é menos robusto porque não é aplicável a outros scripts / usuários / ou pode ser esquecido. E mais uma vez, isso quebraria os links físicos.

    
por 21.12.2016 / 09:43

Tags