Tutorial de Linux / Trate erros no Bash e copie arquivos com segurança

Um script que trabalha com arquivos de forma mais segura valida a origem e o destino, trata falhas explicitamente e evita apresentar um resultado parcial como concluído. Em vez de depender apenas de set -e, defina o significado de cada falha, a limpeza dos arquivos temporários e as condições de sobrescrita.

Esta aula amplia o primeiro script Bash com ramificações de erro e limites de operações em arquivos. Para revisar &&, || e pipelines, consulte também o fluxo de execução do shell.

Defina o alcance de segurança do exercício

O exemplo copia um arquivo regular para um nome novo em um diretório confiável sob seu controle e compara os conteúdos. O diretório de destino deve existir e não pode ser modificável por usuários não confiáveis. Não execute o exercício como administrador. O programa não é uma barreira contra alterações hostis e simultâneas nos caminhos nem uma ferramenta de backup de banco de dados.

Crie o script completo de cópia verificada

Na mesma sessão Bash, execute as linhas de preparação em ordem e pare se alguma falhar. Confirme que mktemp criou uma pasta nova em seu diretório pessoal e que cd entrou nela antes de criar arquivos. O bloco grava checked-copy.sh nessa pasta; a linha final EOF deve ficar sozinha.

lab=$(mktemp -d "$HOME/linux-safe-copy.XXXXXX")
cd "$lab"
mkdir output
printf "important sample\n" > "source file.txt"
cat > checked-copy.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
if (( $# != 2 )); then
  printf "Usage: %s SOURCE DESTINATION\n" "$0" >&2
  exit 2
fi
source_file=$1
destination=$2
if [[ ! -f "$source_file" || -L "$source_file" ]]; then
  printf "Source must be a regular non-symlink file\n" >&2
  exit 1
fi
if [[ -e "$destination" || -L "$destination" ]]; then
  printf "Destination already exists\n" >&2
  exit 1
fi
parent=$(dirname -- "$destination")
if [[ ! -d "$parent" ]]; then
  printf "Destination directory is missing\n" >&2
  exit 1
fi
work=$(mktemp -d "$parent/.copy-work.XXXXXX") || exit 1
cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ! cp -- "$source_file" "$work/payload"; then
  printf "Copy failed\n" >&2
  exit 1
fi
if ! cmp -s -- "$source_file" "$work/payload"; then
  printf "Verification failed\n" >&2
  exit 1
fi
if ! ln -- "$work/payload" "$destination"; then
  printf "Destination creation failed\n" >&2
  exit 1
fi
printf "Created: %s\n" "$destination"
EOF
bash -n checked-copy.sh
bash checked-copy.sh "source file.txt" "output/copy file.txt"
cmp "source file.txt" "output/copy file.txt"

O programa usa set -u para rejeitar variáveis não definidas e umask 077 para restringir permissões de arquivos novos conforme o ambiente. Ele exige dois argumentos, rejeita origem que não seja arquivo regular ou que seja link simbólico e verifica se o destino já existe, inclusive quando é um link simbólico. Confirme que a execução exibe Created: e que cmp termina sem saída e com estado 0.

Após criar um diretório temporário no diretório de destino, o script copia e compara o conteúdo. Então ln cria o nome de destino como link físico para o arquivo verificado. A limpeza em EXIT remove o nome temporário, mantendo o link de destino. A criação de links físicos requer suporte do sistema de arquivos; se falhar, reveja os requisitos em vez de contornar a falha automaticamente.

O teste inicial e a criação não formam uma operação segura contra concorrentes hostis. Por exemplo, se outro processo alterar o destino entre a verificação e ln, o comportamento pode mudar; um diretório criado nesse intervalo pode ser tratado por ln como diretório de destino. Por isso, use somente o ambiente privado e confiável descrito acima e não considere este exemplo proteção contra corrida de caminhos.

A comparação verifica conteúdo, não garante preservação de proprietário, listas de controle de acesso (ACL), atributos estendidos nem durabilidade física após falta de energia. Também não oferece uma cópia consistente de uma origem que muda continuamente. A limpeza recursiva rm -rf atua apenas na pasta de trabalho criada com sucesso por mktemp, não nos caminhos de origem ou destino fornecidos.

Teste as falhas esperadas

bash checked-copy.sh "source file.txt" "output/copy file.txt"
printf "exit=%s\n" "$?"
bash checked-copy.sh missing.txt output/new.txt
printf "exit=%s\n" "$?"
find output -maxdepth 1 -name ".copy-work.*" -print

A primeira repetição deve retornar estado 1 porque o destino já existe, mantendo seu conteúdo. A origem inexistente também deve falhar; após o encerramento normal ou falhas tratadas, o último find não deve listar pastas temporárias de trabalho. Compare novamente a origem com a primeira cópia usando cmp. Um sinal SIGKILL ou uma falta de energia impede a execução de trap; dados abandonados exigem uma política de limpeza separada.

Entenda os limites de errexit e pipefail

bash -c 'false | cat; printf "status=%s\n" "$?"'
bash -o pipefail -c 'false | cat; printf "status=%s\n" "$?"'

O primeiro comando normalmente imprime status=0, pois o último comando, cat, tem sucesso; o segundo imprime status=1 porque pipefail expõe a falha anterior. A opção altera o estado retornado pelo pipeline, mas não recupera o trabalho. set -e tem exceções que dependem do contexto, inclusive em condições e pipelines: teste exatamente onde espera que o script pare.

Em if ! command; then, o estado $? dentro do ramo representa o resultado negado, e não preserva o código original. Se precisar dele, capture-o imediatamente no ramo else, como em if command; then ...; else rc=$?; ...; fi. Defina mensagens e códigos que permitam ao programa chamador distinguir os resultados.

Confira as premissas antes de automatizar

  • Proteja expansões de variáveis com aspas e use -- antes de nomes de arquivo quando o comando aceitar essa opção.
  • Separe a prévia da operação que altera dados e confirme que a prévia não os modifica.
  • Revise propriedade, permissões, links simbólicos, concorrência e possíveis alterações da origem.
  • Teste sucesso, falhas e sinais tratados; mantenha backups recuperáveis.

Referências oficiais