[{"data":1,"prerenderedAt":3029},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002F":3,"content-directory":2481},{"id":4,"title":5,"body":6,"date":2467,"description":2468,"difficulty":2469,"draft":2470,"extension":2471,"meta":2472,"navigation":202,"path":2473,"seo":2474,"stem":2475,"tags":2476,"updated":2467,"__hash__":2480},"content\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002Findex.md","Calling External Commands Safely with subprocess",{"type":7,"value":8,"toc":2448},"minimark",[9,38,43,77,81,84,88,120,154,158,161,1049,1052,1081,1102,1114,1130,1133,1137,1148,1617,1636,1640,1647,1701,1704,1708,1718,2106,2119,2307,2314,2318,2330,2334,2345,2352,2359,2368,2375,2389,2393,2410,2414,2444],[10,11,12,13,17,18,17,21,17,24,27,28,31,32,37],"p",{},"Your CLI needs to run another program — ",[14,15,16],"code",{},"rsync",", ",[14,19,20],{},"terraform",[14,22,23],{},"ffmpeg",[14,25,26],{},"kubectl"," — and act on the result. The one-liner that works on your machine (",[14,29,30],{},"subprocess.run(f\"rsync -a {src} {dest}\", shell=True)",") ignores failures, breaks on spaces in paths, decodes output with whatever locale is active, can hang forever, and turns a malicious filename into a command. This guide replaces it with a small helper you can drop into any Typer or Click project: one function that runs a program with every behaviour chosen on purpose, and reports failures as your tool's errors rather than Python's. It is the practical companion to the ",[33,34,36],"a",{"href":35},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002F","subprocess topic overview",".",[39,40,42],"h2",{"id":41},"prerequisites","Prerequisites",[44,45,46,54,57,63],"ul",{},[47,48,49,50,53],"li",{},"Python 3.10 or newer (the code uses ",[14,51,52],{},"X | None"," union syntax).",[47,55,56],{},"A CLI built with Typer or Click — the examples use Typer; the helper itself has no framework dependency.",[47,58,59,62],{},[14,60,61],{},"pytest"," for the tests at the end.",[47,64,65,66,69,70,72,73,76],{},"Some external program to call. The examples use ",[14,67,68],{},"git"," and ",[14,71,16],{},", but anything on your ",[14,74,75],{},"PATH"," works.",[39,78,80],{"id":79},"why-the-one-liner-is-dangerous","Why the one-liner is dangerous",[10,82,83],{},"Before the recipe, it is worth being precise about what goes wrong with a command string, because each problem maps to one line of the fix.",[85,86],"inline-diagram",{"name":87},"sp-argv-vs-string",[10,89,90,91,94,95,98,99,69,102,105,106,17,109,17,112,115,116,119],{},"With ",[14,92,93],{},"shell=True",", Python runs ",[14,96,97],{},"\u002Fbin\u002Fsh -c \"\u003Cyour string>\"",". The shell splits on whitespace, so a path with a space becomes two arguments. It expands ",[14,100,101],{},"*",[14,103,104],{},"~",", so a filename with a glob character matches other files. It interprets ",[14,107,108],{},";",[14,110,111],{},"|",[14,113,114],{},"&&",", backticks and ",[14,117,118],{},"$(...)",", so a crafted value runs extra commands. Quoting correctly by hand is possible but fragile, and nobody reviewing the code can tell at a glance whether it was done right.",[10,121,122,123,126,127,130,131,17,134,137,138,141,142,145,146,149,150,153],{},"Without ",[14,124,125],{},"check=True",", a failing child returns a non-zero ",[14,128,129],{},"returncode"," that nothing reads, and your tool carries on as though the step succeeded. Without an explicit ",[14,132,133],{},"encoding",[14,135,136],{},"text=True"," decodes with ",[14,139,140],{},"locale.getpreferredencoding()"," — UTF-8 on most Linux systems, often ",[14,143,144],{},"cp1252"," on Windows — so the same output parses on one machine and raises ",[14,147,148],{},"UnicodeDecodeError"," on another. Without a ",[14,151,152],{},"timeout",", a child waiting for a password prompt that will never be answered blocks your CLI until someone notices.",[39,155,157],{"id":156},"the-recipe-one-helper-for-every-call","The recipe: one helper for every call",[10,159,160],{},"The helper below is the whole technique. It takes an argument list and returns a result object; every failure mode becomes a single exception type your command layer can turn into a message.",[162,163,168],"pre",{"className":164,"code":165,"language":166,"meta":167,"style":167},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fproc.py\nfrom __future__ import annotations\n\nimport os\nimport shutil\nimport subprocess\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\n\nclass CommandError(Exception):\n    \"\"\"A child program could not be run, timed out, or failed.\"\"\"\n\n    def __init__(self, message: str, exit_code: int, detail: str = \"\") -> None:\n        super().__init__(message)\n        self.exit_code = exit_code\n        self.detail = detail\n\n\n@dataclass(frozen=True)\nclass Result:\n    stdout: str\n    stderr: str\n    returncode: int\n\n\ndef run_command(\n    argv: list[str],\n    *,\n    cwd: Path | None = None,\n    extra_env: dict[str, str] | None = None,\n    timeout: float = 120.0,\n    check: bool = True,\n) -> Result:\n    \"\"\"Run argv without a shell and return its decoded output.\"\"\"\n    if not argv:\n        raise ValueError(\"argv must not be empty\")\n\n    program = shutil.which(argv[0])\n    if program is None:\n        raise CommandError(f\"{argv[0]!r} was not found on PATH\", exit_code=127)\n\n    env = {**os.environ, **(extra_env or {})}\n    try:\n        proc = subprocess.run(\n            [program, *argv[1:]],\n            cwd=cwd,\n            env=env,\n            stdin=subprocess.DEVNULL,\n            capture_output=True,\n            text=True,\n            encoding=\"utf-8\",\n            errors=\"replace\",\n            timeout=timeout,\n            check=False,\n        )\n    except subprocess.TimeoutExpired as exc:\n        raise CommandError(\n            f\"{argv[0]} did not finish within {timeout:g}s\", exit_code=124\n        ) from exc\n\n    if check and proc.returncode != 0:\n        raise CommandError(\n            f\"{argv[0]} exited with status {proc.returncode}\",\n            exit_code=1,\n            detail=proc.stderr.strip(),\n        )\n    return Result(proc.stdout, proc.stderr, proc.returncode)\n","python","",[14,169,170,179,197,204,213,221,229,242,255,260,265,284,291,296,337,352,367,380,385,390,410,420,429,437,446,451,456,468,479,488,505,530,546,562,568,574,586,602,607,624,639,685,690,718,726,737,753,764,775,791,803,815,828,841,852,865,871,886,894,936,947,952,974,981,1012,1024,1035,1040],{"__ignoreMap":167},[171,172,175],"span",{"class":173,"line":174},"line",1,[171,176,178],{"class":177},"sJ8bj","# src\u002Fmytool\u002Fproc.py\n",[171,180,182,186,190,193],{"class":173,"line":181},2,[171,183,185],{"class":184},"szBVR","from",[171,187,189],{"class":188},"sj4cs"," __future__",[171,191,192],{"class":184}," import",[171,194,196],{"class":195},"sVt8B"," annotations\n",[171,198,200],{"class":173,"line":199},3,[171,201,203],{"emptyLinePlaceholder":202},true,"\n",[171,205,207,210],{"class":173,"line":206},4,[171,208,209],{"class":184},"import",[171,211,212],{"class":195}," os\n",[171,214,216,218],{"class":173,"line":215},5,[171,217,209],{"class":184},[171,219,220],{"class":195}," shutil\n",[171,222,224,226],{"class":173,"line":223},6,[171,225,209],{"class":184},[171,227,228],{"class":195}," subprocess\n",[171,230,232,234,237,239],{"class":173,"line":231},7,[171,233,185],{"class":184},[171,235,236],{"class":195}," dataclasses ",[171,238,209],{"class":184},[171,240,241],{"class":195}," dataclass\n",[171,243,245,247,250,252],{"class":173,"line":244},8,[171,246,185],{"class":184},[171,248,249],{"class":195}," pathlib ",[171,251,209],{"class":184},[171,253,254],{"class":195}," Path\n",[171,256,258],{"class":173,"line":257},9,[171,259,203],{"emptyLinePlaceholder":202},[171,261,263],{"class":173,"line":262},10,[171,264,203],{"emptyLinePlaceholder":202},[171,266,268,271,275,278,281],{"class":173,"line":267},11,[171,269,270],{"class":184},"class",[171,272,274],{"class":273},"sScJk"," CommandError",[171,276,277],{"class":195},"(",[171,279,280],{"class":188},"Exception",[171,282,283],{"class":195},"):\n",[171,285,287],{"class":173,"line":286},12,[171,288,290],{"class":289},"sZZnC","    \"\"\"A child program could not be run, timed out, or failed.\"\"\"\n",[171,292,294],{"class":173,"line":293},13,[171,295,203],{"emptyLinePlaceholder":202},[171,297,299,302,305,308,311,314,317,320,322,325,328,331,334],{"class":173,"line":298},14,[171,300,301],{"class":184},"    def",[171,303,304],{"class":188}," __init__",[171,306,307],{"class":195},"(self, message: ",[171,309,310],{"class":188},"str",[171,312,313],{"class":195},", exit_code: ",[171,315,316],{"class":188},"int",[171,318,319],{"class":195},", detail: ",[171,321,310],{"class":188},[171,323,324],{"class":184}," =",[171,326,327],{"class":289}," \"\"",[171,329,330],{"class":195},") -> ",[171,332,333],{"class":188},"None",[171,335,336],{"class":195},":\n",[171,338,340,343,346,349],{"class":173,"line":339},15,[171,341,342],{"class":188},"        super",[171,344,345],{"class":195},"().",[171,347,348],{"class":188},"__init__",[171,350,351],{"class":195},"(message)\n",[171,353,355,358,361,364],{"class":173,"line":354},16,[171,356,357],{"class":188},"        self",[171,359,360],{"class":195},".exit_code ",[171,362,363],{"class":184},"=",[171,365,366],{"class":195}," exit_code\n",[171,368,370,372,375,377],{"class":173,"line":369},17,[171,371,357],{"class":188},[171,373,374],{"class":195},".detail ",[171,376,363],{"class":184},[171,378,379],{"class":195}," detail\n",[171,381,383],{"class":173,"line":382},18,[171,384,203],{"emptyLinePlaceholder":202},[171,386,388],{"class":173,"line":387},19,[171,389,203],{"emptyLinePlaceholder":202},[171,391,393,396,398,402,404,407],{"class":173,"line":392},20,[171,394,395],{"class":273},"@dataclass",[171,397,277],{"class":195},[171,399,401],{"class":400},"s4XuR","frozen",[171,403,363],{"class":184},[171,405,406],{"class":188},"True",[171,408,409],{"class":195},")\n",[171,411,413,415,418],{"class":173,"line":412},21,[171,414,270],{"class":184},[171,416,417],{"class":273}," Result",[171,419,336],{"class":195},[171,421,423,426],{"class":173,"line":422},22,[171,424,425],{"class":195},"    stdout: ",[171,427,428],{"class":188},"str\n",[171,430,432,435],{"class":173,"line":431},23,[171,433,434],{"class":195},"    stderr: ",[171,436,428],{"class":188},[171,438,440,443],{"class":173,"line":439},24,[171,441,442],{"class":195},"    returncode: ",[171,444,445],{"class":188},"int\n",[171,447,449],{"class":173,"line":448},25,[171,450,203],{"emptyLinePlaceholder":202},[171,452,454],{"class":173,"line":453},26,[171,455,203],{"emptyLinePlaceholder":202},[171,457,459,462,465],{"class":173,"line":458},27,[171,460,461],{"class":184},"def",[171,463,464],{"class":273}," run_command",[171,466,467],{"class":195},"(\n",[171,469,471,474,476],{"class":173,"line":470},28,[171,472,473],{"class":195},"    argv: list[",[171,475,310],{"class":188},[171,477,478],{"class":195},"],\n",[171,480,482,485],{"class":173,"line":481},29,[171,483,484],{"class":184},"    *",[171,486,487],{"class":195},",\n",[171,489,491,494,496,499,501,503],{"class":173,"line":490},30,[171,492,493],{"class":195},"    cwd: Path ",[171,495,111],{"class":184},[171,497,498],{"class":188}," None",[171,500,324],{"class":184},[171,502,498],{"class":188},[171,504,487],{"class":195},[171,506,508,511,513,515,517,520,522,524,526,528],{"class":173,"line":507},31,[171,509,510],{"class":195},"    extra_env: dict[",[171,512,310],{"class":188},[171,514,17],{"class":195},[171,516,310],{"class":188},[171,518,519],{"class":195},"] ",[171,521,111],{"class":184},[171,523,498],{"class":188},[171,525,324],{"class":184},[171,527,498],{"class":188},[171,529,487],{"class":195},[171,531,533,536,539,541,544],{"class":173,"line":532},32,[171,534,535],{"class":195},"    timeout: ",[171,537,538],{"class":188},"float",[171,540,324],{"class":184},[171,542,543],{"class":188}," 120.0",[171,545,487],{"class":195},[171,547,549,552,555,557,560],{"class":173,"line":548},33,[171,550,551],{"class":195},"    check: ",[171,553,554],{"class":188},"bool",[171,556,324],{"class":184},[171,558,559],{"class":188}," True",[171,561,487],{"class":195},[171,563,565],{"class":173,"line":564},34,[171,566,567],{"class":195},") -> Result:\n",[171,569,571],{"class":173,"line":570},35,[171,572,573],{"class":289},"    \"\"\"Run argv without a shell and return its decoded output.\"\"\"\n",[171,575,577,580,583],{"class":173,"line":576},36,[171,578,579],{"class":184},"    if",[171,581,582],{"class":184}," not",[171,584,585],{"class":195}," argv:\n",[171,587,589,592,595,597,600],{"class":173,"line":588},37,[171,590,591],{"class":184},"        raise",[171,593,594],{"class":188}," ValueError",[171,596,277],{"class":195},[171,598,599],{"class":289},"\"argv must not be empty\"",[171,601,409],{"class":195},[171,603,605],{"class":173,"line":604},38,[171,606,203],{"emptyLinePlaceholder":202},[171,608,610,613,615,618,621],{"class":173,"line":609},39,[171,611,612],{"class":195},"    program ",[171,614,363],{"class":184},[171,616,617],{"class":195}," shutil.which(argv[",[171,619,620],{"class":188},"0",[171,622,623],{"class":195},"])\n",[171,625,627,629,632,635,637],{"class":173,"line":626},40,[171,628,579],{"class":184},[171,630,631],{"class":195}," program ",[171,633,634],{"class":184},"is",[171,636,498],{"class":188},[171,638,336],{"class":195},[171,640,642,644,647,650,653,656,659,661,664,667,670,673,675,678,680,683],{"class":173,"line":641},41,[171,643,591],{"class":184},[171,645,646],{"class":195}," CommandError(",[171,648,649],{"class":184},"f",[171,651,652],{"class":289},"\"",[171,654,655],{"class":188},"{",[171,657,658],{"class":195},"argv[",[171,660,620],{"class":188},[171,662,663],{"class":195},"]",[171,665,666],{"class":184},"!r",[171,668,669],{"class":188},"}",[171,671,672],{"class":289}," was not found on PATH\"",[171,674,17],{"class":195},[171,676,677],{"class":400},"exit_code",[171,679,363],{"class":184},[171,681,682],{"class":188},"127",[171,684,409],{"class":195},[171,686,688],{"class":173,"line":687},42,[171,689,203],{"emptyLinePlaceholder":202},[171,691,693,696,698,701,704,707,709,712,715],{"class":173,"line":692},43,[171,694,695],{"class":195},"    env ",[171,697,363],{"class":184},[171,699,700],{"class":195}," {",[171,702,703],{"class":184},"**",[171,705,706],{"class":195},"os.environ, ",[171,708,703],{"class":184},[171,710,711],{"class":195},"(extra_env ",[171,713,714],{"class":184},"or",[171,716,717],{"class":195}," {})}\n",[171,719,721,724],{"class":173,"line":720},44,[171,722,723],{"class":184},"    try",[171,725,336],{"class":195},[171,727,729,732,734],{"class":173,"line":728},45,[171,730,731],{"class":195},"        proc ",[171,733,363],{"class":184},[171,735,736],{"class":195}," subprocess.run(\n",[171,738,740,743,745,747,750],{"class":173,"line":739},46,[171,741,742],{"class":195},"            [program, ",[171,744,101],{"class":184},[171,746,658],{"class":195},[171,748,749],{"class":188},"1",[171,751,752],{"class":195},":]],\n",[171,754,756,759,761],{"class":173,"line":755},47,[171,757,758],{"class":400},"            cwd",[171,760,363],{"class":184},[171,762,763],{"class":195},"cwd,\n",[171,765,767,770,772],{"class":173,"line":766},48,[171,768,769],{"class":400},"            env",[171,771,363],{"class":184},[171,773,774],{"class":195},"env,\n",[171,776,778,781,783,786,789],{"class":173,"line":777},49,[171,779,780],{"class":400},"            stdin",[171,782,363],{"class":184},[171,784,785],{"class":195},"subprocess.",[171,787,788],{"class":188},"DEVNULL",[171,790,487],{"class":195},[171,792,794,797,799,801],{"class":173,"line":793},50,[171,795,796],{"class":400},"            capture_output",[171,798,363],{"class":184},[171,800,406],{"class":188},[171,802,487],{"class":195},[171,804,806,809,811,813],{"class":173,"line":805},51,[171,807,808],{"class":400},"            text",[171,810,363],{"class":184},[171,812,406],{"class":188},[171,814,487],{"class":195},[171,816,818,821,823,826],{"class":173,"line":817},52,[171,819,820],{"class":400},"            encoding",[171,822,363],{"class":184},[171,824,825],{"class":289},"\"utf-8\"",[171,827,487],{"class":195},[171,829,831,834,836,839],{"class":173,"line":830},53,[171,832,833],{"class":400},"            errors",[171,835,363],{"class":184},[171,837,838],{"class":289},"\"replace\"",[171,840,487],{"class":195},[171,842,844,847,849],{"class":173,"line":843},54,[171,845,846],{"class":400},"            timeout",[171,848,363],{"class":184},[171,850,851],{"class":195},"timeout,\n",[171,853,855,858,860,863],{"class":173,"line":854},55,[171,856,857],{"class":400},"            check",[171,859,363],{"class":184},[171,861,862],{"class":188},"False",[171,864,487],{"class":195},[171,866,868],{"class":173,"line":867},56,[171,869,870],{"class":195},"        )\n",[171,872,874,877,880,883],{"class":173,"line":873},57,[171,875,876],{"class":184},"    except",[171,878,879],{"class":195}," subprocess.TimeoutExpired ",[171,881,882],{"class":184},"as",[171,884,885],{"class":195}," exc:\n",[171,887,889,891],{"class":173,"line":888},58,[171,890,591],{"class":184},[171,892,893],{"class":195}," CommandError(\n",[171,895,897,900,902,904,906,908,910,912,915,917,919,922,924,927,929,931,933],{"class":173,"line":896},59,[171,898,899],{"class":184},"            f",[171,901,652],{"class":289},[171,903,655],{"class":188},[171,905,658],{"class":195},[171,907,620],{"class":188},[171,909,663],{"class":195},[171,911,669],{"class":188},[171,913,914],{"class":289}," did not finish within ",[171,916,655],{"class":188},[171,918,152],{"class":195},[171,920,921],{"class":184},":g",[171,923,669],{"class":188},[171,925,926],{"class":289},"s\"",[171,928,17],{"class":195},[171,930,677],{"class":400},[171,932,363],{"class":184},[171,934,935],{"class":188},"124\n",[171,937,939,942,944],{"class":173,"line":938},60,[171,940,941],{"class":195},"        ) ",[171,943,185],{"class":184},[171,945,946],{"class":195}," exc\n",[171,948,950],{"class":173,"line":949},61,[171,951,203],{"emptyLinePlaceholder":202},[171,953,955,957,960,963,966,969,972],{"class":173,"line":954},62,[171,956,579],{"class":184},[171,958,959],{"class":195}," check ",[171,961,962],{"class":184},"and",[171,964,965],{"class":195}," proc.returncode ",[171,967,968],{"class":184},"!=",[171,970,971],{"class":188}," 0",[171,973,336],{"class":195},[171,975,977,979],{"class":173,"line":976},63,[171,978,591],{"class":184},[171,980,893],{"class":195},[171,982,984,986,988,990,992,994,996,998,1001,1003,1006,1008,1010],{"class":173,"line":983},64,[171,985,899],{"class":184},[171,987,652],{"class":289},[171,989,655],{"class":188},[171,991,658],{"class":195},[171,993,620],{"class":188},[171,995,663],{"class":195},[171,997,669],{"class":188},[171,999,1000],{"class":289}," exited with status ",[171,1002,655],{"class":188},[171,1004,1005],{"class":195},"proc.returncode",[171,1007,669],{"class":188},[171,1009,652],{"class":289},[171,1011,487],{"class":195},[171,1013,1015,1018,1020,1022],{"class":173,"line":1014},65,[171,1016,1017],{"class":400},"            exit_code",[171,1019,363],{"class":184},[171,1021,749],{"class":188},[171,1023,487],{"class":195},[171,1025,1027,1030,1032],{"class":173,"line":1026},66,[171,1028,1029],{"class":400},"            detail",[171,1031,363],{"class":184},[171,1033,1034],{"class":195},"proc.stderr.strip(),\n",[171,1036,1038],{"class":173,"line":1037},67,[171,1039,870],{"class":195},[171,1041,1043,1046],{"class":173,"line":1042},68,[171,1044,1045],{"class":184},"    return",[171,1047,1048],{"class":195}," Result(proc.stdout, proc.stderr, proc.returncode)\n",[10,1050,1051],{},"A few choices deserve explanation.",[10,1053,1054,1061,1062,1065,1066,1069,1070,69,1073,1076,1077,1080],{},[1055,1056,1057,1060],"strong",{},[14,1058,1059],{},"shutil.which()"," before running."," Without it, a missing program raises ",[14,1063,1064],{},"FileNotFoundError"," from deep inside ",[14,1067,1068],{},"subprocess",", with a message that names the internal call rather than the tool the user needs to install. Resolving first gives you a clean error and, on Windows, the full path to ",[14,1071,1072],{},".cmd",[14,1074,1075],{},".bat"," shims such as ",[14,1078,1079],{},"npm.cmd",", which cannot be launched by bare name without a shell.",[10,1082,1083,1089,1090,1093,1094,1097,1098,1101],{},[1055,1084,1085,1088],{},[14,1086,1087],{},"check=False"," internally, with our own check."," ",[14,1091,1092],{},"subprocess.run(check=True)"," raises ",[14,1095,1096],{},"CalledProcessError",", which we would only catch and re-raise. Checking the return code ourselves keeps one code path and lets callers opt out — useful for commands like ",[14,1099,1100],{},"git diff --quiet"," where exit code 1 is an answer, not a failure.",[10,1103,1104,1109,1110,1113],{},[1055,1105,1106,37],{},[14,1107,1108],{},"stdin=subprocess.DEVNULL"," A child that decides to prompt (\"Password:\", \"Overwrite? ",[171,1111,1112],{},"y\u002FN","\") gets end-of-file immediately and fails fast instead of hanging. If your tool is reading its own input from a pipe, this also stops the child swallowing it.",[10,1115,1116,1121,1122,1126,1127,37],{},[1055,1117,1118,37],{},[14,1119,1120],{},"errors=\"replace\""," Output you only display should never crash your tool over one bad byte. If you ",[1123,1124,1125],"em",{},"parse"," the output and need to know about corruption, switch that call to ",[14,1128,1129],{},"errors=\"strict\"",[85,1131],{"name":1132},"sp-run-kwargs",[39,1134,1136],{"id":1135},"wiring-it-into-a-command","Wiring it into a command",[10,1138,1139,1140,1143,1144,1147],{},"The command layer's only job is to call the helper and translate ",[14,1141,1142],{},"CommandError"," into user-facing output and an exit code. Keep that translation in one place — a small decorator or a top-level handler — rather than repeating ",[14,1145,1146],{},"try\u002Fexcept"," in every command.",[162,1149,1151],{"className":164,"code":1150,"language":166,"meta":167,"style":167},"# src\u002Fmytool\u002Fcli.py\nfrom pathlib import Path\n\nimport typer\n\nfrom mytool.proc import CommandError, run_command\n\napp = typer.Typer(no_args_is_help=True)\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Deployment helpers for the docs site.\"\"\"\n\n\n@app.command()\ndef sync(\n    src: Path = typer.Argument(..., exists=True, file_okay=False),\n    dest: str = typer.Argument(..., help=\"Local path or host:path\"),\n    dry_run: bool = typer.Option(False, \"--dry-run\", \"-n\"),\n) -> None:\n    \"\"\"Mirror SRC into DEST using rsync.\"\"\"\n    argv = [\"rsync\", \"-a\", \"--delete\"]\n    if dry_run:\n        argv.append(\"--dry-run\")\n    argv += [\"--\", f\"{src}\u002F\", dest]\n    try:\n        run_command(argv, timeout=600)\n    except CommandError as exc:\n        typer.secho(f\"error: {exc}\", fg=typer.colors.RED, err=True)\n        if exc.detail:\n            typer.echo(exc.detail.splitlines()[-1], err=True)\n        raise typer.Exit(exc.exit_code)\n    typer.echo(f\"synced {src} -> {dest}\")\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1152,1153,1158,1168,1172,1179,1183,1195,1199,1218,1222,1226,1234,1248,1253,1257,1261,1268,1277,1311,1336,1362,1370,1375,1401,1408,1417,1448,1454,1468,1479,1522,1530,1551,1558,1588,1592,1596,1612],{"__ignoreMap":167},[171,1154,1155],{"class":173,"line":174},[171,1156,1157],{"class":177},"# src\u002Fmytool\u002Fcli.py\n",[171,1159,1160,1162,1164,1166],{"class":173,"line":181},[171,1161,185],{"class":184},[171,1163,249],{"class":195},[171,1165,209],{"class":184},[171,1167,254],{"class":195},[171,1169,1170],{"class":173,"line":199},[171,1171,203],{"emptyLinePlaceholder":202},[171,1173,1174,1176],{"class":173,"line":206},[171,1175,209],{"class":184},[171,1177,1178],{"class":195}," typer\n",[171,1180,1181],{"class":173,"line":215},[171,1182,203],{"emptyLinePlaceholder":202},[171,1184,1185,1187,1190,1192],{"class":173,"line":223},[171,1186,185],{"class":184},[171,1188,1189],{"class":195}," mytool.proc ",[171,1191,209],{"class":184},[171,1193,1194],{"class":195}," CommandError, run_command\n",[171,1196,1197],{"class":173,"line":231},[171,1198,203],{"emptyLinePlaceholder":202},[171,1200,1201,1204,1206,1209,1212,1214,1216],{"class":173,"line":244},[171,1202,1203],{"class":195},"app ",[171,1205,363],{"class":184},[171,1207,1208],{"class":195}," typer.Typer(",[171,1210,1211],{"class":400},"no_args_is_help",[171,1213,363],{"class":184},[171,1215,406],{"class":188},[171,1217,409],{"class":195},[171,1219,1220],{"class":173,"line":257},[171,1221,203],{"emptyLinePlaceholder":202},[171,1223,1224],{"class":173,"line":262},[171,1225,203],{"emptyLinePlaceholder":202},[171,1227,1228,1231],{"class":173,"line":267},[171,1229,1230],{"class":273},"@app.callback",[171,1232,1233],{"class":195},"()\n",[171,1235,1236,1238,1241,1244,1246],{"class":173,"line":286},[171,1237,461],{"class":184},[171,1239,1240],{"class":273}," main",[171,1242,1243],{"class":195},"() -> ",[171,1245,333],{"class":188},[171,1247,336],{"class":195},[171,1249,1250],{"class":173,"line":293},[171,1251,1252],{"class":289},"    \"\"\"Deployment helpers for the docs site.\"\"\"\n",[171,1254,1255],{"class":173,"line":298},[171,1256,203],{"emptyLinePlaceholder":202},[171,1258,1259],{"class":173,"line":339},[171,1260,203],{"emptyLinePlaceholder":202},[171,1262,1263,1266],{"class":173,"line":354},[171,1264,1265],{"class":273},"@app.command",[171,1267,1233],{"class":195},[171,1269,1270,1272,1275],{"class":173,"line":369},[171,1271,461],{"class":184},[171,1273,1274],{"class":273}," sync",[171,1276,467],{"class":195},[171,1278,1279,1282,1284,1287,1290,1292,1295,1297,1299,1301,1304,1306,1308],{"class":173,"line":382},[171,1280,1281],{"class":195},"    src: Path ",[171,1283,363],{"class":184},[171,1285,1286],{"class":195}," typer.Argument(",[171,1288,1289],{"class":188},"...",[171,1291,17],{"class":195},[171,1293,1294],{"class":400},"exists",[171,1296,363],{"class":184},[171,1298,406],{"class":188},[171,1300,17],{"class":195},[171,1302,1303],{"class":400},"file_okay",[171,1305,363],{"class":184},[171,1307,862],{"class":188},[171,1309,1310],{"class":195},"),\n",[171,1312,1313,1316,1318,1320,1322,1324,1326,1329,1331,1334],{"class":173,"line":387},[171,1314,1315],{"class":195},"    dest: ",[171,1317,310],{"class":188},[171,1319,324],{"class":184},[171,1321,1286],{"class":195},[171,1323,1289],{"class":188},[171,1325,17],{"class":195},[171,1327,1328],{"class":400},"help",[171,1330,363],{"class":184},[171,1332,1333],{"class":289},"\"Local path or host:path\"",[171,1335,1310],{"class":195},[171,1337,1338,1341,1343,1345,1348,1350,1352,1355,1357,1360],{"class":173,"line":392},[171,1339,1340],{"class":195},"    dry_run: ",[171,1342,554],{"class":188},[171,1344,324],{"class":184},[171,1346,1347],{"class":195}," typer.Option(",[171,1349,862],{"class":188},[171,1351,17],{"class":195},[171,1353,1354],{"class":289},"\"--dry-run\"",[171,1356,17],{"class":195},[171,1358,1359],{"class":289},"\"-n\"",[171,1361,1310],{"class":195},[171,1363,1364,1366,1368],{"class":173,"line":412},[171,1365,330],{"class":195},[171,1367,333],{"class":188},[171,1369,336],{"class":195},[171,1371,1372],{"class":173,"line":422},[171,1373,1374],{"class":289},"    \"\"\"Mirror SRC into DEST using rsync.\"\"\"\n",[171,1376,1377,1380,1382,1385,1388,1390,1393,1395,1398],{"class":173,"line":431},[171,1378,1379],{"class":195},"    argv ",[171,1381,363],{"class":184},[171,1383,1384],{"class":195}," [",[171,1386,1387],{"class":289},"\"rsync\"",[171,1389,17],{"class":195},[171,1391,1392],{"class":289},"\"-a\"",[171,1394,17],{"class":195},[171,1396,1397],{"class":289},"\"--delete\"",[171,1399,1400],{"class":195},"]\n",[171,1402,1403,1405],{"class":173,"line":439},[171,1404,579],{"class":184},[171,1406,1407],{"class":195}," dry_run:\n",[171,1409,1410,1413,1415],{"class":173,"line":448},[171,1411,1412],{"class":195},"        argv.append(",[171,1414,1354],{"class":289},[171,1416,409],{"class":195},[171,1418,1419,1421,1424,1426,1429,1431,1433,1435,1437,1440,1442,1445],{"class":173,"line":453},[171,1420,1379],{"class":195},[171,1422,1423],{"class":184},"+=",[171,1425,1384],{"class":195},[171,1427,1428],{"class":289},"\"--\"",[171,1430,17],{"class":195},[171,1432,649],{"class":184},[171,1434,652],{"class":289},[171,1436,655],{"class":188},[171,1438,1439],{"class":195},"src",[171,1441,669],{"class":188},[171,1443,1444],{"class":289},"\u002F\"",[171,1446,1447],{"class":195},", dest]\n",[171,1449,1450,1452],{"class":173,"line":458},[171,1451,723],{"class":184},[171,1453,336],{"class":195},[171,1455,1456,1459,1461,1463,1466],{"class":173,"line":470},[171,1457,1458],{"class":195},"        run_command(argv, ",[171,1460,152],{"class":400},[171,1462,363],{"class":184},[171,1464,1465],{"class":188},"600",[171,1467,409],{"class":195},[171,1469,1470,1472,1475,1477],{"class":173,"line":481},[171,1471,876],{"class":184},[171,1473,1474],{"class":195}," CommandError ",[171,1476,882],{"class":184},[171,1478,885],{"class":195},[171,1480,1481,1484,1486,1489,1491,1494,1496,1498,1500,1503,1505,1508,1511,1513,1516,1518,1520],{"class":173,"line":490},[171,1482,1483],{"class":195},"        typer.secho(",[171,1485,649],{"class":184},[171,1487,1488],{"class":289},"\"error: ",[171,1490,655],{"class":188},[171,1492,1493],{"class":195},"exc",[171,1495,669],{"class":188},[171,1497,652],{"class":289},[171,1499,17],{"class":195},[171,1501,1502],{"class":400},"fg",[171,1504,363],{"class":184},[171,1506,1507],{"class":195},"typer.colors.",[171,1509,1510],{"class":188},"RED",[171,1512,17],{"class":195},[171,1514,1515],{"class":400},"err",[171,1517,363],{"class":184},[171,1519,406],{"class":188},[171,1521,409],{"class":195},[171,1523,1524,1527],{"class":173,"line":507},[171,1525,1526],{"class":184},"        if",[171,1528,1529],{"class":195}," exc.detail:\n",[171,1531,1532,1535,1538,1540,1543,1545,1547,1549],{"class":173,"line":532},[171,1533,1534],{"class":195},"            typer.echo(exc.detail.splitlines()[",[171,1536,1537],{"class":184},"-",[171,1539,749],{"class":188},[171,1541,1542],{"class":195},"], ",[171,1544,1515],{"class":400},[171,1546,363],{"class":184},[171,1548,406],{"class":188},[171,1550,409],{"class":195},[171,1552,1553,1555],{"class":173,"line":548},[171,1554,591],{"class":184},[171,1556,1557],{"class":195}," typer.Exit(exc.exit_code)\n",[171,1559,1560,1563,1565,1568,1570,1572,1574,1577,1579,1582,1584,1586],{"class":173,"line":564},[171,1561,1562],{"class":195},"    typer.echo(",[171,1564,649],{"class":184},[171,1566,1567],{"class":289},"\"synced ",[171,1569,655],{"class":188},[171,1571,1439],{"class":195},[171,1573,669],{"class":188},[171,1575,1576],{"class":289}," -> ",[171,1578,655],{"class":188},[171,1580,1581],{"class":195},"dest",[171,1583,669],{"class":188},[171,1585,652],{"class":289},[171,1587,409],{"class":195},[171,1589,1590],{"class":173,"line":570},[171,1591,203],{"emptyLinePlaceholder":202},[171,1593,1594],{"class":173,"line":576},[171,1595,203],{"emptyLinePlaceholder":202},[171,1597,1598,1601,1604,1607,1610],{"class":173,"line":588},[171,1599,1600],{"class":184},"if",[171,1602,1603],{"class":188}," __name__",[171,1605,1606],{"class":184}," ==",[171,1608,1609],{"class":289}," \"__main__\"",[171,1611,336],{"class":195},[171,1613,1614],{"class":173,"line":604},[171,1615,1616],{"class":195},"    app()\n",[10,1618,1619,1620,1623,1624,1626,1627,1630,1631,1635],{},"Notice ",[14,1621,1622],{},"--"," before the positional paths. The argument list already prevents shell injection, but ",[14,1625,16],{}," still parses its own options — a directory named ",[14,1628,1629],{},"--rsh=..."," would otherwise be read as a flag. The ",[33,1632,1634],{"href":1633},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis\u002F","shell injection guide"," covers that second layer in more depth.",[39,1637,1639],{"id":1638},"ux-considerations","UX considerations",[10,1641,1642,1643,1646],{},"The helper decides ",[1123,1644,1645],{},"what"," happens; these decisions shape how it feels to the person running your tool.",[44,1648,1649,1658,1668,1677,1687],{},[47,1650,1651,1654,1655,1657],{},[1055,1652,1653],{},"Fail before starting."," If a command needs three programs, check all three with ",[14,1656,1059],{}," at the top of the command and report every missing one together. Discovering the second missing dependency after the first step has already modified files is the worst possible experience.",[47,1659,1660,1663,1664,1667],{},[1055,1661,1662],{},"Name the program and the fix."," \"rsync was not found on PATH\" is good; adding \"install it with ",[14,1665,1666],{},"brew install rsync"," or your package manager\" is better. Users can act on the second one without searching.",[47,1669,1670,1673,1674,37],{},[1055,1671,1672],{},"Show the tail, not the flood."," When a child fails, its last few stderr lines usually contain the reason. Print those, and offer the full log behind ",[14,1675,1676],{},"--verbose",[47,1678,1679,1682,1683,37],{},[1055,1680,1681],{},"Keep your stdout clean."," Child output you pass through goes to stderr unless it is genuinely your command's result. Scripts piping your tool depend on that separation; the reasoning is in ",[33,1684,1686],{"href":1685},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","working with stdin, stdout and pipes",[47,1688,1689,1692,1693,1696,1697,1700],{},[1055,1690,1691],{},"Pick timeouts per call."," Two minutes suits ",[14,1694,1695],{},"git fetch","; a video transcode might need an hour. Expose a ",[14,1698,1699],{},"--timeout"," option for the long ones rather than one global value.",[85,1702],{"name":1703},"sp-which-terminal",[39,1705,1707],{"id":1706},"testing-the-behaviour","Testing the behaviour",[10,1709,1710,1711,1714,1715,1717],{},"Test the helper against real, always-available programs — the Python interpreter itself is ideal, because ",[14,1712,1713],{},"sys.executable"," exists on every machine that runs your tests. Test commands by replacing the helper, so they never depend on ",[14,1716,16],{}," being installed.",[162,1719,1721],{"className":164,"code":1720,"language":166,"meta":167,"style":167},"# tests\u002Ftest_proc.py\nimport sys\n\nimport pytest\n\nfrom mytool.proc import CommandError, run_command\n\n\ndef test_captures_stdout():\n    result = run_command([sys.executable, \"-c\", \"print('héllo')\"])\n    assert result.stdout == \"héllo\\n\"\n\n\ndef test_nonzero_exit_raises_with_detail():\n    code = \"import sys; sys.stderr.write('boom\\\\n'); sys.exit(3)\"\n    with pytest.raises(CommandError) as info:\n        run_command([sys.executable, \"-c\", code])\n    assert info.value.exit_code == 1\n    assert info.value.detail == \"boom\"\n\n\ndef test_check_false_returns_code():\n    result = run_command([sys.executable, \"-c\", \"raise SystemExit(1)\"], check=False)\n    assert result.returncode == 1\n\n\ndef test_missing_program_is_127():\n    with pytest.raises(CommandError) as info:\n        run_command([\"definitely-not-a-real-program-xyz\"])\n    assert info.value.exit_code == 127\n\n\ndef test_timeout_is_124():\n    with pytest.raises(CommandError) as info:\n        run_command([sys.executable, \"-c\", \"import time; time.sleep(5)\"], timeout=0.2)\n    assert info.value.exit_code == 124\n\n\ndef test_stdin_is_closed():\n    result = run_command([sys.executable, \"-c\", \"import sys; print(repr(sys.stdin.read()))\"])\n    assert result.stdout.strip() == \"''\"\n",[14,1722,1723,1728,1735,1739,1746,1750,1760,1764,1768,1778,1798,1818,1822,1826,1835,1851,1864,1874,1886,1898,1902,1906,1915,1941,1952,1956,1960,1969,1979,1989,2000,2004,2008,2017,2027,2049,2060,2064,2068,2077,2094],{"__ignoreMap":167},[171,1724,1725],{"class":173,"line":174},[171,1726,1727],{"class":177},"# tests\u002Ftest_proc.py\n",[171,1729,1730,1732],{"class":173,"line":181},[171,1731,209],{"class":184},[171,1733,1734],{"class":195}," sys\n",[171,1736,1737],{"class":173,"line":199},[171,1738,203],{"emptyLinePlaceholder":202},[171,1740,1741,1743],{"class":173,"line":206},[171,1742,209],{"class":184},[171,1744,1745],{"class":195}," pytest\n",[171,1747,1748],{"class":173,"line":215},[171,1749,203],{"emptyLinePlaceholder":202},[171,1751,1752,1754,1756,1758],{"class":173,"line":223},[171,1753,185],{"class":184},[171,1755,1189],{"class":195},[171,1757,209],{"class":184},[171,1759,1194],{"class":195},[171,1761,1762],{"class":173,"line":231},[171,1763,203],{"emptyLinePlaceholder":202},[171,1765,1766],{"class":173,"line":244},[171,1767,203],{"emptyLinePlaceholder":202},[171,1769,1770,1772,1775],{"class":173,"line":257},[171,1771,461],{"class":184},[171,1773,1774],{"class":273}," test_captures_stdout",[171,1776,1777],{"class":195},"():\n",[171,1779,1780,1783,1785,1788,1791,1793,1796],{"class":173,"line":262},[171,1781,1782],{"class":195},"    result ",[171,1784,363],{"class":184},[171,1786,1787],{"class":195}," run_command([sys.executable, ",[171,1789,1790],{"class":289},"\"-c\"",[171,1792,17],{"class":195},[171,1794,1795],{"class":289},"\"print('héllo')\"",[171,1797,623],{"class":195},[171,1799,1800,1803,1806,1809,1812,1815],{"class":173,"line":267},[171,1801,1802],{"class":184},"    assert",[171,1804,1805],{"class":195}," result.stdout ",[171,1807,1808],{"class":184},"==",[171,1810,1811],{"class":289}," \"héllo",[171,1813,1814],{"class":188},"\\n",[171,1816,1817],{"class":289},"\"\n",[171,1819,1820],{"class":173,"line":286},[171,1821,203],{"emptyLinePlaceholder":202},[171,1823,1824],{"class":173,"line":293},[171,1825,203],{"emptyLinePlaceholder":202},[171,1827,1828,1830,1833],{"class":173,"line":298},[171,1829,461],{"class":184},[171,1831,1832],{"class":273}," test_nonzero_exit_raises_with_detail",[171,1834,1777],{"class":195},[171,1836,1837,1840,1842,1845,1848],{"class":173,"line":339},[171,1838,1839],{"class":195},"    code ",[171,1841,363],{"class":184},[171,1843,1844],{"class":289}," \"import sys; sys.stderr.write('boom",[171,1846,1847],{"class":188},"\\\\",[171,1849,1850],{"class":289},"n'); sys.exit(3)\"\n",[171,1852,1853,1856,1859,1861],{"class":173,"line":354},[171,1854,1855],{"class":184},"    with",[171,1857,1858],{"class":195}," pytest.raises(CommandError) ",[171,1860,882],{"class":184},[171,1862,1863],{"class":195}," info:\n",[171,1865,1866,1869,1871],{"class":173,"line":369},[171,1867,1868],{"class":195},"        run_command([sys.executable, ",[171,1870,1790],{"class":289},[171,1872,1873],{"class":195},", code])\n",[171,1875,1876,1878,1881,1883],{"class":173,"line":382},[171,1877,1802],{"class":184},[171,1879,1880],{"class":195}," info.value.exit_code ",[171,1882,1808],{"class":184},[171,1884,1885],{"class":188}," 1\n",[171,1887,1888,1890,1893,1895],{"class":173,"line":387},[171,1889,1802],{"class":184},[171,1891,1892],{"class":195}," info.value.detail ",[171,1894,1808],{"class":184},[171,1896,1897],{"class":289}," \"boom\"\n",[171,1899,1900],{"class":173,"line":392},[171,1901,203],{"emptyLinePlaceholder":202},[171,1903,1904],{"class":173,"line":412},[171,1905,203],{"emptyLinePlaceholder":202},[171,1907,1908,1910,1913],{"class":173,"line":422},[171,1909,461],{"class":184},[171,1911,1912],{"class":273}," test_check_false_returns_code",[171,1914,1777],{"class":195},[171,1916,1917,1919,1921,1923,1925,1927,1930,1932,1935,1937,1939],{"class":173,"line":431},[171,1918,1782],{"class":195},[171,1920,363],{"class":184},[171,1922,1787],{"class":195},[171,1924,1790],{"class":289},[171,1926,17],{"class":195},[171,1928,1929],{"class":289},"\"raise SystemExit(1)\"",[171,1931,1542],{"class":195},[171,1933,1934],{"class":400},"check",[171,1936,363],{"class":184},[171,1938,862],{"class":188},[171,1940,409],{"class":195},[171,1942,1943,1945,1948,1950],{"class":173,"line":439},[171,1944,1802],{"class":184},[171,1946,1947],{"class":195}," result.returncode ",[171,1949,1808],{"class":184},[171,1951,1885],{"class":188},[171,1953,1954],{"class":173,"line":448},[171,1955,203],{"emptyLinePlaceholder":202},[171,1957,1958],{"class":173,"line":453},[171,1959,203],{"emptyLinePlaceholder":202},[171,1961,1962,1964,1967],{"class":173,"line":458},[171,1963,461],{"class":184},[171,1965,1966],{"class":273}," test_missing_program_is_127",[171,1968,1777],{"class":195},[171,1970,1971,1973,1975,1977],{"class":173,"line":470},[171,1972,1855],{"class":184},[171,1974,1858],{"class":195},[171,1976,882],{"class":184},[171,1978,1863],{"class":195},[171,1980,1981,1984,1987],{"class":173,"line":481},[171,1982,1983],{"class":195},"        run_command([",[171,1985,1986],{"class":289},"\"definitely-not-a-real-program-xyz\"",[171,1988,623],{"class":195},[171,1990,1991,1993,1995,1997],{"class":173,"line":490},[171,1992,1802],{"class":184},[171,1994,1880],{"class":195},[171,1996,1808],{"class":184},[171,1998,1999],{"class":188}," 127\n",[171,2001,2002],{"class":173,"line":507},[171,2003,203],{"emptyLinePlaceholder":202},[171,2005,2006],{"class":173,"line":532},[171,2007,203],{"emptyLinePlaceholder":202},[171,2009,2010,2012,2015],{"class":173,"line":548},[171,2011,461],{"class":184},[171,2013,2014],{"class":273}," test_timeout_is_124",[171,2016,1777],{"class":195},[171,2018,2019,2021,2023,2025],{"class":173,"line":564},[171,2020,1855],{"class":184},[171,2022,1858],{"class":195},[171,2024,882],{"class":184},[171,2026,1863],{"class":195},[171,2028,2029,2031,2033,2035,2038,2040,2042,2044,2047],{"class":173,"line":570},[171,2030,1868],{"class":195},[171,2032,1790],{"class":289},[171,2034,17],{"class":195},[171,2036,2037],{"class":289},"\"import time; time.sleep(5)\"",[171,2039,1542],{"class":195},[171,2041,152],{"class":400},[171,2043,363],{"class":184},[171,2045,2046],{"class":188},"0.2",[171,2048,409],{"class":195},[171,2050,2051,2053,2055,2057],{"class":173,"line":576},[171,2052,1802],{"class":184},[171,2054,1880],{"class":195},[171,2056,1808],{"class":184},[171,2058,2059],{"class":188}," 124\n",[171,2061,2062],{"class":173,"line":588},[171,2063,203],{"emptyLinePlaceholder":202},[171,2065,2066],{"class":173,"line":604},[171,2067,203],{"emptyLinePlaceholder":202},[171,2069,2070,2072,2075],{"class":173,"line":609},[171,2071,461],{"class":184},[171,2073,2074],{"class":273}," test_stdin_is_closed",[171,2076,1777],{"class":195},[171,2078,2079,2081,2083,2085,2087,2089,2092],{"class":173,"line":626},[171,2080,1782],{"class":195},[171,2082,363],{"class":184},[171,2084,1787],{"class":195},[171,2086,1790],{"class":289},[171,2088,17],{"class":195},[171,2090,2091],{"class":289},"\"import sys; print(repr(sys.stdin.read()))\"",[171,2093,623],{"class":195},[171,2095,2096,2098,2101,2103],{"class":173,"line":641},[171,2097,1802],{"class":184},[171,2099,2100],{"class":195}," result.stdout.strip() ",[171,2102,1808],{"class":184},[171,2104,2105],{"class":289}," \"''\"\n",[10,2107,2108,2109,2112,2113,69,2115,2118],{},"For the command layer, patch ",[14,2110,2111],{},"run_command"," where the CLI module imports it and assert on the argument list — that is where the ",[14,2114,1622],{},[14,2116,2117],{},"--dry-run"," logic lives:",[162,2120,2122],{"className":164,"code":2121,"language":166,"meta":167,"style":167},"from typer.testing import CliRunner\n\nfrom mytool import cli\n\nrunner = CliRunner()\n\n\ndef test_sync_builds_argv(monkeypatch, tmp_path):\n    calls = []\n    monkeypatch.setattr(cli, \"run_command\", lambda argv, **kw: calls.append(argv))\n    result = runner.invoke(cli.app, [\"sync\", str(tmp_path), \"backup:\u002Fsrv\", \"-n\"])\n    assert result.exit_code == 0\n    assert calls == [[\"rsync\", \"-a\", \"--delete\", \"--dry-run\", \"--\", f\"{tmp_path}\u002F\", \"backup:\u002Fsrv\"]]\n",[14,2123,2124,2136,2140,2152,2156,2166,2170,2174,2184,2194,2215,2243,2255],{"__ignoreMap":167},[171,2125,2126,2128,2131,2133],{"class":173,"line":174},[171,2127,185],{"class":184},[171,2129,2130],{"class":195}," typer.testing ",[171,2132,209],{"class":184},[171,2134,2135],{"class":195}," CliRunner\n",[171,2137,2138],{"class":173,"line":181},[171,2139,203],{"emptyLinePlaceholder":202},[171,2141,2142,2144,2147,2149],{"class":173,"line":199},[171,2143,185],{"class":184},[171,2145,2146],{"class":195}," mytool ",[171,2148,209],{"class":184},[171,2150,2151],{"class":195}," cli\n",[171,2153,2154],{"class":173,"line":206},[171,2155,203],{"emptyLinePlaceholder":202},[171,2157,2158,2161,2163],{"class":173,"line":215},[171,2159,2160],{"class":195},"runner ",[171,2162,363],{"class":184},[171,2164,2165],{"class":195}," CliRunner()\n",[171,2167,2168],{"class":173,"line":223},[171,2169,203],{"emptyLinePlaceholder":202},[171,2171,2172],{"class":173,"line":231},[171,2173,203],{"emptyLinePlaceholder":202},[171,2175,2176,2178,2181],{"class":173,"line":244},[171,2177,461],{"class":184},[171,2179,2180],{"class":273}," test_sync_builds_argv",[171,2182,2183],{"class":195},"(monkeypatch, tmp_path):\n",[171,2185,2186,2189,2191],{"class":173,"line":257},[171,2187,2188],{"class":195},"    calls ",[171,2190,363],{"class":184},[171,2192,2193],{"class":195}," []\n",[171,2195,2196,2199,2202,2204,2207,2210,2212],{"class":173,"line":262},[171,2197,2198],{"class":195},"    monkeypatch.setattr(cli, ",[171,2200,2201],{"class":289},"\"run_command\"",[171,2203,17],{"class":195},[171,2205,2206],{"class":184},"lambda",[171,2208,2209],{"class":195}," argv, ",[171,2211,703],{"class":184},[171,2213,2214],{"class":195},"kw: calls.append(argv))\n",[171,2216,2217,2219,2221,2224,2227,2229,2231,2234,2237,2239,2241],{"class":173,"line":267},[171,2218,1782],{"class":195},[171,2220,363],{"class":184},[171,2222,2223],{"class":195}," runner.invoke(cli.app, [",[171,2225,2226],{"class":289},"\"sync\"",[171,2228,17],{"class":195},[171,2230,310],{"class":188},[171,2232,2233],{"class":195},"(tmp_path), ",[171,2235,2236],{"class":289},"\"backup:\u002Fsrv\"",[171,2238,17],{"class":195},[171,2240,1359],{"class":289},[171,2242,623],{"class":195},[171,2244,2245,2247,2250,2252],{"class":173,"line":286},[171,2246,1802],{"class":184},[171,2248,2249],{"class":195}," result.exit_code ",[171,2251,1808],{"class":184},[171,2253,2254],{"class":188}," 0\n",[171,2256,2257,2259,2262,2264,2267,2269,2271,2273,2275,2277,2279,2281,2283,2285,2287,2289,2291,2293,2296,2298,2300,2302,2304],{"class":173,"line":293},[171,2258,1802],{"class":184},[171,2260,2261],{"class":195}," calls ",[171,2263,1808],{"class":184},[171,2265,2266],{"class":195}," [[",[171,2268,1387],{"class":289},[171,2270,17],{"class":195},[171,2272,1392],{"class":289},[171,2274,17],{"class":195},[171,2276,1397],{"class":289},[171,2278,17],{"class":195},[171,2280,1354],{"class":289},[171,2282,17],{"class":195},[171,2284,1428],{"class":289},[171,2286,17],{"class":195},[171,2288,649],{"class":184},[171,2290,652],{"class":289},[171,2292,655],{"class":188},[171,2294,2295],{"class":195},"tmp_path",[171,2297,669],{"class":188},[171,2299,1444],{"class":289},[171,2301,17],{"class":195},[171,2303,2236],{"class":289},[171,2305,2306],{"class":195},"]]\n",[10,2308,2309,2310,37],{},"More patterns for this style of test are in ",[33,2311,2313],{"href":2312},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F","testing Click commands with CliRunner",[39,2315,2317],{"id":2316},"conclusion","Conclusion",[10,2319,2320,2321,2325,2326,37],{},"A safe subprocess call is not clever code — it is a dozen deliberate keyword arguments and a clear error type. Put them in one helper, route every external program through it, and the recurring bugs disappear together: no injection because there is no shell, no silent failures because the exit code is checked, no hangs because there is a timeout and no stdin, and no encoding crashes because decoding is explicit. When you need output while the child is still running, graduate to ",[33,2322,2324],{"href":2323},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time\u002F","streaming subprocess output in real time","; when you need finer control over what a timeout kills, read ",[33,2327,2329],{"href":2328},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes\u002F","handling subprocess timeouts and exit codes",[39,2331,2333],{"id":2332},"frequently-asked-questions","Frequently asked questions",[2335,2336,2338,2339,2341,2342,2344],"h3",{"id":2337},"why-not-just-use-checktrue-and-catch-calledprocesserror","Why not just use ",[14,2340,125],{}," and catch ",[14,2343,1096],{},"?",[10,2346,2347,2348,2351],{},"That works, and for a one-off script it is fine. A helper that checks the code itself gives you one exception type for all three failures — missing program, timeout and non-zero exit — each with the exit code your CLI should use. Callers then need a single ",[14,2349,2350],{},"except"," clause instead of three.",[2335,2353,2355,2356,2358],{"id":2354},"is-shutilwhich-a-security-check","Is ",[14,2357,1059],{}," a security check?",[10,2360,2361,2362,2364,2365,2367],{},"No. It finds the first matching executable on ",[14,2363,75],{},", which the user controls. That is the correct behaviour for a CLI — users expect their own ",[14,2366,75],{}," to be respected — but if your tool runs with elevated privileges, pass absolute paths to trusted binaries instead of searching.",[2335,2369,2371,2372,2374],{"id":2370},"should-i-pass-texttrue-or-handle-bytes-myself","Should I pass ",[14,2373,136],{}," or handle bytes myself?",[10,2376,2377,2378,2380,2381,2384,2385,2388],{},"Use ",[14,2379,136],{}," with an explicit encoding for anything line-oriented and human-readable. Keep bytes (omit ",[14,2382,2383],{},"text",") for binary output — image data, archives, or tools like ",[14,2386,2387],{},"git cat-file"," that can emit arbitrary content — and decode only the parts you need.",[2335,2390,2392],{"id":2391},"how-do-i-let-the-child-use-the-terminal-interactively","How do I let the child use the terminal interactively?",[10,2394,2395,2396,17,2399,69,2402,2405,2406,2409],{},"Leave ",[14,2397,2398],{},"stdin",[14,2400,2401],{},"stdout",[14,2403,2404],{},"stderr"," at their defaults (inherit) and do not set ",[14,2407,2408],{},"capture_output",". That is the right choice for launching an editor or a pager. Add a separate helper for those calls rather than adding flags to the capturing one.",[39,2411,2413],{"id":2412},"related","Related",[44,2415,2416,2422,2427,2432,2438],{},[47,2417,2418,2419],{},"Up: ",[33,2420,2421],{"href":35},"Running subprocesses from Python CLIs",[47,2423,2424],{},[33,2425,2426],{"href":2328},"Handling subprocess timeouts and exit codes",[47,2428,2429],{},[33,2430,2431],{"href":1633},"Avoiding shell injection in Python CLIs",[47,2433,2434],{},[33,2435,2437],{"href":2436},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli\u002F","Wrapping git and other tools from a Python CLI",[47,2439,2440],{},[33,2441,2443],{"href":2442},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","Choosing exit codes for CLI tools",[2445,2446,2447],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":167,"searchDepth":181,"depth":181,"links":2449},[2450,2451,2452,2453,2454,2455,2456,2457,2466],{"id":41,"depth":181,"text":42},{"id":79,"depth":181,"text":80},{"id":156,"depth":181,"text":157},{"id":1135,"depth":181,"text":1136},{"id":1638,"depth":181,"text":1639},{"id":1706,"depth":181,"text":1707},{"id":2316,"depth":181,"text":2317},{"id":2332,"depth":181,"text":2333,"children":2458},[2459,2461,2463,2465],{"id":2337,"depth":199,"text":2460},"Why not just use check=True and catch CalledProcessError?",{"id":2354,"depth":199,"text":2462},"Is shutil.which() a security check?",{"id":2370,"depth":199,"text":2464},"Should I pass text=True or handle bytes myself?",{"id":2391,"depth":199,"text":2392},{"id":2412,"depth":181,"text":2413},"2026-09-18","A reusable, safe subprocess.run helper for Python CLIs: argument lists, check=True, explicit encoding, timeouts, missing-program detection and clean errors.","beginner",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess",{"title":5,"description":2468},"cli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002Findex",[1068,2477,2478,2479],"security","errors","cli","juy0ySStqMQ1M2O6KyS5SRash0499FMxqVczLveRFks",[2482,2485,2488,2491,2494,2497,2500,2503,2506,2509,2512,2515,2518,2521,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2560,2563,2566,2569,2572,2575,2578,2581,2584,2587,2590,2593,2596,2599,2602,2605,2608,2611,2614,2617,2620,2623,2626,2629,2632,2635,2638,2641,2644,2647,2650,2653,2656,2659,2662,2665,2668,2671,2674,2677,2680,2683,2686,2689,2692,2695,2698,2701,2704,2707,2710,2713,2716,2719,2722,2723,2726,2729,2732,2735,2738,2741,2744,2747,2750,2753,2756,2759,2762,2765,2768,2771,2774,2777,2780,2783,2786,2789,2792,2795,2798,2801,2804,2807,2810,2813,2816,2819,2822,2825,2828,2831,2834,2837,2840,2843,2846,2849,2852,2855,2858,2861,2864,2867,2870,2873,2876,2879,2882,2885,2888,2891,2894,2897,2900,2903,2906,2909,2912,2915,2918,2921,2924,2927,2930,2933,2936,2939,2942,2945,2948,2951,2954,2957,2960,2963,2966,2969,2972,2975,2978,2981,2984,2987,2990,2993,2996,2999,3002,3005,3008,3011,3014,3017,3020,3023,3026],{"path":2483,"title":2484},"\u002Fabout","About Python CLI Toolcraft",{"path":2486,"title":2487},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2489,"title":2490},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2492,"title":2493},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2495,"title":2496},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2498,"title":2499},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2501,"title":2502},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":2504,"title":2505},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":2507,"title":2508},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2510,"title":2511},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2513,"title":2514},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":2516,"title":2517},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":2519,"title":2520},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2522,"title":2523},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2525,"title":2526},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2528,"title":2529},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2531,"title":2532},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2534,"title":2535},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":2537,"title":2538},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2540,"title":2541},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":2543,"title":2544},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2546,"title":2547},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":2549,"title":2550},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2552,"title":2553},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2555,"title":2556},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2558,"title":2559},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":2561,"title":2562},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2564,"title":2565},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":2567,"title":2568},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2570,"title":2571},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":2573,"title":2574},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2576,"title":2577},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2579,"title":2580},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2582,"title":2583},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":2585,"title":2586},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2588,"title":2589},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2591,"title":2592},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2594,"title":2595},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2597,"title":2598},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2600,"title":2601},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2603,"title":2604},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":2606,"title":2607},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2609,"title":2610},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":2612,"title":2613},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2615,"title":2616},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2618,"title":2619},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2621,"title":2622},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2624,"title":2625},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2627,"title":2628},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2630,"title":2631},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2633,"title":2634},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2636,"title":2637},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2639,"title":2640},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2642,"title":2643},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2645,"title":2646},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2648,"title":2649},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2651,"title":2652},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2654,"title":2655},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2657,"title":2658},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2660,"title":2661},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2663,"title":2664},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2666,"title":2667},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2669,"title":2670},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2672,"title":2673},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2675,"title":2676},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2678,"title":2679},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2681,"title":2682},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2684,"title":2685},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2687,"title":2688},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2690,"title":2691},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2693,"title":2694},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2696,"title":2697},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2699,"title":2700},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2702,"title":2703},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2705,"title":2706},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2708,"title":2709},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2711,"title":2712},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2714,"title":2715},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2717,"title":2718},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2720,"title":2721},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2473,"title":5},{"path":2724,"title":2725},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2727,"title":2728},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2730,"title":2731},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2733,"title":2734},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2736,"title":2737},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2739,"title":2740},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2742,"title":2743},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2745,"title":2746},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2748,"title":2749},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2751,"title":2752},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2754,"title":2755},"\u002F","Python CLI Toolcraft",{"path":2757,"title":2758},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2760,"title":2761},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2763,"title":2764},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2766,"title":2767},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2769,"title":2770},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2772,"title":2773},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2775,"title":2776},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2778,"title":2779},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2781,"title":2782},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2784,"title":2785},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2787,"title":2788},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2790,"title":2791},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2793,"title":2794},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2796,"title":2797},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2799,"title":2800},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2802,"title":2803},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2805,"title":2806},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2808,"title":2809},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2811,"title":2812},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2814,"title":2815},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2817,"title":2818},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2820,"title":2821},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":2823,"title":2824},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":2826,"title":2827},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2829,"title":2830},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":2832,"title":2833},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2835,"title":2836},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2838,"title":2839},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2841,"title":2842},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2844,"title":2845},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2847,"title":2848},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2850,"title":2851},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":2853,"title":2854},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2856,"title":2857},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2859,"title":2860},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2862,"title":2863},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2865,"title":2866},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":2868,"title":2869},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2871,"title":2872},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":2874,"title":2875},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2877,"title":2878},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2880,"title":2881},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2883,"title":2884},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2886,"title":2887},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2889,"title":2890},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2892,"title":2893},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2895,"title":2896},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2898,"title":2899},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2901,"title":2902},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2904,"title":2905},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2907,"title":2908},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2910,"title":2911},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2913,"title":2914},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2916,"title":2917},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2919,"title":2920},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2922,"title":2923},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2925,"title":2926},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2928,"title":2929},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":2931,"title":2932},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2934,"title":2935},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2937,"title":2938},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2940,"title":2941},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2943,"title":2944},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2946,"title":2947},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2949,"title":2950},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2952,"title":2953},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2955,"title":2956},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2958,"title":2959},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2961,"title":2962},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2964,"title":2965},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2967,"title":2968},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2970,"title":2971},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2973,"title":2974},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2976,"title":2977},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2979,"title":2980},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2982,"title":2983},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2985,"title":2986},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2988,"title":2989},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2991,"title":2992},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2994,"title":2995},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":2997,"title":2998},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":3000,"title":3001},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":3003,"title":3004},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3006,"title":3007},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":3009,"title":3010},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":3012,"title":3013},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":3015,"title":3016},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3018,"title":3019},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3021,"title":3022},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3024,"title":3025},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3027,"title":3028},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905050]