It failed, but the exit code was 0
Make the exit code tell the truth
Goal
Turn a one-line shell check into an "operations tool": the exit code 0, 1, and 2 promise, separation of standard output and standard error, log levels, JSON output, and an importable main().
Why it matters
cron and CI do not read the screen; they look only at the exit code. If diagnostic messages get mixed into standard output, the next step of the pipeline breaks. A traceback is information for the person who built the tool but noise for the person alerted at dawn. The tool in this lab is small, but the shape you learn here becomes the skeleton of an operations tool hundreds of lines long. You use only the standard library (argparse, logging, json, pathlib).
Steps
- Create
/root/pyops/cli/dircheck.py. With argparse, define the positional argumentpathand the options--min-files(int, default 1),--max-age-hours(float, default 24),--json, and-v/--verbose.python3 dircheck.py --helpmust end with exit code 0. - Measure the number of files in the directory and the age (in hours) of the oldest file, then print one line,
files=<n> oldest_age_hours=<x> ok=<true|false>, to standard output. The condition is that the file count is at least--min-filesand the age of the oldest file is at most--max-age-hours. - End with exit code 0 if the condition holds and 1 if it is violated. An empty directory has
files=0, which is a violation when--min-files 1applies. - If the path does not exist or is not a directory, write one line to standard error without a traceback and end with exit code 2. Write nothing to standard output in this case.
- Add logging. The default level is INFO, and
-vmakes it DEBUG. When you run with-v, lines containingDEBUGmust appear only on standard error, and standard output must contain only the one result line. - With
--json, print exactly one JSON object to standard output. The keys arepath,files,oldest_age_hours, andok;okis a boolean andfilesis an integer. The exit code promise stays the same. - Accept several paths (
nargs="+"). Print one result line per directory (or, with JSON, a single array), and end with the worst exit code (2 > 1 > 0). - Refactor into a
main(argv=None) -> intfunction and callsys.exit(main())only at the very bottom of the file.import dircheckalone must run nothing, anddircheck.main(["<디렉터리>"])must return an integer.
Notes
- The age is
time.time() - path.stat().st_mtimedivided by 3600. Three decimal places are enough. - Create the test directories yourself:
mkdir -p /root/pyops/cli/samples/ok /root/pyops/cli/samples/empty && touch /root/pyops/cli/samples/ok/a.txt - Common mistakes: printing diagnostics with
print()and dirtying standard output, catching an exception and still ending with 0, and callingparse_args()outsidemain()(at module top level).
Define the arguments and --help
Create /root/pyops/cli/dircheck.py and define path, --min-files, --max-age-hours, --json, and -v with argparse. --help ends with exit code 0.
Call add_argument on argparse.ArgumentParser() five times. argparse generates --help automatically and ends with exit code 0. Keep type=int, type=float, and action="store_true" straight.
Measure the file count and the oldest age
Measure the number of files in the directory and the age (in hours) of the oldest file, then print one line, files=<n> oldest_age_hours=<x> ok=<true|false>, to standard output.
Count only the entries from Path(path).iterdir() for which is_file() is true, and compute the age as (time.time() - p.stat().st_mtime) / 3600. For max() on an empty directory, pass default=0.0.
The promise of 0 and 1
End with exit code 0 if the condition holds, and 1 on a violation (too few files or too old).
Have main() return an integer and hand it over with sys.exit(main()). If ok is False, the code is 1. An empty directory with --min-files 1 is a violation.
Tool errors are 2, with no traceback
If the path does not exist or is not a directory, write one line to standard error and end with exit code 2. Standard output must be empty and no traceback may appear.
Filter with Path.is_dir() first, then catch the OSError that stat() raises and turn it into a one-line message. Emit the message with print(..., file=sys.stderr) or with logging.
Diagnostics go to standard error, and -v sets the level
Configure logging.basicConfig with INFO by default and DEBUG for -v. When you run with -v, the DEBUG lines appear only on standard error, and standard output keeps only the one result line.
The default destination of basicConfig is sys.stderr. If you put %(levelname)s in format, the word DEBUG is printed. Place one log.debug() at the point where the check starts.
Machine-readable results as JSON
With --json, print exactly one JSON object to standard output. The keys are path, files, oldest_age_hours, and ok; ok is a boolean and files is an integer. Keep the exit code promise.
print the result of json.dumps(dict). If you emit both the human-readable line and the JSON, parsing breaks, so emit only one of them.
Several directories, the worst code
Accept several paths with nargs="+" for path. Print one line per directory (an array for JSON) and end with the worst exit code (2 > 1 > 0).
Update worst = max(worst, code) for each directory, and do not skip the remaining checks because of one missing path (continue).
A tool that does not run when imported
Refactor into main(argv=None) -> int and call sys.exit(main()) only inside if __name__ == "__main__": at the bottom of the file. import dircheck runs nothing, and dircheck.main([""]) returns an integer.
If you pass argv to parse_args(argv), it does not read sys.argv. If parse_args() or sys.exit() is left at module top level, it runs at the moment of import.