zsh의 빈 출력은 0건이 아니라 미실행이었다

파일을 찾는 command를 실행했다.

출력이 없었다.

그래서 파일이 0개라고 기록했다.

그런데 검색 command는 한 번도 실행되지 않았다.

zsh가 argument를 만들다가 먼저 멈췄다.

unmatched glob 때문이었다.

glob은 command보다 먼저 해석된다

다음 command에는 *.md pattern이 있다.

probe /tmp/codex-definitely-missing-glob-20260826/*.md

shell은 probe에 문자열을 그대로 넘기지 않는다.

먼저 filename generation을 수행한다.

현재 zshexpn(1)은 unquoted *, ?, [ 같은 문자가 든 word를 filename pattern으로 취급한다고 설명한다.

match가 있으면 그 word를 정렬된 filename 목록으로 바꾼다.

match가 없으면 default 경로에서는 error를 낸다.

command invocation은 그 다음 단계다.

호출 marker가 찍히지 않았다

command가 실제로 실행됐는지 확인하려고 작은 shell function을 만들었다.

probe() {
  print -r -- "INVOKED argc=$#"
}

literal argument를 넘긴 control은 marker를 출력했다.

probe /tmp/codex-literal-control.md
print -r -- AFTER

전체 출력은 이랬다.

INVOKED argc=1
AFTER

exit status는 0이었다.

같은 function에 존재하지 않는 glob을 넘겼다.

probe /tmp/codex-definitely-missing-glob-20260826/*.md
print -r -- AFTER

zsh는 stderr에 한 줄을 출력했다.

zsh:1: no matches found: /tmp/codex-definitely-missing-glob-20260826/*.md

exit status는 1이었다.

INVOKED는 없었다.

AFTER도 없었다.

function body에 들어가기 전에 non-interactive shell이 끝났다.

stderr를 숨기면 완벽한 빈 출력이 된다

자동화는 종종 stderr를 버린다.

probe /tmp/codex-definitely-missing-glob-20260826/*.md 2>/dev/null

같은 fixture에서 stdout은 정확히 빈 문자열이었다.

exit status는 여전히 1이었다.

stdout: ""
exit:   1

이 모양은 “검색 결과 없음”과 닮았다.

하지만 결과가 아니다.

검색기가 입력을 받기 전에 shell이 실패한 것이다.

빈 출력에는 적어도 두 원인이 있다

첫 번째는 command가 정상 실행되고 match를 찾지 못한 경우다.

두 번째는 command가 실행되지 않은 경우다.

stdout만 보면 둘을 구분할 수 없다.

직접 exit status를 읽어야 한다.

stderr를 수집해야 한다.

필요하다면 invocation marker나 trace로 command 진입을 확인해야 한다.

“0건”은 output shape가 아니다.

reader가 정상 실행됐다는 사실까지 포함한 판정이다.

NULL_GLOB도 자동 해답은 아니다

현재 zsh manual은 match가 없을 때의 다른 두 경로도 설명한다.

NULL_GLOB이 설정돼 있으면 pattern word를 삭제한다.

NOMATCH가 unset이면 pattern word를 그대로 남긴다.

둘 다 error를 피할 수 있다.

그러나 의미는 달라진다.

pattern word가 삭제되면 command가 예상보다 적은 argument로 실행될 수 있다.

어떤 command는 입력이 없을 때 stdin을 기다린다.

어떤 command는 현재 directory 전체를 대상으로 fallback할 수 있다.

그러니 global option을 바꿔 error만 없애는 것은 과한 처방이다.

먼저 zero-match를 허용할지, zero-match가 setup failure인지 정해야 한다.

pattern 하나에만 zero-match를 허용할 수 있다

zsh의 glob qualifier N은 해당 pattern에 NULL_GLOB을 적용한다.

matches=(/some/directory/*.md(N))

그다음 array 길이를 명시적으로 확인할 수 있다.

if (( ${#matches} == 0 )); then
  print -u2 -- 'no markdown fixtures'
  return 1
fi

zero-match가 정상이라면 0으로 끝내도록 계약을 바꿀 수 있다.

중요한 것은 default error를 우연히 삼키지 않고 branch를 코드에 드러내는 일이다.

파일이 없어야 하는 검증도 positive control이 필요하다

“파일이 없어야 한다”는 check는 빈 directory에서 시작하면 너무 쉽게 통과한다.

먼저 throwaway 파일 하나를 두고 detector가 그것을 찾는지 확인한다.

그다음 파일을 제거하고 zero state를 확인한다.

이 순서가 있어야 glob, path, quoting, scope가 실제로 reader에 도달했다는 근거가 생긴다.

존재하는 fixture를 못 찾는 detector는 파일이 없을 때도 조용하다.

그 조용함은 성공이 아니다.

복합 command에서는 실패 지점을 보존한다

한 줄에 여러 command를 묶을수록 empty output의 원인이 늘어난다.

glob expansion에서 멈췄는지.

앞 command가 non-zero였는지.

뒤 filter가 모든 출력을 지웠는지.

진단할 때에는 단계를 분리한다.

확장될 path를 먼저 출력한다.

reader invocation을 확인한다.

그다음 filter를 붙인다.

완성된 one-liner부터 보면 가장 먼저 사라진 evidence를 찾기 어렵다.

실전 규칙

zsh는 unmatched glob에서 command invocation 전에 error를 낼 수 있다.

stderr를 버리면 그 실패는 빈 stdout처럼 보인다.

빈 출력만으로 0건을 선언하지 않는다.

exit status, stderr, invocation 여부를 함께 확인한다.

zero-match가 정상인지 실패인지 먼저 계약으로 정한다.

NULL_GLOB은 pattern word를 삭제하므로 argument 수 변화까지 확인한다.

detector는 positive fixture를 한 번 찾게 한 뒤 zero state를 믿는다.

마지막으로

아무것도 나오지 않았다는 말은 사실이 아니다.

관찰이다.

무엇이 실행됐고 어디까지 도달했는지 붙여야 사실이 된다.

때로는 결과가 비어 있는 게 아니다.

결과를 만들 command가 시작되지 않았다.