Limpeza de erros para scripts de shell

1

Qual é a melhor maneira de incluir lógica de limpeza de erros para scripts de shell?

Especificamente, tenho um script que faz algo assim:

mount a x
mount b y
setup_thing
mount c z
do_something
umount z
cleanup_thing
umount y
umount x

Qualquer uma das montagens, além de do_something , pode falhar. Se, digamos, o mount c z falhar, eu gostaria que o script desmontasse as montagens que tiveram êxito antes de sair.

Eu não quero ter que repetir o código de limpeza várias vezes, e eu não quero envolver tudo em um if-nest (porque então você tem que re-recuar tudo se você adicionar uma montagem extra).

Existe uma maneira de criar uma pilha final ou algo assim, de modo que o acima poderia ser escrito como:

set -e
mount a x && finally umount x
mount b y && finally umount y
setup_thing && finally cleanup_thing
mount c z && finally umount z
do_something

A idéia é que uma vez que o comando "finally" esteja registrado, ele executará (em ordem inversa) esse comando ao sair, passar ou falhar - mas as coisas que não foram configuradas com sucesso não serão limpas. (porque a limpeza pode não ser segura se a configuração falhar).

Ou, em outras palavras, se tudo der certo, ele executará umount z , cleanup_thing , umount y , umount x nessa ordem - mesmo se apenas do_something falhar. Mas se o mount b y falhar, ele só executará umount x .

(E independentemente disso, é necessário sair com 0 ou não-0 adequadamente, embora não seja necessário preservar o código de saída exato na falha.)

Eu sei que há um trap embutido que permite executar um comando na saída, mas ele suporta apenas um comando e o substitui a cada vez. Existe uma maneira de estender isso em uma pilha como acima? Ou alguma outra maneira limpa de fazer isso?

Nos padrões de manipulação de erros do código Ye Olde C, você provavelmente alcançaria algo assim com x || goto cleanup_before_x , mas é claro que não há nenhum goto.

Idealmente, algo que funciona em (da) sh, embora eu esteja ok com a exigência de bash se isso simplifica as coisas. (Talvez usando matrizes?)

    
por Miral 13.07.2017 / 04:52

3 respostas

0

Revisado para lidar com pontos de montagem pré-existentes. Supondo que o código esteja neste formato, em que as linhas que podem causar problemas tenham um comando por linha, que é mount ou umount :

mount a x
mount b y
setup_thing
mount c z
do_something
umount z
cleanup_thing
umount y
umount x

Este kludge pode funcionar ... copie este código para a segunda linha do script:

mount | cut -d' ' -f3 | sed 's/.*/^u\?mount [^[:space:]]\* &$/' | 
  grep -v -f - $0 | sed 2d | exec sh -s -- "$@"

Como isso (teoricamente) funciona:

  1. Use mount e cut para criar uma lista de pontos de montagem pré-existentes, para serem deixados em paz.
  2. Use sed para criar uma lista de grep padrões que correspondam aos comandos mount ou umount que usam qualquer um desses pontos de montagem.
  3. Use grep -v para pesquisar o script atual em busca de linhas que não correspondam à lista anterior de grep patterns. Deixando todo o código adequado para ser executado.
  4. Use sed para remover a segunda linha, para evitar a recursão.
  5. Use exec sh -s -- "$@" para executar apenas esse código, junto com qualquer argumento de linha de comando. Nada além da linha exec é executada.

Teste de antemão alterando exec para cat # e examinando a saída. Se estiver bom, coloque exec de volta.

    
por 13.07.2017 / 08:11
0

Pessoalmente, eu iria para o aninhado if then , pois seria mais fácil de ler e manter. No entanto, se você tiver muitos níveis de aninhamento, poderá tentar algo assim (eu coloquei o prefixo echo para testar):

#!/bin/bash
run1(){ echo mount a x;}
run2(){ echo mount b y;}
run3(){ echo setup_thing;}
run4(){ echo mount c z;}
run5(){ echo do_something;}

undo5(){ :;}
undo4(){ echo umount z;}
undo3(){ echo cleanup_thing;}
undo2(){ echo umount y;}
undo1(){ echo umount x;}

for i in {1..5}
do      run$i
        code=$?
        [ $code != 0 ] && break
done
let i=i-1
while [ $i -gt 0 ]
do      undo$i
        let i=i-1
done
exit $code

Mantive as funções de execução e desfazer na ordem do seu exemplo, mas você pode ganhar colocando-as mais próximas:

run1(){  mount a x;}
undo1(){ umount x;}
run2(){  mount b y;}
undo2(){ umount y;}
run3(){  setup_thing;}
undo3(){ cleanup_thing;}
...

Em vez de numerar as funções 1,2,3 ... você poderia nomear as funções run e listar os nomes na ordem desejada. Adicione um prefixo consistente para a função desfazer para facilitar:

mnta(){ ... }
undomnta(){ ... }
mntb(){ ... }
undomntb(){ ... }

order='mnta mntb ...' toundo=
for i in $order
do      $i
        code=$?
        [ $code != 0 ] && break
        toundo="undo$i $toundo"
done
for i in $toundo
do  $i
done

Ou você poderia usar trap , mas configurá-lo apenas uma vez com trap mytrap exit e usar uma variável global para reter o que limpar e adicioná-lo a cada etapa: clean="1 $clean" . A função mytrap passaria então pelos valores em $clean .

    
por 13.07.2017 / 10:45
0

Uma técnica comum é usar um trap para desfazer tudo quando estiver pronto. Ele precisa ser idempotente, ou seja, deve falhar normalmente se algumas das etapas não puderem ser concluídas.

#!/bin/bash

clean_up=false
errorhandler () {
    umount z || true
    $clean_up && cleanup_thing
    umount y || true
    umount x
}
trap errorhandler ERR EXIT

mount a x
mount b y
setup_thing
clean_up=true
mount c z
do_something

Observe que o trap também é acionado em EXIT , de modo que será executado antes que o script termine normalmente; então você não precisa limpar explicitamente.

O pseudo-sinal ERR é uma extensão do Bash, eu acredito. Então isso não vai funcionar em Ash / Dash / legado Bourne shell etc.

    
por 13.07.2017 / 11:29