문제섹션 링크

zsh 스크립트에서 루프를 실행한 뒤 ls가 command not found로 실패하면 명령 설치 상태부터 확인하기 쉽다. 하지만 루프 변수 이름이 path였다면 실행 파일이 없어졌다는 뜻이 아닐 수 있다. path에 값이 대입되면서 명령을 찾는 PATH가 바뀌고, 그 뒤의 ls를 찾지 못하는 경우가 있다.

재현 조건섹션 링크

이 문제는 짧은 새 zsh 프로세스에서 분리해 확인할 수 있다. 파일을 만들거나 지우지 않고 루프 변수만 대입해도 같은 실패가 재현된다. 따라서 설치 여부보다 변수 이름과 명령 검색 경로를 먼저 확인하는 편이 원인을 빠르게 좁힌다.

잘못된 코드섹션 링크

zsh
/bin/zsh -f -c 'for path in demo; do :; done; ls'

긴 코드는 좌우로 밀어 전체를 볼 수 있습니다.

이 예제의 콜론은 아무 작업도 하지 않는 셸 내장 명령이다. 루프 본문은 파일을 바꾸거나 다른 프로그램을 실행하지 않는다. 그런데도 루프 뒤의 ls는 command not found가 된다. 이 최소 예제는 원인을 루프 변수 대입으로 한정한다.

원인섹션 링크

zsh의 소문자 path는 일반 문자열 변수가 아니라 대문자 PATH와 연결된 특수 배열이다. 배열 항목은 명령을 찾을 디렉터리에 대응하므로 path를 바꾸면 PATH도 바뀐다. 이 연결은 zsh 공식 매개변수 문서에 정의되어 있다.

루프는 반복할 때마다 대상 변수에 값을 대입한다. path에 demo를 넣으면 명령 검색 경로도 demo로 바뀐다. 실행 파일은 그대로여도 셸이 찾는 위치가 달라져 ls 같은 외부 명령을 발견하지 못한다. 대소문자만 다르면 별개 변수라고 생각하기 쉬운 지점이다.

수정한 코드섹션 링크

zsh
/bin/zsh -f -c 'original_path=$PATH; for item_path in demo; do :; done; [[ $PATH == $original_path ]] && command -v ls && print -r -- PASS'

긴 코드는 좌우로 밀어 전체를 볼 수 있습니다.

순회 항목에는 item_path처럼 path와 다른 이름을 쓴다. original_path에는 시작 시점의 PATH를 저장하고, 루프 뒤에도 값이 같은지 비교한다. ls가 실행되는지만 보지 않고 검색 경로가 보존되는지 함께 확인하면 수정 목적을 직접 검사할 수 있다.

검증섹션 링크

새 zsh 프로세스에서 실패 예제의 ls는 command not found와 종료 코드 127을 냈다. 수정 예제는 PATH 보존 비교, command -v ls, PASS 출력을 모두 통과했고 종료 코드는 0이었다.

두 예제는 모두 -f 옵션으로 사용자 설정 파일의 영향을 줄인다. 실패와 성공을 각각 새 프로세스에서 실행하므로 바뀐 검색 경로가 다음 검증에 섞이지 않는다. 이 결과는 이 최소 예제의 동작을 확인한 것이며, 다른 셸 설정까지 같은 결과가 된다는 뜻은 아니다.

예방섹션 링크

zsh 반복문에서는 item_path, source_file처럼 용도를 드러내고 path와 겹치지 않는 이름을 쓴다. 검사 규칙으로 zsh 파일의 for path 또는 path= 대입을 찾고, 의도적으로 특수 배열을 바꾸는 경우만 명시적으로 예외 처리할 수 있다. 이 규칙은 문법 전체를 해석하지 않으므로 문자열 안의 일치는 별도로 확인한다.

회귀 검증에는 루프 전후의 PATH 비교와 필요한 명령의 command -v 확인을 넣는다. 명령 설치 여부만 확인하는 것보다 변수 대입이 셸의 실행 환경을 바꾸지 않는지 검사하는 편이 같은 원인의 재발을 막는다.