Removendo arquivos temporários criados em saída inesperada do bash

89

Estou criando arquivos temporários de um script bash. Estou excluindo-os no final do processamento, mas como o script está em execução há um bom tempo, se eu encerrá-lo ou simplesmente CTRL-C durante a execução, os arquivos temporários não serão excluídos.
Existe uma maneira de capturar esses eventos e limpar os arquivos antes que a execução termine?

Além disso, existe algum tipo de prática recomendada para a nomenclatura e a localização desses arquivos temporários?
No momento, não tenho certeza entre usar:

TMP1=`mktemp -p /tmp`
TMP2=`mktemp -p /tmp`
...

e

TMP1=/tmp/`basename $0`1.$$
TMP2=/tmp/`basename $0`2.$$
...

Ou talvez existam soluções melhores?

skinp
fonte

Respostas:

97

Você pode definir uma " armadilha " para executar na saída ou em um control-c para limpar.

trap "{ rm -f $LOCKFILE; }" EXIT

Como alternativa, um dos meus unix-isms favoritos é abrir um arquivo e excluí-lo enquanto ainda estiver aberto. O arquivo permanece no sistema de arquivos e você pode lê-lo e gravá-lo, mas assim que o programa termina, o arquivo desaparece. Não tenho certeza de como você faria isso no bash, no entanto.

BTW: Um argumento a favor do mktemp em vez de usar sua própria solução: se o usuário antecipar que seu programa criará arquivos temporários enormes, ele pode querer definir TMPDIRpara algum lugar maior, como / var / tmp. mktemp reconhece isso, sua solução enrolada à mão (segunda opção) não. Eu uso freqüentemente TMPDIR=/var/tmp gvim -d foo bar, por exemplo.

Paul Tomblin
fonte
8
Com Bash, exec 5<>$TMPFILEdescritor de arquivo laços 5 a US $ tmpfile como leitura e escrita, e você pode usar <&5, >&5e /proc/$$/fd/5(Linux) depois disso. O único problema é que o Bash não tem seekfunção ...
efêmero
Aceito sua resposta, pois o link que você forneceu é o que melhor explica o que eu precisava. Obrigado
skinp
4
Algumas observações sobre trap: não há como interceptar SIGKILL(por design, uma vez que termina imediatamente a execução). Portanto, se isso acontecer, tenha um plano alternativo (como tmpreaper). Em segundo lugar, as armadilhas não são cumulativas - se você tiver mais de uma ação a realizar, todas elas devem estar no trapcomando. Uma maneira de lidar com várias ações de limpeza é definir uma função (e você pode redefini-la como seu programa procede, se necessário) e referência que: trap cleanup_function EXIT.
Toby Speight
1
Tive de usar trap "rm -f $LOCKFILE" EXITou receberia um erro inesperado de fim de arquivo.
Jaakko de
3
Shellcheck alertou para o uso de aspas simples, de que a expressão seria expandida 'agora' com aspas duplas, em vez de mais tarde quando a armadilha fosse invocada.
LaFayette,
110

Normalmente crio um diretório no qual coloco todos os meus arquivos temporários e, imediatamente depois, crio um manipulador EXIT para limpar esse diretório quando o script for encerrado.

MYTMPDIR=$(mktemp -d)
trap "rm -rf $MYTMPDIR" EXIT

Se você colocar todos os seus arquivos temporários em $MYTMPDIR, todos eles serão excluídos quando o script for encerrado na maioria das circunstâncias. Eliminar um processo com SIGKILL (kill -9) elimina o processo imediatamente, portanto, seu manipulador EXIT não será executado nesse caso.

Chris AtLee
fonte
27
+1 Definitivamente use uma armadilha em SAIR, não é bobo TERM / INT / HUP / qualquer outra coisa que você possa imaginar. Porém, lembre-se de citar suas expansões de parâmetro e eu também recomendo que você
coloque
7
Aspas simples, porque então sua armadilha ainda funcionará se mais tarde em seu script você decidir limpar e alterar TMPDIR por causa das circunstâncias.
lhunath
1
@AaronDigulla Por que $ () vs crases é importante?
Ogre Salm33
3
@ OgrePsalm33: stackoverflow.com/questions/4708549/…
Aaron Digulla
3
O código @AlexanderTorstling deve sempre estar entre aspas simples para evitar a injeção que resulte na execução arbitrária do código. Se você expandir os dados em um código bash STRING, esses dados agora podem fazer qualquer coisa que o código faça que resulte em bugs inocentes no espaço em branco, mas também bugs destrutivos, como limpar seu homedir por motivos bizarros ou introduzir brechas de segurança. Observe que o trap pega uma string de código bash que será avaliada como está mais tarde. Portanto, mais tarde, quando a armadilha for acionada, as aspas simples desaparecerão e haverá apenas as aspas duplas sintáticas.
lhunath
25

Você deseja usar o comando trap para lidar com a saída do script ou sinais como CTRL-C. Consulte o Wiki de Greg para obter detalhes.

Para seus arquivos temporários, basename $0é uma boa ideia usar , além de fornecer um modelo que forneça espaço para arquivos temporários suficientes:

tempfile() {
    tempprefix=$(basename "$0")
    mktemp /tmp/${tempprefix}.XXXXXX
}

TMP1=$(tempfile)
TMP2=$(tempfile)

trap 'rm -f $TMP1 $TMP2' EXIT
Brian Campbell
fonte
1
Não intercepte em TERM / INT. Armadilha na SAÍDA. Tentar prever a condição de saída com base nos sinais recebidos é bobagem e definitivamente não é uma coisa complicada.
lhunath
3
Ponto secundário: Use $ () em vez de crases simples. E coloque aspas duplas em torno de $ 0 porque pode conter espaços.
Aaron Digulla
Bem, os backticks funcionam bem neste comentário, mas isso é um ponto justo, é bom ter o hábito de usar $(). Adicionadas as aspas duplas também.
Brian Campbell
1
Você pode substituir toda a sua sub-rotina por apenas TMP1 = $ (tempfile -s "XXXXXX")
Ruslan Kabalin
4
@RuslanKabalin Nem todos os sistemas têm um tempfilecomando, enquanto todos os sistemas modernos razoáveis ​​que conheço têm um mktempcomando.
Brian Campbell
9

Basta ter em mente que a resposta escolhida é bashism, o que significa solução como

trap "{ rm -f $LOCKFILE }" EXIT

funcionaria apenas no bash (não irá capturar Ctrl + C se o shell for dashou clássico sh), mas se você quiser compatibilidade, então ainda precisa enumerar todos os sinais que deseja capturar.

Também tenha em mente que, quando o script sai, a armadilha do sinal "0" (também conhecida como EXIT) é sempre executada, resultando em dupla execução do trapcomando.

Essa é a razão para não empilhar todos os sinais em uma linha se houver sinal EXIT.

Para melhor entendê-lo, observe o seguinte script que funcionará em diferentes sistemas sem alterações:

#!/bin/sh

on_exit() {
  echo 'Cleaning up...(remove tmp files, etc)'
}

on_preExit() {
  echo
  echo 'Exiting...' # Runs just before actual exit,
                    # shell will execute EXIT(0) after finishing this function
                    # that we hook also in on_exit function
  exit 2
}


trap on_exit EXIT                           # EXIT = 0
trap on_preExit HUP INT QUIT TERM STOP PWR  # 1 2 3 15 30


sleep 3 # some actual code...

exit 

Esta solução lhe dará mais controle, pois você pode executar parte do seu código na ocorrência do sinal real antes da saída final ( preExitfunção) e, se necessário, pode executar algum código no sinal EXIT real (estágio final de saída)

Alex
fonte
4

A alternativa de usar um nome de arquivo previsível com $$ é uma brecha de segurança e você nunca, jamais, deve pensar em usá-lo. Mesmo que seja apenas um simples script pessoal em seu PC de usuário único. É um péssimo hábito que você não deve adquirir. BugTraq está cheio de incidentes de "arquivo temporário inseguro". Consulte aqui , aqui e aqui para obter mais informações sobre o aspecto de segurança de arquivos temporários.

Eu estava inicialmente pensando em citar as atribuições inseguras de TMP1 e TMP2, mas pensando bem, provavelmente não seria uma boa ideia .

Hlovdal
fonte
Eu daria se pudesse: +1 para o conselho de segurança e outro +1 para não citar uma má ideia e a referência
TMG
1

Eu prefiro usar o tempfileque cria um arquivo em / tmp de maneira segura e você não precisa se preocupar com sua nomenclatura:

tmp=$(tempfile -s "your_sufix")
trap "rm -f '$tmp'" exit
Ruslan Kabalin
fonte
Infelizmente, o arquivo temporário é muito difícil de ser transportado, embora seja mais seguro, por isso é melhor evitá-lo ou pelo menos emulá-lo.
lericson
1

Não posso acreditar que tantas pessoas presumam que um nome de arquivo não conterá um espaço. O mundo irá travar se $ TMPDIR for atribuído ao "diretório temporário".

zTemp=$(mktemp --tmpdir "$(basename "$0")-XXX.ps")
trap "rm -f ${zTemp@Q}" EXIT

Espaços e outros caracteres especiais como aspas simples e retornos de carro em nomes de arquivo devem ser considerados no código como um requisito para um hábito de programação decente.

Paulo
fonte
+1 Embora aspas simples tratem trap 'rm -f "${zTemp}"' EXITcorretamente de espaços e outros caracteres especiais, a solução desta resposta não adia a avaliação de zTemp. Portanto, não há necessidade de se preocupar com o valor de zTempser alterado posteriormente no script. Além disso, zTemppode ser declarado local para uma função; não precisa ser uma variável de script global.
Robin A. Meade
As aspas duplas em torno do RHS da atribuição são desnecessárias.
Robin A. Meade
Deve-se notar que as ${parameter@operator}expansões foram adicionadas no Bash 4.4 (lançado em setembro de 2016).
Robin A. Meade
-4

Você não precisa se preocupar em remover os arquivos tmp criados com mktemp. Eles serão excluídos de qualquer maneira mais tarde.

Use mktemp se puder, pois ele gera mais arquivos exclusivos do que o prefixo '$$'. E parece uma forma mais multiplataforma de criar arquivos temporários e, em seguida, colocá-los explicitamente em / tmp.

Mykola Golubyev
fonte
4
Excluído por quem ou o quê?
innaM
Excluído pela própria operação | sistema de arquivos após algum tempo
Mykola Golubyev
4
Magia? Um cronjob? Ou uma máquina Solaris reiniciada?
innaM
Provavelmente um deles. Se o arquivo temporário não foi removido por alguma interrupção (não será muito frequente), algum dia os arquivos tmp serão removidos - é por isso que eles chamaram o temp.
Mykola Golubyev
22
Você não pode, não deve, não deve presumir que algo colocado em / tmp permanecerá lá para sempre; ao mesmo tempo, você não deve presumir que ele irá desaparecer magicamente.
innaM