O que acontece se você editar um script durante a execução?

31

Eu tenho uma pergunta geral, que pode ser o resultado de um mal-entendido de como os processos são tratados no Linux.

Para meus propósitos, vou definir um 'script' como um trecho de código de bash salvo em um arquivo de texto com as permissões de execução ativadas para o usuário atual.

Eu tenho uma série de scripts que se chamam em conjunto. Por uma questão de simplicidade, eu os chamarei de scripts A, B e C. O script A executa uma série de instruções e faz uma pausa, depois executa o script B, depois faz uma pausa e depois executa o script C. Em outras palavras, a série de etapas é algo como isto:

Execute o Script A:

  1. Série de declarações
  2. Pausa
  3. Executar script B
  4. Pausa
  5. Executar script C

Sei por experiência própria que, se eu executar o script A até a primeira pausa, e fizer edições no script B, essas edições serão refletidas na execução do código quando eu permitir que ele seja retomado. Da mesma forma, se eu fizer edições no script C enquanto o script A ainda estiver em pausa e permitir que continue após salvar as alterações, essas alterações serão refletidas na execução do código.

Aqui está a verdadeira questão: existe alguma maneira de editar o Script A enquanto ele ainda está em execução? Ou é impossível editar uma vez que sua execução começa?

Especialista em cafeína
fonte
2
eu acho que depende da casca. embora você afirme que está usando o bash. parece que seria dependente da maneira como o shell carrega scripts internamente.
strugee
o comportamento também pode mudar se você originar o arquivo em vez de executá-lo.
strugee
11
Eu acho que o bash lê um script inteiro na memória antes de executar.
W4etwetewtwet
2
@ handuel, não, não. Como se não esperasse até você digitar "exit" no prompt para começar a interpretar os comandos digitados.
Stéphane Chazelas
11
@StephaneChazelas Sim, ler no terminal não é, mas isso é diferente de executar um script.
W4etwetewtwet

Respostas:

21

No Unix, a maioria dos editores trabalha criando um novo arquivo temporário que contém o conteúdo editado. Quando o arquivo editado é salvo, o arquivo original é excluído e o arquivo temporário renomeado para o nome original. (Existem, é claro, várias salvaguardas para impedir o dataloss.) Esse é, por exemplo, o estilo usado por sedou perlquando chamado com a -ibandeira ("in-place"), que não é realmente "in-place". Deveria ter sido chamado de "novo local com nome antigo".

Isso funciona bem porque o unix garante (pelo menos para sistemas de arquivos locais) que um arquivo aberto continue existindo até que seja fechado, mesmo que seja "excluído" e um novo arquivo com o mesmo nome seja criado. (Não é coincidência que a chamada do sistema unix para "excluir" um arquivo seja chamada de "desvincular".) Portanto, de um modo geral, se um interpretador de shell tiver algum arquivo de origem aberto e você "editar" o arquivo da maneira descrita acima , o shell nem verá as alterações, pois ainda possui o arquivo original aberto.

[Nota: como em todos os comentários baseados em padrões, os itens acima estão sujeitos a várias interpretações e existem vários casos de canto, como o NFS. Os pedestres são convidados a preencher os comentários com exceções.]

É claro que é possível modificar arquivos diretamente; simplesmente não é muito conveniente para fins de edição, porque, embora você possa sobrescrever dados em um arquivo, não é possível excluir ou inserir sem alterar todos os dados a seguir, o que implicaria muita reescrita. Além disso, enquanto você fazia essa troca, o conteúdo do arquivo seria imprevisível e os processos que tinham o arquivo aberto seriam prejudicados. Para se livrar disso (como nos sistemas de banco de dados, por exemplo), você precisa de um conjunto sofisticado de protocolos de modificação e bloqueios distribuídos; coisas que estão muito além do escopo de um utilitário típico de edição de arquivos.

Portanto, se você deseja editar um arquivo enquanto este está sendo processado por um shell, você tem duas opções:

  1. Você pode anexar ao arquivo. Isso sempre deve funcionar.

  2. Você pode sobrescrever o arquivo com novos conteúdos exatamente do mesmo comprimento . Isso pode ou não funcionar, dependendo se o shell já leu essa parte do arquivo ou não. Como a maioria das E / S de arquivos envolve buffers de leitura, e como todos os shells que conheço leem um comando composto inteiro antes de executá-lo, é muito improvável que você possa se safar disso. Certamente não seria confiável.

Não conheço nenhuma redação no padrão Posix que realmente exija a possibilidade de anexar a um arquivo de script enquanto o arquivo está sendo executado, portanto, pode não funcionar com todos os shell compatíveis com Posix, muito menos com a oferta atual de quase e conchas às vezes compatíveis com posix. Então YMMV. Mas, tanto quanto eu sei, ele funciona de maneira confiável com o bash.

Como evidência, aqui está uma implementação "sem loop" do infame programa de 99 garrafas de cerveja no bash, que ddsubstitui e acrescenta (a substituição é presumivelmente segura porque substitui a linha em execução no momento, que é sempre a última linha do arquivo, com um comentário exatamente do mesmo comprimento; fiz isso para que o resultado final possa ser executado sem o comportamento de modificação automática.)

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #
rici
fonte
Quando eu executo isso, ele começa com "Não mais" e depois continua para -1 e para os números negativos indefinidamente.
Daniel Hershcovich 4/13/13
Se eu fizer isso export beer=100antes de executar o script, ele funcionará conforme o esperado.
Daniel Hershcovich 4/13/13
@DanielHershcovich: muito certo; teste superficial da minha parte. Acho que consertei; agora é necessário um parâmetro opcional de contagem. Uma correção melhor e mais interessante seria redefinir automaticamente se o parâmetro não corresponder à cópia em cache.
rici 4/09/2013
18

bash percorre um longo caminho para garantir que ele leia comandos antes de executá-los.

Por exemplo, em:

cmd1
cmd2

O shell lerá o script por blocos, provavelmente lerá os dois comandos, interpretará o primeiro e, em seguida, procurará o final do cmd1script e lerá o script novamente para lê cmd2-lo e executá-lo.

Você pode verificar facilmente:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(embora olhando para a stracesaída disso, parece que ele faz algumas coisas mais sofisticadas (como ler os dados várias vezes, procurar de volta ...) do que quando tentei o mesmo há alguns anos, então minha declaração acima sobre a busca por retorno pode não se aplica mais em versões mais recentes).

Se, no entanto, você escrever seu script como:

{
  cmd1
  cmd2
  exit
}

O shell terá que ler até o fechamento }, armazenar na memória e executá-lo. Por causa disso exit, o shell não lê o script novamente, para que você possa editá-lo com segurança enquanto o shell o estiver interpretando.

Como alternativa, ao editar o script, certifique-se de escrever uma nova cópia do script. O shell continuará lendo o original (mesmo que seja excluído ou renomeado).

Para fazer isso, mudar o nome the-scriptpara the-script.olde copiar the-script.oldpara the-scripte editá-lo.

Stéphane Chazelas
fonte
4

Não há realmente nenhuma maneira segura de modificar o script enquanto estiver em execução, porque o shell pode usar buffer para ler o arquivo. Além disso, se o script for modificado substituindo-o por um novo arquivo, os shells normalmente apenas lerão o novo arquivo após executar determinadas operações.

Geralmente, quando um script é alterado durante a execução, o shell acaba relatando erros de sintaxe. Isso ocorre porque, quando o shell fecha e reabre o arquivo de script, ele usa o deslocamento de bytes no arquivo para se reposicionar no retorno.

cinza
fonte
4

Você pode contornar isso definindo uma armadilha no seu script e usando execpara selecionar o novo conteúdo do script. Observe, no entanto, que a execchamada inicia o script do zero e não de onde chegou no processo em execução; portanto, o script B será chamado (e assim por diante).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Isso continuará exibindo a data na tela. Eu poderia editar meu script e mudar datepara echo "Date: $(date)". Ao escrever isso, o script em execução ainda exibe a data. No entanto, se eu enviar o sinal que defini trappara capturar, o script exec(substitui o processo atual em execução pelo comando especificado), que é o comando $CMDe os argumentos $@. Você pode fazer isso emitindo kill -1 PID- onde PID é o PID do script em execução - e a saída muda para mostrar Date:antes da datesaída do comando.

Você pode armazenar o "estado" do seu script em um arquivo externo (em / tmp) e ler o conteúdo para saber onde "retomar" quando o programa for reexecutado. Você pode então adicionar uma terminação de traps adicionais (SIGINT / SIGQUIT / SIGKILL / SIGTERM) para limpar esse arquivo tmp, portanto, quando você reiniciar após interromper o "Script A", ele começará do início. Uma versão com estado seria algo como:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup
Drav Sloan
fonte
Corrigi esse problema capturando $0e $@no início do script e usando essas variáveis ​​no execlugar.
Drav Sloan