Sair com segurança enquanto faz loops no bash

7

Digamos que eu tenha um script bash que faça:

while :
do
    foo
done

Eu gostaria de poder executar este script a partir do console e poder sair dele em um tempo arbitrário, contanto que isso aconteça entre duas execuções de foo. Então, se, digamos, eu pressionar Ctrl + C (pode ser outra ação que faça o script sair, Ctrl + C é apenas um exemplo) que irá sair no próximo ponto disponível após o foo ser executado:

while :
do
    foo
    if [pressed_ctrl_c]:
        break
done
    
por Henry Henrinson 09.04.2015 / 17:40

2 respostas

5

Você pode tentar esse tipo de construção:

#!/bin/bash
#
INTR=
trap 'INTR=yes; echo "** INTR **" >&2' INT

while :
do
    (
        # Protect the subshell block
        trap '' INT

        # Protected code here
        echo -n "The date/time is: "
        sleep 2
        date
        read -t2 -p 'Continue (y/n)? ' YN || echo
        test n = "$YN" && echo "Asked for BREAK" >&2 && exit 90
    )
    SS=$?
    test 90 -eq $SS && echo "Matched BREAK" >&2 && break

    # Ctrl/C, perhaps?
    test yes = "$INTR" && echo "Matched INTR" >&2 && break
done
exit 0

Algumas notas

  • O par read e test demonstra o controle interativo para o segmento de código protegido dentro do bloco ( ... ) .
  • O exit 90 é o equivalente a break , mas dentro de um subshell. A linha test 0 != $? ... imediatamente após o subnível final termina para capturar o status exit 90 e implementar o break que o código realmente queria.
  • O subshell pode usar diferentes valores de status de saída para indicar diferentes tipos de fluxo de controle necessário ( break , exit , etc ...)
  • Isso não impede que um programa instale seu próprio manipulador de sinal. Por exemplo, gdb instala seu próprio manipulador para SIGINT ( Ctrl C ). Se o objetivo é evitar que um usuário saia da sessão, a alteração da chave de interrupção pode ajudar a ofuscar a situação (veja o código abaixo). Deselegante, mas potencialmente eficaz.

Alterando a tecla SIGINT no terminal

G=$(stty -g)                    # Save settings
test -n "$G" && stty intr ^A    # That is caret and A, not Ctrl/A

# ... SIGINT generated with Ctrl/A rather than Ctrl/C ...

test -n "$G" && stty "$G"       # Restore original settings
    
por 09.04.2015 / 18:54
4

Isso parece funcionar:

#!/bin/sh
pressed_ctrl_c=
trap "pressed_ctrl_c=1" INT
while true
do
        (trap "" INT; foo)&
        wait  ||  wait
        if [ "$pressed_ctrl_c" ]
        then
                # echo
                break
        fi
done
  • Inicialize pressed_ctrl_c para nulo. Isso provavelmente não é necessário em um script.
  • trap command signum diz ao shell para configurar para capturar o número de sinal signum e execute command quando ele pega um. "INT" é a abreviação de "SIGINT", que é a abreviação de "sinal de interrupção", qual é o termo técnico para o sinal gerado por Ctrl + C , portanto, quando você digita Ctrl + C , o shell define pressed_ctrl_c para 1. (SIGINT tem um valor numérico de 2, então você pode dizer trap "pressed_ctrl_c=1" 2 se quiser economizar na digitação.
  • Dentro do loop, temos uma linha de comando entre parênteses. Isso cria um sub-shell.
  • Usamos o comando trap novamente. Desta vez, command é uma string nula; isto diz ao shell para ignorar o número do sinal signum . Como isso está dentro dos parênteses, afeta apenas a subcamada.
  • Executar foo . Desde que foi executado a partir do subshell, foo irá ignorar Ctrl + C , isto é, continuará funcionando mesmo que você o digite.
  • Coloque o subnível em segundo plano ...
  • … e aguarde a conclusão.
  • Se o comando wait for bem-sucedido, vá para a instrução if . Se falhar, execute outro. (Eu vou voltar a isso.)
  • Se $pressed_ctrl_c estiver definido, interrompa o loop. Opcionalmente, remova o comentário do comando echo se Ctrl + C aparece no seu terminal como ^C e você quer passar para a próxima linha.

Nós executamos um comando em segundo plano e imediatamente wait para ele. Isso é muito semelhante a executar o comando em primeiro plano (pelo menos, quando feito em um script). O comando wait terminará com sucesso quando a subshell terminar; isto é, quando o comando foo termina. (O comando wait terminará com sucesso mesmo se foo retornar um erro. Quando o primeiro comando wait termina com sucesso, nós pulamos o segundo e vamos para o if .

O shell que está executando o loop está capturando interrupções mas o subshell e, portanto, o processo foo , estão ignorando-os. Então, quando você digita Ctrl + C , o shell define pressed_ctrl_c para 1 e aborta o comando (primeiro) wait . Como o primeiro comando wait falhou, passamos para o segundo. Lembre-se de que a subpasta foo ainda está em execução então esse wait ainda tem algo a ver (isto é, esperará que o foo termine).

Finalmente, se a variável foi definida para indicar que um Ctrl + C foi pressionado enquanto foo estava sendo executado, interromper a saída do loop.

Se você pressionar Ctrl + C duas vezes, o segundo interromperá e abortará o segundo comando wait . Isso fará com que seu script termine e retorne ao prompt do shell, deixando foo em execução no plano de fundo. Você pode atenuar isso dizendo wait  ||  wait  ||  wait  ||  wait ; estendendo-o até onde você quiser. Você precisaria digitar Ctrl + C uma vez para cada wait para terminar o script prematuramente.

Doh!

Um problema com isso é que os processos são colocados em segundo plano por um script tem sua entrada padrão definida como /dev/null . Se o seu foo for lido no teclado, o texto acima precisará de revisão.

    
por 09.04.2015 / 19:44