빌드노트
cp -n이 0으로 끝나도 대상에는 예전 내용이 남을 수 있다
GNU cp에서 기존 대상을 건너뛰어도 종료 코드 0이 나오는 조건을 실제로 비교했다. 실행 파일·배포 패키지와 대상 내용을 확인하고, 건너뛰기 허용·내용 일치·실패 처리 중 완료 정책을 정한다.
cp -n source target이 종료 코드 0을 반환했는데 target에는 예전 내용이 남아 있다면, 기존 대상을 건너뛴 것인지 확인해야 한다. -n은 기존 파일을 덮어쓰지 않는 옵션이며, 종료 코드 0이 새 내용을 썼다는 증거는 아니다. 이번 GNU cp 실험에서는 실제 복사와 건너뛰기가 모두 0으로 끝났다.
먼저 실패를 의심한 스크립트와 같은 환경에서 실행 파일과 두 파일을 확인한다. 다음은 Bash에서 쓰는 읽기 전용 조회다. src와 dst를 실제 원본·대상 일반 파일 경로로 바꾼다. 디렉터리나 심볼릭 링크를 대상으로 한 복사는 이 예의 범위에서 제외한다.
src='./source'
dst='./target'
type -a cp
cp --version
ls -l -- "$src" "$dst"
cmp -s -- "$src" "$dst"
printf 'cmp status=%s\n' "$?"
type -a cp는 별칭·함수나 여러 실행 파일이 있는지 확인하는 용도다. 버전을 확인한 실행 파일과 실제 복사에 쓰인 것이 같은지 본다. --version은 GNU 방식이므로 BSD 계열에서 지원하지 않으면 그 시스템의 man cp와 패키지 정보를 확인한다.
GNU cmp -s의 상태는 0이면 바이트 내용이 같고, 1이면 다르며, 2이면 파일 누락 같은 비교 오류다. 바로 뒤의 printf가 그 상태를 출력한다. ls에서 대상이 없거나 cmp가 2라면 먼저 경로·읽기 오류를 확인한다. cmp가 1이면 대상이 존재한다는 사실과 별개로 원하는 내용이 들어 있지 않다. 0이어도 이번에 새로 복사됐다는 뜻은 아니다. 원래 같은 내용이었을 수도 있다.
같은 0이었지만 파일 내용은 달랐다
별도 실험은 Ubuntu 24.04.4의 WSL2에서 실행했다. /usr/bin/cp --version은 GNU coreutils 9.4였고, 설치 패키지는 9.4-3ubuntu6.3이었다. 새로 만든 일반 파일만 사용했으며 원본에는 new와 줄바꿈, 기존 대상에는 old와 줄바꿈을 넣었다.
아래 명령은 현재 위치에 새 실험 폴더를 만들어, 기존 대상과 없는 대상에 같은 복사를 실행한다. Bash와 GNU cp, mktemp, cat이 필요하며 현재 폴더에 쓰기 권한이 있어야 한다. 실험 파일은 남는다. 괄호 안의 set +e는 의도한 비정상 종료가 나와도 내용을 확인하기 위한 설정이다.
(
set +e
lab=$(mktemp -d ./cp-check.XXXXXX) || exit 1
cd "$lab" || exit 1
printf 'new\n' > source
printf 'old\n' > target
cp -n -- source target
printf 'existing status=%s content=' "$?"
cat -- target
cp -n -- source fresh
printf 'absent status=%s content=' "$?"
cat -- fresh
)
확인한 Ubuntu 패키지의 표준 출력은 다음과 같았다. 표준 오류에는 각 cp -n 실행마다 -n의 동작이 이식 가능하지 않으며 --update=none을 쓰라는 경고도 나왔다. 경고가 있었어도 이 두 명령의 종료 코드는 0이었다.
existing status=0 content=old
absent status=0 content=new
기존 대상은 그대로였고, 없던 fresh에만 새 내용이 생겼다. 원본과 대상의 바이트·해시를 전후 비교해 기존 대상이 보존된 것도 확인했다. 없는 원본을 복사하는 별도 대조에서는 종료 코드 1과 cannot stat 오류가 나왔으며 대상도 생기지 않았다. 이 환경에서는 건너뛰기와 실제 오류를 구분할 수 있었지만, 0만으로 건너뛰기와 새 복사를 구분할 수는 없었다.
Windows의 Git 배포본에 포함된 GNU coreutils 8.32에서도 새 소유 파일로 같은 조건을 실행했다. 기존 대상은 0과 old, 없는 대상은 0과 new였고, 없는 원본은 1이었다. 이쪽의 정상 두 조건에서는 경고도 없었다. Windows에서의 관찰을 Linux의 모든 버전에 적용하지는 않는다.
GNU끼리도 버전과 배포 패키지를 함께 봐야 한다. GNU 9.2 릴리스 공지 (새 탭에서 열림)는 기존 대상 때문에 건너뛴 cp -n이 비정상 종료 상태를 내도록 변경했다고 설명한다. GNU 9.5 릴리스 공지 (새 탭에서 열림)는 이 변경을 되돌리고, 플랫폼 차이 때문에 -n 사용을 피하도록 안내한다. 따라서 여기서 측정한 Ubuntu 패키지의 0을 상류 GNU 9.4 전체나 BSD cp의 계약으로 확대할 수 없다. BSD 실행은 이번에 확인하지 않았다.
건너뛰기를 허용할지부터 정한다
기존 파일을 그대로 두는 것이 목적이고 건너뛰기도 정상 처리라면, 지원되는 GNU cp에서 --update=none으로 그 정책을 표현할 수 있다. 확인한 Ubuntu 패키지에서는 기존 대상이 있을 때 0과 old, 없을 때 0과 new였다. 이 옵션도 내용 일치를 확인해 주지는 않는다. GNU 8.32에서는 값이 붙은 --update=none 자체가 거부됐다.
반대로 기존 대상 때문에 복사하지 못한 것도 실패여야 한다면, 지원되는 버전의 cp --update=none-fail -- "$src" "$dst"를 검토한다. 위 9.5 공지는 이 옵션의 도입과 기존 대상에 대한 진단·실패 처리를 설명한다. 이번 두 실행 파일은 모두 이 옵션을 거부했다. Ubuntu 쪽은 invalid argument 'none-fail' for '--update'로 끝났으므로, 이 종료 코드 1을 기존 대상을 발견한 정상적인 정책 동작으로 세지 않았다. 지원되는 버전에서의 실행 검증은 남아 있다.
새 파일을 썼는지보다 최종 바이트가 원본과 같은지가 완료 조건일 수도 있다. 다음은 --update=none 지원을 확인한 환경에서 사용할 짧은 스크립트다. 앞에서 정한 src와 dst를 사용하며, 두 파일을 다른 프로세스가 동시에 바꾸지 않는 조건이다. 복사 오류를 먼저 중단하고 내용 비교의 불일치와 오류를 나눈다.
cp --update=none -- "$src" "$dst" || exit "$?"
cmp -s -- "$src" "$dst"
check=$?
case "$check" in
0) printf '%s\n' '내용 일치' ;;
1) printf '%s\n' '내용 불일치' >&2; exit 1 ;;
*) printf '내용 비교 실패: %s\n' "$check" >&2; exit "$check" ;;
esac
이 블록은 cmp의 상태를 직접 처리하므로 set -e를 끈 스크립트에서 실행한다. Ubuntu 실험에서 기존 대상이 old면 복사는 0으로 끝나도 블록 전체는 1로 끝났고, 대상이 없으면 새로 복사한 뒤 0으로 끝났다. 처음부터 같은 내용인 기존 대상도 0이었다. 이 마지막 경우는 건너뛰기를 허용한 결과이며, 새로 썼음을 증명한 결과가 아니다. 권한·소유자·시간 정보의 일치까지 검사하지도 않는다.
이번 검증은 WSL의 Windows 드라이브 연결 파일시스템인 v9fs와 Windows의 새 일반 파일에 한정했다. 동시 변경, 여러 파일을 한 번에 복사한 경우, 다른 파일시스템의 동작은 확인하지 않았다. 실행 파일·패키지 버전과 함께 대상의 존재·내용을 확인한 뒤, 기존 대상 보존만 필요한지, 내용 일치가 필요한지, 건너뛰기를 실패로 처리할지 선택해야 한다.