Linux 강좌 / Bash 오류 처리와 안전한 파일 작업하기
안전한 파일 스크립트는 입력과 목적지를 먼저 확인하고, 작업 실패를 명시적으로 처리하며, 실패한 중간 결과를 정상 결과처럼 남기지 않아야 합니다. set -e 하나에 의존하기보다 각 명령의 실패가 어떤 의미인지 판단하고 임시 파일 정리와 덮어쓰기 조건을 설계하세요.
이 글은 첫 Bash 스크립트를 바탕으로 실패 분기와 파일 작업의 한계를 다룹니다. &&·||·파이프라인의 기본 실행 규칙은 셸 실행 흐름에서 먼저 확인하세요.
실습에서 보장할 범위 정하기
다음 예제는 본인만 쓰는 홈 디렉터리 아래에서 일반 파일 하나를 새 이름으로 복사하고 내용이 같은지 확인합니다. 목적지 디렉터리는 이미 있어야 하며 다른 사용자가 수정할 수 없는 신뢰할 수 있는 위치여야 합니다. 공격자가 경로를 동시에 바꾸는 환경을 방어하는 보안 도구나 데이터베이스 백업 도구는 아닙니다. 관리자 권한으로 실행하지 않습니다.
검사·임시 복사·게시를 나눈 전체 예제
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"
일반 파일을 임시 디렉터리로 복사하고 비교한 뒤 ln으로 새 목적지 이름을 만듭니다. 임시 파일을 목적지와 같은 파일 시스템에 만들었으므로 하드 링크를 사용할 수 있으며, 이미 이름이 존재하면 ln이 실패해 덮어쓰지 않습니다. EXIT 정리는 임시 이름을 지우지만 목적지 링크는 남깁니다. 하드 링크를 지원하지 않는 파일 시스템에서는 마지막 단계가 실패하므로 강제로 다른 방식으로 우회하지 말고 요구 조건을 다시 검토합니다.
이 예제는 내용 복사를 검증하지만 소유권·ACL·확장 속성 보존이나 쓰기 완료의 물리적 내구성까지 보장하지 않습니다. 복사 중 원본이 계속 수정되는 작업에도 적합하지 않습니다. rm -rf는 mktemp가 성공해 만든 작업 경로 하나만 대상으로 하며 입력받은 원본·목적지 경로에는 사용하지 않습니다.
실패 조건을 실제로 바꾸어 시험하기
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
첫 재실행은 목적지가 이미 있으므로 상태 1로 끝나고 기존 내용을 보존해야 합니다. 없는 원본도 실패해야 하며, 정상 종료·일반 실패 뒤에는 마지막 find에 임시 작업 경로가 나오지 않아야 합니다. SIGKILL이나 전원 손실에서는 trap이 실행되지 않으므로 남은 파일의 별도 정리 정책이 필요합니다. 실패 실험 후 원본과 최초 복사본이 같은지 cmp로 다시 확인합니다.
set -e와 pipefail을 과신하지 않기
bash -c 'false | cat; printf "status=%s\n" "$?"'
bash -o pipefail -c 'false | cat; printf "status=%s\n" "$?"'
첫 명령은 파이프의 마지막 cat이 성공해 보통 status=0, 둘째는 pipefail 때문에 status=1을 출력합니다. pipefail은 실패를 상태에 반영하는 옵션이지 자동 복구 기능은 아닙니다. set -e는 if 검사, 조건 목록, 파이프 등 문맥에 따라 적용 예외가 있으므로 예상한 위치에서 중단되는지 따로 시험해야 합니다.
if ! command; then은 실패 분기를 쓰기 편하지만 그 안의 $?는 부정 연산 뒤 상태이므로 원래 종료 코드를 그대로 보존하지 않습니다. 원래 코드를 남겨야 하면 if command; then ...; else rc=$?; ...; fi처럼 실패 직후 저장하세요. 스크립트가 반환하는 코드와 오류 메시지도 호출자가 구별할 수 있게 설계합니다.
운영 자동화에 적용하기 전 확인할 것
- 변수 확장은 따옴표로 보호하고 파일명 인수 앞의 -- 지원 여부를 확인합니다.
- 미리보기 모드와 실제 변경 모드를 분리하고 미리보기에서 데이터가 바뀌지 않게 합니다.
- 경로의 소유권·권한, 심볼릭 링크, 동시 실행, 원본 변경 가능성을 검토합니다.
- 성공·실패·신호 종료를 따로 시험하고 복구 가능한 백업을 남깁니다.




