TT Lab
Get started
Learn Learning paths Courses

It failed, but the exit code was 0

Make the exit code tell the truth

Continue in TT Lab

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

  1. Create /root/pyops/cli/dircheck.py. With argparse, define the positional argument path and the options --min-files (int, default 1), --max-age-hours (float, default 24), --json, and -v/--verbose. python3 dircheck.py --help must end with exit code 0.
  2. 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-files and the age of the oldest file is at most --max-age-hours.
  3. 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 1 applies.
  4. 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.
  5. Add logging. The default level is INFO, and -v makes it DEBUG. When you run with -v, lines containing DEBUG must appear only on standard error, and standard output must contain only the one result line.
  6. 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. The exit code promise stays the same.
  7. 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).
  8. Refactor into a main(argv=None) -> int function and call sys.exit(main()) only at the very bottom of the file. import dircheck alone must run nothing, and dircheck.main(["<디렉터리>"]) must return an integer.

Notes

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.