It failed, but the exit code was 0
Make the exit code tell the truth
한국어 원문으로 표시합니다.
목표
셸 한 줄짜리 검사를 "운영 도구" 로 만든다 — 종료 코드 0·1·2 약속, 표준 출력과 표준 오류의 분리, 로그 수준, JSON 출력, 임포트 가능한 main().
왜 중요한가
cron 과 CI 는 화면을 읽지 않고 종료 코드만 본다. 진단 메시지가 표준 출력에 섞이면 파이프라인의 다음 단계가 깨진다. 트레이스백은 만든 사람에게는 정보지만 새벽에 알림을 받은 사람에게는 소음이다. 이 실습의 도구는 작지만, 여기서 익힌 모양이 그대로 수백 줄짜리 운영 도구의 뼈대가 된다. 표준 라이브러리(argparse·logging·json·pathlib)만 쓴다.
단계
/root/pyops/cli/dircheck.py를 만든다. 위치 인자path, 옵션--min-files(int, 기본 1)·--max-age-hours(float, 기본 24)·--json·-v/--verbose를 argparse 로 정의한다.python3 dircheck.py --help가 종료 코드 0 으로 끝나야 한다.- 디렉터리 안의 파일 수와 가장 오래된 파일의 나이(시간)를 재서
files=<n> oldest_age_hours=<x> ok=<true|false>한 줄을 표준 출력에 낸다. 조건은 파일 수 ≥--min-files이고 가장 오래된 파일의 나이 ≤--max-age-hours다. - 조건을 만족하면 종료 코드 0, 위반이면 1 로 끝낸다. 빈 디렉터리는
files=0이고--min-files 1이면 위반이다. - 경로가 없거나 디렉터리가 아니면 트레이스백 없이 표준 오류에 한 줄을 적고 종료 코드 2 로 끝낸다. 이때 표준 출력에는 아무것도 쓰지 않는다.
- logging 을 붙인다. 기본은 INFO,
-v면 DEBUG.-v로 돌리면DEBUG가 포함된 줄이 표준 오류에만 나오고 표준 출력에는 결과 한 줄만 남아야 한다. --json이면 표준 출력에 JSON 객체 한 개만 낸다. 키는path·files·oldest_age_hours·ok이고ok는 불리언,files는 정수다. 종료 코드 약속은 그대로다.- 경로를 여러 개 받는다(
nargs="+"). 디렉터리마다 결과 한 줄(또는 JSON 이면 배열 하나)을 내고, 종료 코드는 가장 나쁜 것(2 > 1 > 0)으로 끝낸다. main(argv=None) -> int함수로 정리하고 파일 맨 아래에서만sys.exit(main())을 부른다.import dircheck만으로는 아무것도 실행되지 않아야 하고,dircheck.main(["<디렉터리>"])가 정수를 돌려줘야 한다.
참고
- 나이는
time.time() - path.stat().st_mtime을 3600 으로 나눈 값이다. 소수 셋째 자리까지면 충분하다. - 시험용 디렉터리는 직접 만든다:
mkdir -p /root/pyops/cli/samples/ok /root/pyops/cli/samples/empty && touch /root/pyops/cli/samples/ok/a.txt - 흔한 실수: 진단을
print()로 찍어 표준 출력을 더럽히는 것, 예외를 잡아 놓고 0 으로 끝내는 것,parse_args()를main()밖(모듈 최상단)에서 부르는 것.
인자 정의와 --help
/root/pyops/cli/dircheck.py 를 만들고 path·--min-files·--max-age-hours·--json·-v 를 argparse 로 정의한다. --help 가 종료 코드 0 으로 끝난다.
argparse.ArgumentParser() 에 add_argument 를 다섯 번 부르면 됩니다. --help 는 argparse 가 자동으로 만들고 종료 코드 0 으로 끝냅니다. type=int, type=float, action="store_true" 를 구분하세요.
파일 수와 가장 오래된 나이를 잰다
디렉터리의 파일 수와 가장 오래된 파일의 나이(시간)를 재서 files=<n> oldest_age_hours=<x> ok=<true|false> 한 줄을 표준 출력에 낸다.
Path(path).iterdir() 에서 is_file() 인 것만 세고, 나이는 (time.time() - p.stat().st_mtime) / 3600 입니다. 빈 디렉터리의 max() 는 default=0.0 을 주세요.
0 과 1 의 약속
조건을 만족하면 종료 코드 0, 위반(파일 부족 또는 너무 오래됨)이면 1 로 끝낸다.
main() 이 정수를 돌려주고 sys.exit(main()) 으로 넘기세요. ok 가 False 면 1 입니다. 빈 디렉터리에 --min-files 1 이면 위반입니다.
도구 오류는 2, 트레이스백은 없이
없는 경로나 디렉터리가 아닌 경로면 표준 오류에 한 줄을 적고 종료 코드 2 로 끝낸다. 표준 출력은 비어 있어야 하고 트레이스백이 나오면 안 된다.
Path.is_dir() 로 먼저 거르고, stat() 이 던지는 OSError 를 잡아 메시지 한 줄로 바꾸세요. 메시지는 print(..., file=sys.stderr) 나 logging 으로 냅니다.
진단은 표준 오류로, -v 로 수준 조절
logging.basicConfig 로 기본 INFO, -v 면 DEBUG 로 설정한다. -v 로 돌리면 DEBUG 줄이 표준 오류에만 나오고 표준 출력에는 결과 한 줄만 남는다.
basicConfig 의 기본 목적지는 sys.stderr 입니다. format 에 %(levelname)s 를 넣으면 DEBUG 라는 낱말이 찍힙니다. log.debug() 를 검사 시작 지점에 하나 두세요.
기계가 읽을 결과는 JSON 으로
--json 이면 표준 출력에 JSON 객체 하나만 낸다. 키는 path·files·oldest_age_hours·ok 이고 ok 는 불리언, files 는 정수다. 종료 코드 약속은 유지한다.
json.dumps(dict) 를 print 하세요. 사람용 한 줄과 JSON 을 동시에 내면 파싱이 깨집니다 — 둘 중 하나만.
여러 디렉터리, 가장 나쁜 코드
path 를 nargs="+" 로 여러 개 받는다. 디렉터리마다 한 줄씩(JSON 이면 배열) 내고 종료 코드는 가장 나쁜 것(2 > 1 > 0)으로 끝낸다.
worst = max(worst, code) 를 디렉터리마다 갱신하고, 없는 경로 하나 때문에 나머지 검사를 건너뛰지 마세요(continue).
임포트해도 실행되지 않는 도구
main(argv=None) -> int 로 정리하고 파일 맨 아래 if __name__ == "__main__": 안에서만 sys.exit(main()) 을 부른다. import dircheck 가 아무것도 실행하지 않고, dircheck.main(["<디렉터리>"]) 가 정수를 돌려준다.
parse_args(argv) 에 argv 를 넘기면 sys.argv 를 안 읽습니다. 모듈 최상단에 parse_args() 나 sys.exit() 이 남아 있으면 임포트 순간 실행됩니다.