[{"data":1,"prerenderedAt":2151},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F":3,"content-directory":1905},{"id":4,"title":5,"body":6,"date":1890,"description":1891,"difficulty":1892,"draft":1893,"extension":1894,"meta":1895,"navigation":258,"path":1896,"seo":1897,"stem":1898,"tags":1899,"updated":1903,"__hash__":1904},"content\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Findex.md","Shell Completion for Python CLIs",{"type":7,"value":8,"toc":1868},"minimark",[9,13,18,68,72,76,91,102,116,120,123,156,159,206,218,222,225,228,231,393,428,431,578,610,618,622,633,636,639,783,821,825,828,861,867,871,922,926,929,934,958,965,971,1053,1059,1081,1091,1095,1106,1218,1227,1230,1239,1245,1251,1255,1258,1497,1500,1504,1507,1737,1744,1750,1754,1759,1769,1773,1780,1784,1790,1794,1800,1804,1823,1827,1830,1834,1864],[10,11,12],"p",{},"Tab completion is the fastest quality-of-life upgrade you can ship for a command-line tool: press Tab and the shell fills in your subcommands, options, and even valid values for an argument. Done well it also teaches the interface — users discover commands without ever opening the docs. This overview explains how completion actually works, what Click and Typer give you out of the box, and how the two deeper guides fit together so you can turn it on and install it on every shell your users run.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,46,57],"ul",{},[22,23,24,25,29],"li",{},"Completion is a handshake: when the user presses Tab, the shell re-invokes ",[26,27,28],"em",{},"your"," program in a special completion mode, your program prints candidate strings, and the shell displays them.",[22,31,32,36,37,41,42,45],{},[33,34,35],"strong",{},"Typer"," has it built in — ",[38,39,40],"code",{},"--install-completion"," writes the shell script for you. ",[33,43,44],{},"Click 8"," has the same engine underneath but you wire the install step yourself.",[22,47,48,49,52,53,56],{},"Completions can be ",[33,50,51],{},"static"," (the fixed set of subcommands and choices, computed for free from your command tree) or ",[33,54,55],{},"dynamic"," (values pulled at Tab-time from a file, an API, or the current arguments).",[22,58,59,60,63,64,67],{},"Two moving parts: ",[26,61,62],{},"enabling"," completion in your Python code, and ",[26,65,66],{},"installing"," the generated script into bash, zsh, or fish. They fail independently, so this section splits them into two guides.",[69,70],"inline-diagram",{"name":71},"shell-completion-flow",[14,73,75],{"id":74},"what-completion-is-and-why-it-matters","What completion is and why it matters",[10,77,78,79,82,83,86,87,90],{},"When you type ",[38,80,81],{},"git com"," and press Tab and it becomes ",[38,84,85],{},"git commit",", the shell did not read a static list of git's subcommands from a config file. It ran a small piece of shell code — a ",[26,88,89],{},"completion function"," — that git registered when your shell started. For a Python CLI the goal is the same: register a completion function that knows your commands, options, and arguments, and keep it in sync with your code automatically instead of by hand.",[10,92,93,94,97,98,101],{},"The payoff is concrete. Users stop mistyping subcommands, stop guessing flag names, and stop grepping ",[38,95,96],{},"--help"," for the option that takes an environment name. For a tool with dynamic inputs — deploy targets, dataset IDs, profile names — completion can suggest the ",[26,99,100],{},"actual"," valid values, which is the difference between a CLI that feels alive and one that feels like a form you have to fill out perfectly on the first try.",[10,103,104,105,110,111,115],{},"Completion also composes with the rest of a good CLI. It works best on a clean command tree (see ",[106,107,109],"a",{"href":108},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","structuring multi-command Python CLIs",") and it pairs naturally with a polished ",[106,112,114],{"href":113},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","interactive terminal UI",": completion helps users assemble the command, Rich makes the result readable.",[14,117,119],{"id":118},"the-completion-handshake","The completion handshake",[10,121,122],{},"Here is the mechanism every framework builds on. Nothing about it is Python-specific.",[124,125,126,133,139,150,153],"ol",{},[22,127,128,129,132],{},"When your shell starts, it sources a small script that registers a completion function for your command name — for example ",[38,130,131],{},"yourcli",".",[22,134,135,136,132],{},"The user types ",[38,137,138],{},"yourcli deploy --env \u003CTab>",[22,140,141,142,145,146,149],{},"The registered function re-runs your program with an environment variable set (Click and Typer use ",[38,143,144],{},"_YOURCLI_COMPLETE",") and the current words passed in. This is ",[26,147,148],{},"completion mode",": your program does not do its real work, it prints candidates.",[22,151,152],{},"Your program prints one candidate per line (plus a type marker) and exits.",[22,154,155],{},"The shell reads that list and either fills in the single match or shows the menu.",[10,157,158],{},"You can watch the handshake happen by hand, which is the single most useful debugging trick in this whole area:",[160,161,166],"pre",{"className":162,"code":163,"language":164,"meta":165,"style":165},"language-bash shiki shiki-themes github-light github-dark","$ _YOURCLI_COMPLETE=bash_complete COMP_WORDS=\"yourcli dep\" COMP_CWORD=1 yourcli\nplain,deploy\nplain,describe\n","bash","",[38,167,168,194,200],{"__ignoreMap":165},[169,170,173,177,181,184,187,191],"span",{"class":171,"line":172},"line",1,[169,174,176],{"class":175},"sScJk","$",[169,178,180],{"class":179},"sZZnC"," _YOURCLI_COMPLETE=bash_complete",[169,182,183],{"class":179}," COMP_WORDS=\"yourcli dep\"",[169,185,186],{"class":179}," COMP_CWORD=",[169,188,190],{"class":189},"sj4cs","1",[169,192,193],{"class":179}," yourcli\n",[169,195,197],{"class":171,"line":196},2,[169,198,199],{"class":175},"plain,deploy\n",[169,201,203],{"class":171,"line":202},3,[169,204,205],{"class":175},"plain,describe\n",[10,207,208,209,213,214,132],{},"That is Click's completion protocol running directly — no shell involved. If this prints your candidates, your Python side works and any problem is in the install step. If it prints nothing, the bug is in your code. Keeping those two failure modes separate is exactly why this section is split into an ",[106,210,212],{"href":211},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer\u002F","enabling guide"," and an ",[106,215,217],{"href":216},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish\u002F","installing guide",[14,219,221],{"id":220},"click-vs-typer-support-at-a-glance","Click vs Typer support at a glance",[10,223,224],{},"Both frameworks share the same completion engine — Typer is built on Click — so the underlying protocol is identical. What differs is how much is handed to you.",[69,226],{"name":227},"completion-support-matrix",[10,229,230],{},"Typer wires up an install command for free. Every Typer app automatically gains two hidden options:",[160,232,236],{"className":233,"code":234,"language":235,"meta":165,"style":165},"language-python shiki shiki-themes github-light github-dark","# app.py\nimport typer\n\napp = typer.Typer()\n\n@app.command()\ndef deploy(env: str, replicas: int = 1) -> None:\n    \"\"\"Deploy the service.\"\"\"\n    typer.echo(f\"Deploying to {env} with {replicas} replicas\")\n\nif __name__ == \"__main__\":\n    app()\n","python",[38,237,238,244,254,260,272,277,286,322,328,365,370,387],{"__ignoreMap":165},[169,239,240],{"class":171,"line":172},[169,241,243],{"class":242},"sJ8bj","# app.py\n",[169,245,246,250],{"class":171,"line":196},[169,247,249],{"class":248},"szBVR","import",[169,251,253],{"class":252},"sVt8B"," typer\n",[169,255,256],{"class":171,"line":202},[169,257,259],{"emptyLinePlaceholder":258},true,"\n",[169,261,263,266,269],{"class":171,"line":262},4,[169,264,265],{"class":252},"app ",[169,267,268],{"class":248},"=",[169,270,271],{"class":252}," typer.Typer()\n",[169,273,275],{"class":171,"line":274},5,[169,276,259],{"emptyLinePlaceholder":258},[169,278,280,283],{"class":171,"line":279},6,[169,281,282],{"class":175},"@app.command",[169,284,285],{"class":252},"()\n",[169,287,289,292,295,298,301,304,307,310,313,316,319],{"class":171,"line":288},7,[169,290,291],{"class":248},"def",[169,293,294],{"class":175}," deploy",[169,296,297],{"class":252},"(env: ",[169,299,300],{"class":189},"str",[169,302,303],{"class":252},", replicas: ",[169,305,306],{"class":189},"int",[169,308,309],{"class":248}," =",[169,311,312],{"class":189}," 1",[169,314,315],{"class":252},") -> ",[169,317,318],{"class":189},"None",[169,320,321],{"class":252},":\n",[169,323,325],{"class":171,"line":324},8,[169,326,327],{"class":179},"    \"\"\"Deploy the service.\"\"\"\n",[169,329,331,334,337,340,343,346,349,352,354,357,359,362],{"class":171,"line":330},9,[169,332,333],{"class":252},"    typer.echo(",[169,335,336],{"class":248},"f",[169,338,339],{"class":179},"\"Deploying to ",[169,341,342],{"class":189},"{",[169,344,345],{"class":252},"env",[169,347,348],{"class":189},"}",[169,350,351],{"class":179}," with ",[169,353,342],{"class":189},[169,355,356],{"class":252},"replicas",[169,358,348],{"class":189},[169,360,361],{"class":179}," replicas\"",[169,363,364],{"class":252},")\n",[169,366,368],{"class":171,"line":367},10,[169,369,259],{"emptyLinePlaceholder":258},[169,371,373,376,379,382,385],{"class":171,"line":372},11,[169,374,375],{"class":248},"if",[169,377,378],{"class":189}," __name__",[169,380,381],{"class":248}," ==",[169,383,384],{"class":179}," \"__main__\"",[169,386,321],{"class":252},[169,388,390],{"class":171,"line":389},12,[169,391,392],{"class":252},"    app()\n",[160,394,396],{"className":162,"code":395,"language":164,"meta":165,"style":165},"$ python app.py --install-completion   # detects your shell and installs\n$ python app.py --show-completion       # prints the script to stdout instead\n",[38,397,398,414],{"__ignoreMap":165},[169,399,400,402,405,408,411],{"class":171,"line":172},[169,401,176],{"class":175},[169,403,404],{"class":179}," python",[169,406,407],{"class":179}," app.py",[169,409,410],{"class":189}," --install-completion",[169,412,413],{"class":242},"   # detects your shell and installs\n",[169,415,416,418,420,422,425],{"class":171,"line":196},[169,417,176],{"class":175},[169,419,404],{"class":179},[169,421,407],{"class":179},[169,423,424],{"class":189}," --show-completion",[169,426,427],{"class":242},"       # prints the script to stdout instead\n",[10,429,430],{},"Click has the same machinery but leaves the install step to you — you tell users to eval or source a generated script:",[160,432,434],{"className":233,"code":433,"language":235,"meta":165,"style":165},"# cli.py\nimport click\n\n@click.group()\ndef cli() -> None:\n    \"\"\"Example tool.\"\"\"\n\n@cli.command()\n@click.option(\"--env\", required=True)\ndef deploy(env: str) -> None:\n    \"\"\"Deploy the service.\"\"\"\n    click.echo(f\"Deploying to {env}\")\n\nif __name__ == \"__main__\":\n    cli()\n",[38,435,436,441,448,452,459,473,478,482,489,514,530,534,554,559,572],{"__ignoreMap":165},[169,437,438],{"class":171,"line":172},[169,439,440],{"class":242},"# cli.py\n",[169,442,443,445],{"class":171,"line":196},[169,444,249],{"class":248},[169,446,447],{"class":252}," click\n",[169,449,450],{"class":171,"line":202},[169,451,259],{"emptyLinePlaceholder":258},[169,453,454,457],{"class":171,"line":262},[169,455,456],{"class":175},"@click.group",[169,458,285],{"class":252},[169,460,461,463,466,469,471],{"class":171,"line":274},[169,462,291],{"class":248},[169,464,465],{"class":175}," cli",[169,467,468],{"class":252},"() -> ",[169,470,318],{"class":189},[169,472,321],{"class":252},[169,474,475],{"class":171,"line":279},[169,476,477],{"class":179},"    \"\"\"Example tool.\"\"\"\n",[169,479,480],{"class":171,"line":288},[169,481,259],{"emptyLinePlaceholder":258},[169,483,484,487],{"class":171,"line":324},[169,485,486],{"class":175},"@cli.command",[169,488,285],{"class":252},[169,490,491,494,497,500,503,507,509,512],{"class":171,"line":330},[169,492,493],{"class":175},"@click.option",[169,495,496],{"class":252},"(",[169,498,499],{"class":179},"\"--env\"",[169,501,502],{"class":252},", ",[169,504,506],{"class":505},"s4XuR","required",[169,508,268],{"class":248},[169,510,511],{"class":189},"True",[169,513,364],{"class":252},[169,515,516,518,520,522,524,526,528],{"class":171,"line":367},[169,517,291],{"class":248},[169,519,294],{"class":175},[169,521,297],{"class":252},[169,523,300],{"class":189},[169,525,315],{"class":252},[169,527,318],{"class":189},[169,529,321],{"class":252},[169,531,532],{"class":171,"line":372},[169,533,327],{"class":179},[169,535,536,539,541,543,545,547,549,552],{"class":171,"line":389},[169,537,538],{"class":252},"    click.echo(",[169,540,336],{"class":248},[169,542,339],{"class":179},[169,544,342],{"class":189},[169,546,345],{"class":252},[169,548,348],{"class":189},[169,550,551],{"class":179},"\"",[169,553,364],{"class":252},[169,555,557],{"class":171,"line":556},13,[169,558,259],{"emptyLinePlaceholder":258},[169,560,562,564,566,568,570],{"class":171,"line":561},14,[169,563,375],{"class":248},[169,565,378],{"class":189},[169,567,381],{"class":248},[169,569,384],{"class":179},[169,571,321],{"class":252},[169,573,575],{"class":171,"line":574},15,[169,576,577],{"class":252},"    cli()\n",[160,579,581],{"className":162,"code":580,"language":164,"meta":165,"style":165},"$ eval \"$(_CLI_COMPLETE=bash_source cli)\"   # activate for the current shell\n",[38,582,583],{"__ignoreMap":165},[169,584,585,587,590,593,596,598,601,604,607],{"class":171,"line":172},[169,586,176],{"class":175},[169,588,589],{"class":179}," eval",[169,591,592],{"class":179}," \"$(",[169,594,595],{"class":252},"_CLI_COMPLETE",[169,597,268],{"class":248},[169,599,600],{"class":179},"bash_source ",[169,602,603],{"class":175},"cli",[169,605,606],{"class":179},")\"",[169,608,609],{"class":242},"   # activate for the current shell\n",[10,611,612,613,617],{},"The trade-off between these two frameworks — convenience versus control — is the same one that runs through ",[106,614,616],{"href":615},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click: when to use each",". For completion specifically, Typer saves you writing an install command, and Click gives you a script you can drop into a package's post-install step.",[14,619,621],{"id":620},"static-vs-dynamic-completions","Static vs dynamic completions",[10,623,624,625,628,629,632],{},"Static completions are the ones your framework can compute from the command tree with no help: the names of subcommands, the option flags, and any ",[38,626,627],{},"click.Choice"," \u002F ",[38,630,631],{},"Enum"," values. You get these the moment completion is enabled — they cost nothing.",[69,634],{"name":635},"completion-static-vs-dynamic",[10,637,638],{},"Dynamic completions are computed at Tab-time by a callback you write. Use them when the valid values live outside your source code:",[160,640,642],{"className":233,"code":641,"language":235,"meta":165,"style":165},"import click\n\ndef complete_env(ctx, param, incomplete):\n    # In real code, read from a config file or API.\n    known = [\"staging\", \"prod-eu\", \"prod-us\"]\n    return [e for e in known if e.startswith(incomplete)]\n\n@click.command()\n@click.option(\"--env\", shell_complete=complete_env)\ndef deploy(env: str) -> None:\n    click.echo(f\"Deploying to {env}\")\n",[38,643,644,650,654,664,669,695,720,724,731,749,765],{"__ignoreMap":165},[169,645,646,648],{"class":171,"line":172},[169,647,249],{"class":248},[169,649,447],{"class":252},[169,651,652],{"class":171,"line":196},[169,653,259],{"emptyLinePlaceholder":258},[169,655,656,658,661],{"class":171,"line":202},[169,657,291],{"class":248},[169,659,660],{"class":175}," complete_env",[169,662,663],{"class":252},"(ctx, param, incomplete):\n",[169,665,666],{"class":171,"line":262},[169,667,668],{"class":242},"    # In real code, read from a config file or API.\n",[169,670,671,674,676,679,682,684,687,689,692],{"class":171,"line":274},[169,672,673],{"class":252},"    known ",[169,675,268],{"class":248},[169,677,678],{"class":252}," [",[169,680,681],{"class":179},"\"staging\"",[169,683,502],{"class":252},[169,685,686],{"class":179},"\"prod-eu\"",[169,688,502],{"class":252},[169,690,691],{"class":179},"\"prod-us\"",[169,693,694],{"class":252},"]\n",[169,696,697,700,703,706,709,712,715,717],{"class":171,"line":279},[169,698,699],{"class":248},"    return",[169,701,702],{"class":252}," [e ",[169,704,705],{"class":248},"for",[169,707,708],{"class":252}," e ",[169,710,711],{"class":248},"in",[169,713,714],{"class":252}," known ",[169,716,375],{"class":248},[169,718,719],{"class":252}," e.startswith(incomplete)]\n",[169,721,722],{"class":171,"line":288},[169,723,259],{"emptyLinePlaceholder":258},[169,725,726,729],{"class":171,"line":324},[169,727,728],{"class":175},"@click.command",[169,730,285],{"class":252},[169,732,733,735,737,739,741,744,746],{"class":171,"line":330},[169,734,493],{"class":175},[169,736,496],{"class":252},[169,738,499],{"class":179},[169,740,502],{"class":252},[169,742,743],{"class":505},"shell_complete",[169,745,268],{"class":248},[169,747,748],{"class":252},"complete_env)\n",[169,750,751,753,755,757,759,761,763],{"class":171,"line":367},[169,752,291],{"class":248},[169,754,294],{"class":175},[169,756,297],{"class":252},[169,758,300],{"class":189},[169,760,315],{"class":252},[169,762,318],{"class":189},[169,764,321],{"class":252},[169,766,767,769,771,773,775,777,779,781],{"class":171,"line":372},[169,768,538],{"class":252},[169,770,336],{"class":248},[169,772,339],{"class":179},[169,774,342],{"class":189},[169,776,345],{"class":252},[169,778,348],{"class":189},[169,780,551],{"class":179},[169,782,364],{"class":252},[10,784,785,786,789,790,793,794,797,798,801,802,805,806,809,810,812,813,816,817,820],{},"Now ",[38,787,788],{},"yourcli deploy --env pro\u003CTab>"," offers ",[38,791,792],{},"prod-eu"," and ",[38,795,796],{},"prod-us",". The callback receives the partial word (",[38,799,800],{},"incomplete",") so you can filter server-side and keep the list short. Two rules keep dynamic completion pleasant: make the callback ",[33,803,804],{},"fast"," (it runs on every keypress-plus-Tab, so cache or bound any network call), and make it ",[33,807,808],{},"safe to fail"," (return an empty list rather than raising, so a broken suggestion never blocks the user's shell). The ",[106,811,212],{"href":211}," covers the equivalent ",[38,814,815],{},"autocompletion"," hook in Typer and how to return richer ",[38,818,819],{},"CompletionItem"," values with help text.",[14,822,824],{"id":823},"the-two-guides-in-this-section","The two guides in this section",[10,826,827],{},"Completion has two independent jobs, and this section gives each its own guide:",[19,829,830,849],{},[22,831,832,837,838,841,842,845,846,848],{},[33,833,834],{},[106,835,836],{"href":211},"Enabling tab completion in Click and Typer"," — the Python side. Turn completion on, add dynamic completions for arguments and options with Click's ",[38,839,840],{},"shell_complete="," and Typer's ",[38,843,844],{},"autocompletion=",", complete choices and enums automatically, and understand the ",[38,847,144],{}," trigger.",[22,850,851,856,857,860],{},[33,852,853],{},[106,854,855],{"href":216},"Installing shell completion for bash, zsh, fish"," — the shell side. Generate the completion script per shell, put it where each shell will load it, and troubleshoot completions that never fire (rehash, a fresh shell, ",[38,858,859],{},"PATH",", and caching).",[10,862,863,864,866],{},"Read them in that order: get the handshake working in Python first (verify with the ",[38,865,144],{}," trick above), then install the script so it runs automatically.",[14,868,870],{"id":869},"production-notes","Production notes",[19,872,873,883,892,911],{},[22,874,875,878,879,132],{},[33,876,877],{},"Completion runs your import path on every Tab."," A slow startup makes completion feel laggy, so keep top-level imports light — the same discipline as ",[106,880,882],{"href":881},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI startup performance and lazy loading",[22,884,885,888,889,891],{},[33,886,887],{},"Never print to stdout during completion mode."," Anything your program writes to stdout while ",[38,890,144],{}," is set is parsed as a candidate. Route stray output to stderr or guard it.",[22,893,894,897,898,900,901,903,904,628,907,910],{},[33,895,896],{},"Pin your framework."," The ",[38,899,743],{}," API landed in Click 8.0 and replaced the older ",[38,902,815],{}," argument; Typer's install flags stabilised around 0.12. Pin ",[38,905,906],{},"click>=8.1",[38,908,909],{},"typer>=0.12"," and test against the version you ship.",[22,912,913,916,917,921],{},[33,914,915],{},"The installed name matters."," The completion script keys off your console-script name, so it must match your ",[106,918,920],{"href":919},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","entry point",". Rename the command and you must regenerate the script.",[14,923,925],{"id":924},"turning-completion-on-per-framework","Turning completion on, per framework",[10,927,928],{},"The mechanics differ in spelling more than substance.",[10,930,931,933],{},[33,932,35],{}," ships the installer:",[160,935,937],{"className":162,"code":936,"language":164,"meta":165,"style":165},"mytool --install-completion          # writes the script and the profile line\nmytool --show-completion             # prints it instead, if you would rather install it yourself\n",[38,938,939,949],{"__ignoreMap":165},[169,940,941,944,946],{"class":171,"line":172},[169,942,943],{"class":175},"mytool",[169,945,410],{"class":189},[169,947,948],{"class":242},"          # writes the script and the profile line\n",[169,950,951,953,955],{"class":171,"line":196},[169,952,943],{"class":175},[169,954,424],{"class":189},[169,956,957],{"class":242},"             # prints it instead, if you would rather install it yourself\n",[10,959,960,961,964],{},"The generated script is written into the user's shell configuration directory and a line is\nappended to their profile. The step people miss is that a ",[33,962,963],{},"new shell"," is required — the profile\nonly runs at start-up, so nothing happens in the terminal where the command was typed.",[10,966,967,970],{},[33,968,969],{},"Click"," expects you to install it, which is a one-line instruction in your README:",[160,972,974],{"className":162,"code":973,"language":164,"meta":165,"style":165},"# bash — in ~\u002F.bashrc\neval \"$(_MYTOOL_COMPLETE=bash_source mytool)\"\n\n# zsh — in ~\u002F.zshrc\neval \"$(_MYTOOL_COMPLETE=zsh_source mytool)\"\n\n# fish — in ~\u002F.config\u002Ffish\u002Fcompletions\u002Fmytool.fish\n_MYTOOL_COMPLETE=fish_source mytool | source\n",[38,975,976,981,1000,1004,1009,1026,1030,1035],{"__ignoreMap":165},[169,977,978],{"class":171,"line":172},[169,979,980],{"class":242},"# bash — in ~\u002F.bashrc\n",[169,982,983,986,988,991,993,995,997],{"class":171,"line":196},[169,984,985],{"class":189},"eval",[169,987,592],{"class":179},[169,989,990],{"class":252},"_MYTOOL_COMPLETE",[169,992,268],{"class":248},[169,994,600],{"class":179},[169,996,943],{"class":175},[169,998,999],{"class":179},")\"\n",[169,1001,1002],{"class":171,"line":202},[169,1003,259],{"emptyLinePlaceholder":258},[169,1005,1006],{"class":171,"line":262},[169,1007,1008],{"class":242},"# zsh — in ~\u002F.zshrc\n",[169,1010,1011,1013,1015,1017,1019,1022,1024],{"class":171,"line":274},[169,1012,985],{"class":189},[169,1014,592],{"class":179},[169,1016,990],{"class":252},[169,1018,268],{"class":248},[169,1020,1021],{"class":179},"zsh_source ",[169,1023,943],{"class":175},[169,1025,999],{"class":179},[169,1027,1028],{"class":171,"line":279},[169,1029,259],{"emptyLinePlaceholder":258},[169,1031,1032],{"class":171,"line":288},[169,1033,1034],{"class":242},"# fish — in ~\u002F.config\u002Ffish\u002Fcompletions\u002Fmytool.fish\n",[169,1036,1037,1039,1041,1044,1047,1050],{"class":171,"line":324},[169,1038,990],{"class":252},[169,1040,268],{"class":248},[169,1042,1043],{"class":179},"fish_source",[169,1045,1046],{"class":175}," mytool",[169,1048,1049],{"class":248}," |",[169,1051,1052],{"class":189}," source\n",[10,1054,1055,1056,1058],{},"The variable name is derived from your command name, uppercased with hyphens replaced by\nunderscores. Note that ",[38,1057,985],{}," runs your program on every shell start-up, which for a slow-starting\nCLI is a real cost — generating the script into a file once is the better arrangement:",[160,1060,1062],{"className":162,"code":1061,"language":164,"meta":165,"style":165},"_MYTOOL_COMPLETE=bash_source mytool > ~\u002F.local\u002Fshare\u002Fbash-completion\u002Fcompletions\u002Fmytool\n",[38,1063,1064],{"__ignoreMap":165},[169,1065,1066,1068,1070,1073,1075,1078],{"class":171,"line":172},[169,1067,990],{"class":252},[169,1069,268],{"class":248},[169,1071,1072],{"class":179},"bash_source",[169,1074,1046],{"class":175},[169,1076,1077],{"class":248}," >",[169,1079,1080],{"class":179}," ~\u002F.local\u002Fshare\u002Fbash-completion\u002Fcompletions\u002Fmytool\n",[10,1082,1083,1086,1087,1090],{},[33,1084,1085],{},"argparse"," has no built-in support. ",[38,1088,1089],{},"argcomplete"," fills the gap with a decorator and a\nregistration step, at the cost of the dependency you were probably avoiding by choosing argparse\nin the first place.",[14,1092,1094],{"id":1093},"dynamic-completion-that-stays-fast","Dynamic completion that stays fast",[10,1096,1097,1098,1101,1102,1105],{},"Static completion — command names, enum values, ",[38,1099,1100],{},"Path"," types — is free and costs nothing at run\ntime. Dynamic completion runs ",[26,1103,1104],{},"your code"," inside the user's shell prompt, which changes the\nconstraints entirely.",[160,1107,1109],{"className":233,"code":1108,"language":235,"meta":165,"style":165},"def complete_environment(incomplete: str) -> list[str]:\n    \"\"\"Called on every Tab. Must be fast and must never print to stdout.\"\"\"\n    known = cached_environment_names()          # read a local cache, not the network\n    return [name for name in known if name.startswith(incomplete)]\n\n@app.command()\ndef deploy(\n    env: Annotated[str, typer.Option(autocompletion=complete_environment)],\n) -> None:\n    ...\n",[38,1110,1111,1131,1136,1148,1169,1173,1179,1188,1205,1213],{"__ignoreMap":165},[169,1112,1113,1115,1118,1121,1123,1126,1128],{"class":171,"line":172},[169,1114,291],{"class":248},[169,1116,1117],{"class":175}," complete_environment",[169,1119,1120],{"class":252},"(incomplete: ",[169,1122,300],{"class":189},[169,1124,1125],{"class":252},") -> list[",[169,1127,300],{"class":189},[169,1129,1130],{"class":252},"]:\n",[169,1132,1133],{"class":171,"line":196},[169,1134,1135],{"class":179},"    \"\"\"Called on every Tab. Must be fast and must never print to stdout.\"\"\"\n",[169,1137,1138,1140,1142,1145],{"class":171,"line":202},[169,1139,673],{"class":252},[169,1141,268],{"class":248},[169,1143,1144],{"class":252}," cached_environment_names()          ",[169,1146,1147],{"class":242},"# read a local cache, not the network\n",[169,1149,1150,1152,1155,1157,1160,1162,1164,1166],{"class":171,"line":262},[169,1151,699],{"class":248},[169,1153,1154],{"class":252}," [name ",[169,1156,705],{"class":248},[169,1158,1159],{"class":252}," name ",[169,1161,711],{"class":248},[169,1163,714],{"class":252},[169,1165,375],{"class":248},[169,1167,1168],{"class":252}," name.startswith(incomplete)]\n",[169,1170,1171],{"class":171,"line":274},[169,1172,259],{"emptyLinePlaceholder":258},[169,1174,1175,1177],{"class":171,"line":279},[169,1176,282],{"class":175},[169,1178,285],{"class":252},[169,1180,1181,1183,1185],{"class":171,"line":288},[169,1182,291],{"class":248},[169,1184,294],{"class":175},[169,1186,1187],{"class":252},"(\n",[169,1189,1190,1193,1195,1198,1200,1202],{"class":171,"line":324},[169,1191,1192],{"class":252},"    env: Annotated[",[169,1194,300],{"class":189},[169,1196,1197],{"class":252},", typer.Option(",[169,1199,815],{"class":505},[169,1201,268],{"class":248},[169,1203,1204],{"class":252},"complete_environment)],\n",[169,1206,1207,1209,1211],{"class":171,"line":330},[169,1208,315],{"class":252},[169,1210,318],{"class":189},[169,1212,321],{"class":252},[169,1214,1215],{"class":171,"line":367},[169,1216,1217],{"class":189},"    ...\n",[10,1219,1220,1221,1223,1224,1226],{},"The Click spelling is ",[38,1222,840],{},", with a callback that receives the context, the parameter\nand the incomplete string, and returns ",[38,1225,819],{}," objects — which can carry a help string\nthat zsh and fish will display beside each candidate.",[10,1228,1229],{},"Three rules make the difference between completion that people use and completion they turn off:",[10,1231,1232,1235,1236,132],{},[33,1233,1234],{},"Budget about 100 ms, total."," That includes your interpreter start-up, which for a CLI with\nheavy imports may already exceed it. This is the single strongest practical argument for\n",[106,1237,1238],{"href":881},"keeping startup fast",[10,1240,1241,1244],{},[33,1242,1243],{},"Never touch the network."," A completion callback that makes an HTTP request makes Tab feel\nbroken on a flaky connection and hangs the prompt on a bad one. Read from a cache the tool refreshes\nduring normal commands, and accept that it may be slightly stale.",[10,1246,1247,1250],{},[33,1248,1249],{},"Never print."," Anything your callback writes to stdout is interpreted as completion candidates.\nA stray debug print produces garbage suggestions that are extremely confusing to diagnose.",[14,1252,1254],{"id":1253},"testing-completion-without-a-shell","Testing completion without a shell",[10,1256,1257],{},"An interactive shell is not reproducible in CI, so drive the completion machinery directly. Both\nframeworks respond to the same environment protocol they use in production:",[160,1259,1261],{"className":233,"code":1260,"language":235,"meta":165,"style":165},"import subprocess, sys\n\ndef complete(words: str, cword: int) -> list[str]:\n    result = subprocess.run(\n        [sys.executable, \"-m\", \"mytool\"],\n        env={\n            **os.environ,\n            \"_MYTOOL_COMPLETE\": \"bash_complete\",\n            \"COMP_WORDS\": words,\n            \"COMP_CWORD\": str(cword),\n        },\n        capture_output=True, text=True,\n    )\n    return [line.split(\",\", 1)[1] for line in result.stdout.splitlines() if \",\" in line]\n\ndef test_environment_completion_filters_by_prefix():\n    assert complete(\"mytool deploy --env pro\", 3) == [\"prod\", \"prod-eu\"]\n",[38,1262,1263,1270,1274,1297,1307,1323,1333,1341,1355,1363,1375,1380,1400,1405,1448,1452,1463],{"__ignoreMap":165},[169,1264,1265,1267],{"class":171,"line":172},[169,1266,249],{"class":248},[169,1268,1269],{"class":252}," subprocess, sys\n",[169,1271,1272],{"class":171,"line":196},[169,1273,259],{"emptyLinePlaceholder":258},[169,1275,1276,1278,1281,1284,1286,1289,1291,1293,1295],{"class":171,"line":202},[169,1277,291],{"class":248},[169,1279,1280],{"class":175}," complete",[169,1282,1283],{"class":252},"(words: ",[169,1285,300],{"class":189},[169,1287,1288],{"class":252},", cword: ",[169,1290,306],{"class":189},[169,1292,1125],{"class":252},[169,1294,300],{"class":189},[169,1296,1130],{"class":252},[169,1298,1299,1302,1304],{"class":171,"line":262},[169,1300,1301],{"class":252},"    result ",[169,1303,268],{"class":248},[169,1305,1306],{"class":252}," subprocess.run(\n",[169,1308,1309,1312,1315,1317,1320],{"class":171,"line":274},[169,1310,1311],{"class":252},"        [sys.executable, ",[169,1313,1314],{"class":179},"\"-m\"",[169,1316,502],{"class":252},[169,1318,1319],{"class":179},"\"mytool\"",[169,1321,1322],{"class":252},"],\n",[169,1324,1325,1328,1330],{"class":171,"line":279},[169,1326,1327],{"class":505},"        env",[169,1329,268],{"class":248},[169,1331,1332],{"class":252},"{\n",[169,1334,1335,1338],{"class":171,"line":288},[169,1336,1337],{"class":248},"            **",[169,1339,1340],{"class":252},"os.environ,\n",[169,1342,1343,1346,1349,1352],{"class":171,"line":324},[169,1344,1345],{"class":179},"            \"_MYTOOL_COMPLETE\"",[169,1347,1348],{"class":252},": ",[169,1350,1351],{"class":179},"\"bash_complete\"",[169,1353,1354],{"class":252},",\n",[169,1356,1357,1360],{"class":171,"line":330},[169,1358,1359],{"class":179},"            \"COMP_WORDS\"",[169,1361,1362],{"class":252},": words,\n",[169,1364,1365,1368,1370,1372],{"class":171,"line":367},[169,1366,1367],{"class":179},"            \"COMP_CWORD\"",[169,1369,1348],{"class":252},[169,1371,300],{"class":189},[169,1373,1374],{"class":252},"(cword),\n",[169,1376,1377],{"class":171,"line":372},[169,1378,1379],{"class":252},"        },\n",[169,1381,1382,1385,1387,1389,1391,1394,1396,1398],{"class":171,"line":389},[169,1383,1384],{"class":505},"        capture_output",[169,1386,268],{"class":248},[169,1388,511],{"class":189},[169,1390,502],{"class":252},[169,1392,1393],{"class":505},"text",[169,1395,268],{"class":248},[169,1397,511],{"class":189},[169,1399,1354],{"class":252},[169,1401,1402],{"class":171,"line":556},[169,1403,1404],{"class":252},"    )\n",[169,1406,1407,1409,1412,1415,1417,1419,1422,1424,1427,1429,1432,1434,1437,1439,1442,1445],{"class":171,"line":561},[169,1408,699],{"class":248},[169,1410,1411],{"class":252}," [line.split(",[169,1413,1414],{"class":179},"\",\"",[169,1416,502],{"class":252},[169,1418,190],{"class":189},[169,1420,1421],{"class":252},")[",[169,1423,190],{"class":189},[169,1425,1426],{"class":252},"] ",[169,1428,705],{"class":248},[169,1430,1431],{"class":252}," line ",[169,1433,711],{"class":248},[169,1435,1436],{"class":252}," result.stdout.splitlines() ",[169,1438,375],{"class":248},[169,1440,1441],{"class":179}," \",\"",[169,1443,1444],{"class":248}," in",[169,1446,1447],{"class":252}," line]\n",[169,1449,1450],{"class":171,"line":574},[169,1451,259],{"emptyLinePlaceholder":258},[169,1453,1455,1457,1460],{"class":171,"line":1454},16,[169,1456,291],{"class":248},[169,1458,1459],{"class":175}," test_environment_completion_filters_by_prefix",[169,1461,1462],{"class":252},"():\n",[169,1464,1466,1469,1472,1475,1477,1480,1483,1486,1488,1491,1493,1495],{"class":171,"line":1465},17,[169,1467,1468],{"class":248},"    assert",[169,1470,1471],{"class":252}," complete(",[169,1473,1474],{"class":179},"\"mytool deploy --env pro\"",[169,1476,502],{"class":252},[169,1478,1479],{"class":189},"3",[169,1481,1482],{"class":252},") ",[169,1484,1485],{"class":248},"==",[169,1487,678],{"class":252},[169,1489,1490],{"class":179},"\"prod\"",[169,1492,502],{"class":252},[169,1494,686],{"class":179},[169,1496,694],{"class":252},[10,1498,1499],{},"That is the whole test surface: given a partial command line, the program returns the right\ncandidates. It runs anywhere, it is fast, and it fails when someone breaks the callback — which is\notherwise a regression nobody notices until a user mentions that Tab stopped working.",[14,1501,1503],{"id":1502},"caching-the-data-completion-needs","Caching the data completion needs",[10,1505,1506],{},"The tension in dynamic completion is that the useful candidates — environment names, remote\nbranches, table names — usually live somewhere slow. The resolution is to never fetch them during\ncompletion at all.",[160,1508,1510],{"className":233,"code":1509,"language":235,"meta":165,"style":165},"from pathlib import Path\nimport json, time\n\nCACHE = Path.home() \u002F \".cache\" \u002F \"mytool\" \u002F \"environments.json\"\nMAX_AGE = 24 * 3600\n\ndef cached_environment_names() -> list[str]:\n    \"\"\"Read-only, never refreshes. Returns [] rather than blocking or failing.\"\"\"\n    try:\n        payload = json.loads(CACHE.read_text())\n    except (OSError, ValueError):\n        return []\n    return payload.get(\"names\", [])\n\ndef refresh_environment_cache(names: list[str]) -> None:\n    \"\"\"Called by ordinary commands, which are allowed to be slow.\"\"\"\n    CACHE.parent.mkdir(parents=True, exist_ok=True)\n    CACHE.write_text(json.dumps({\"names\": names, \"at\": time.time()}))\n",[38,1511,1512,1525,1532,1536,1563,1579,1583,1597,1602,1609,1624,1643,1651,1664,1668,1687,1692,1718],{"__ignoreMap":165},[169,1513,1514,1517,1520,1522],{"class":171,"line":172},[169,1515,1516],{"class":248},"from",[169,1518,1519],{"class":252}," pathlib ",[169,1521,249],{"class":248},[169,1523,1524],{"class":252}," Path\n",[169,1526,1527,1529],{"class":171,"line":196},[169,1528,249],{"class":248},[169,1530,1531],{"class":252}," json, time\n",[169,1533,1534],{"class":171,"line":202},[169,1535,259],{"emptyLinePlaceholder":258},[169,1537,1538,1541,1543,1546,1549,1552,1555,1558,1560],{"class":171,"line":262},[169,1539,1540],{"class":189},"CACHE",[169,1542,309],{"class":248},[169,1544,1545],{"class":252}," Path.home() ",[169,1547,1548],{"class":248},"\u002F",[169,1550,1551],{"class":179}," \".cache\"",[169,1553,1554],{"class":248}," \u002F",[169,1556,1557],{"class":179}," \"mytool\"",[169,1559,1554],{"class":248},[169,1561,1562],{"class":179}," \"environments.json\"\n",[169,1564,1565,1568,1570,1573,1576],{"class":171,"line":274},[169,1566,1567],{"class":189},"MAX_AGE",[169,1569,309],{"class":248},[169,1571,1572],{"class":189}," 24",[169,1574,1575],{"class":248}," *",[169,1577,1578],{"class":189}," 3600\n",[169,1580,1581],{"class":171,"line":279},[169,1582,259],{"emptyLinePlaceholder":258},[169,1584,1585,1587,1590,1593,1595],{"class":171,"line":288},[169,1586,291],{"class":248},[169,1588,1589],{"class":175}," cached_environment_names",[169,1591,1592],{"class":252},"() -> list[",[169,1594,300],{"class":189},[169,1596,1130],{"class":252},[169,1598,1599],{"class":171,"line":324},[169,1600,1601],{"class":179},"    \"\"\"Read-only, never refreshes. Returns [] rather than blocking or failing.\"\"\"\n",[169,1603,1604,1607],{"class":171,"line":330},[169,1605,1606],{"class":248},"    try",[169,1608,321],{"class":252},[169,1610,1611,1614,1616,1619,1621],{"class":171,"line":367},[169,1612,1613],{"class":252},"        payload ",[169,1615,268],{"class":248},[169,1617,1618],{"class":252}," json.loads(",[169,1620,1540],{"class":189},[169,1622,1623],{"class":252},".read_text())\n",[169,1625,1626,1629,1632,1635,1637,1640],{"class":171,"line":372},[169,1627,1628],{"class":248},"    except",[169,1630,1631],{"class":252}," (",[169,1633,1634],{"class":189},"OSError",[169,1636,502],{"class":252},[169,1638,1639],{"class":189},"ValueError",[169,1641,1642],{"class":252},"):\n",[169,1644,1645,1648],{"class":171,"line":389},[169,1646,1647],{"class":248},"        return",[169,1649,1650],{"class":252}," []\n",[169,1652,1653,1655,1658,1661],{"class":171,"line":556},[169,1654,699],{"class":248},[169,1656,1657],{"class":252}," payload.get(",[169,1659,1660],{"class":179},"\"names\"",[169,1662,1663],{"class":252},", [])\n",[169,1665,1666],{"class":171,"line":561},[169,1667,259],{"emptyLinePlaceholder":258},[169,1669,1670,1672,1675,1678,1680,1683,1685],{"class":171,"line":574},[169,1671,291],{"class":248},[169,1673,1674],{"class":175}," refresh_environment_cache",[169,1676,1677],{"class":252},"(names: list[",[169,1679,300],{"class":189},[169,1681,1682],{"class":252},"]) -> ",[169,1684,318],{"class":189},[169,1686,321],{"class":252},[169,1688,1689],{"class":171,"line":1454},[169,1690,1691],{"class":179},"    \"\"\"Called by ordinary commands, which are allowed to be slow.\"\"\"\n",[169,1693,1694,1697,1700,1703,1705,1707,1709,1712,1714,1716],{"class":171,"line":1465},[169,1695,1696],{"class":189},"    CACHE",[169,1698,1699],{"class":252},".parent.mkdir(",[169,1701,1702],{"class":505},"parents",[169,1704,268],{"class":248},[169,1706,511],{"class":189},[169,1708,502],{"class":252},[169,1710,1711],{"class":505},"exist_ok",[169,1713,268],{"class":248},[169,1715,511],{"class":189},[169,1717,364],{"class":252},[169,1719,1721,1723,1726,1728,1731,1734],{"class":171,"line":1720},18,[169,1722,1696],{"class":189},[169,1724,1725],{"class":252},".write_text(json.dumps({",[169,1727,1660],{"class":179},[169,1729,1730],{"class":252},": names, ",[169,1732,1733],{"class":179},"\"at\"",[169,1735,1736],{"class":252},": time.time()}))\n",[10,1738,1739,1740,1743],{},"Every normal command that already knows the answer — ",[38,1741,1742],{},"mytool env list",", a deploy that resolved an\nenvironment — writes the cache as a side effect. Completion only ever reads it, and returns an\nempty list when the file is missing or unreadable rather than raising.",[10,1745,1746,1747,1749],{},"The trade is that suggestions can be stale, which is almost always acceptable: a missing candidate\nmeans the user types the name in full, while a two-second Tab means they stop pressing Tab at all.\nIf staleness matters, add a visible refresh command and mention it in ",[38,1748,96],{},", so the behaviour is\nsomething users can reason about rather than a mystery.",[14,1751,1753],{"id":1752},"frequently-asked-questions","Frequently asked questions",[1755,1756,1758],"h3",{"id":1757},"why-does-completion-work-in-one-terminal-and-not-another","Why does completion work in one terminal and not another?",[10,1760,1761,1762,1765,1766,132],{},"Almost always because the profile line has not been loaded in that shell. Completion is installed\nby writing a script and adding a line to a start-up file, so only shells started afterwards see\nit. The second most common cause is zsh's ",[38,1763,1764],{},"compinit"," running before the directory containing your\nscript was added to ",[38,1767,1768],{},"$fpath",[1755,1770,1772],{"id":1771},"should-i-ship-the-completion-script-in-the-package","Should I ship the completion script in the package?",[10,1774,1775,1776,1779],{},"Only if you also tell users where to put it. A generated script inside the wheel does nothing on\nits own; the useful thing is a documented command — ",[38,1777,1778],{},"mytool --show-completion > …"," — or a\npost-install step in your Homebrew or distribution package, where placing the file is expected.",[1755,1781,1783],{"id":1782},"does-completion-slow-down-my-shell-start-up","Does completion slow down my shell start-up?",[10,1785,1786,1787,1789],{},"The ",[38,1788,985],{}," form does, because it runs your program every time a shell opens. Writing the generated\nscript to a file once removes that cost entirely, and it is the arrangement worth documenting for\nanyone whose CLI takes more than a few tens of milliseconds to start.",[1755,1791,1793],{"id":1792},"can-i-complete-file-paths-as-well-as-choices","Can I complete file paths as well as choices?",[10,1795,1796,1797,1799],{},"Yes, and you get it free by using the right type. A ",[38,1798,1100],{}," parameter tells the shell to use its\nown filename completion, which is faster and better behaved than anything you would write. Only\nreach for a custom callback when the candidates are not files.",[1755,1801,1803],{"id":1802},"how-do-i-complete-values-that-depend-on-another-flag","How do I complete values that depend on another flag?",[10,1805,1806,1807,1810,1811,1814,1815,1818,1819,1822],{},"Click's callback receives the context, so ",[38,1808,1809],{},"ctx.params"," gives you the values parsed so far — enough\nto complete ",[38,1812,1813],{},"--table"," based on the ",[38,1816,1817],{},"--database"," already typed. Typer's callback can take a\n",[38,1820,1821],{},"typer.Context"," for the same reason. Be careful about ordering: the user may not have typed the\nother flag yet, so always handle the missing case rather than raising.",[1755,1824,1826],{"id":1825},"is-completion-worth-the-effort-for-an-internal-tool","Is completion worth the effort for an internal tool?",[10,1828,1829],{},"It is arguably worth more there. Internal tools have fewer users but those users run them dozens\nof times a day, and the values they type — environment names, service identifiers, ticket\nreferences — are exactly the kind that nobody remembers exactly. Static completion of subcommands\nand enums costs nothing beyond choosing the right parameter types, so the question is really only\nabout the dynamic parts — and those are usually the values your team argues about in chat, which is a good sign they belong behind Tab.",[14,1831,1833],{"id":1832},"related","Related",[19,1835,1836,1841,1846,1852,1857],{},[22,1837,1838,1840],{},[106,1839,836],{"href":211}," — the Python side, including dynamic completions.",[22,1842,1843,1845],{},[106,1844,855],{"href":216}," — the per-shell install and troubleshooting.",[22,1847,1848,1851],{},[106,1849,1850],{"href":113},"Interactive terminal UI with Rich"," — the other half of a great CLI experience.",[22,1853,1854,1856],{},[106,1855,616],{"href":615}," — how the two frameworks differ, completion included.",[22,1858,1859,1863],{},[106,1860,1862],{"href":1861},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing for Python CLIs"," — the parent track on input and UX.",[1865,1866,1867],"style",{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":165,"searchDepth":196,"depth":196,"links":1869},[1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1889],{"id":16,"depth":196,"text":17},{"id":74,"depth":196,"text":75},{"id":118,"depth":196,"text":119},{"id":220,"depth":196,"text":221},{"id":620,"depth":196,"text":621},{"id":823,"depth":196,"text":824},{"id":869,"depth":196,"text":870},{"id":924,"depth":196,"text":925},{"id":1093,"depth":196,"text":1094},{"id":1253,"depth":196,"text":1254},{"id":1502,"depth":196,"text":1503},{"id":1752,"depth":196,"text":1753,"children":1882},[1883,1884,1885,1886,1887,1888],{"id":1757,"depth":202,"text":1758},{"id":1771,"depth":202,"text":1772},{"id":1782,"depth":202,"text":1783},{"id":1792,"depth":202,"text":1793},{"id":1802,"depth":202,"text":1803},{"id":1825,"depth":202,"text":1826},{"id":1832,"depth":196,"text":1833},"2026-07-05","Add tab completion to Python CLIs: how completion works in Click and Typer, generate scripts for bash, zsh, and fish, and offer dynamic suggestions.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis",{"title":5,"description":1891},"advanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Findex",[1900,1901,1902,164,603],"completion","click","typer","2026-08-01","0nOx34HfgeavSXINhXxckZriKOMWx92-sHL-pf00YFQ",[1906,1909,1912,1915,1918,1921,1924,1927,1930,1933,1936,1939,1942,1945,1948,1951,1954,1956,1959,1962,1965,1968,1969,1972,1975,1978,1981,1984,1987,1990,1993,1996,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088,2091,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121,2124,2127,2130,2133,2136,2139,2142,2145,2148],{"path":1907,"title":1908},"\u002Fabout","About Python CLI Toolcraft",{"path":1910,"title":1911},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1913,"title":1914},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1916,"title":1917},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1919,"title":1920},"\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":1922,"title":1923},"\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":1925,"title":1926},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1928,"title":1929},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1931,"title":1932},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1934,"title":1935},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1937,"title":1938},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1940,"title":1941},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1943,"title":1944},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1946,"title":1947},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1949,"title":1950},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1952,"title":1953},"\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":1955,"title":1862},"\u002Fadvanced-input-parsing-user-experience",{"path":1957,"title":1958},"\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":1960,"title":1961},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1963,"title":1964},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1966,"title":1967},"\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":1896,"title":5},{"path":1970,"title":1971},"\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":1973,"title":1974},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1976,"title":1977},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1979,"title":1980},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1982,"title":1983},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1985,"title":1986},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1988,"title":1989},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1991,"title":1992},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1994,"title":1995},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1548,"title":1997},"Python CLI Toolcraft",{"path":1999,"title":2000},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2002,"title":2003},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2005,"title":2006},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2008,"title":2009},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2011,"title":2012},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2014,"title":2015},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2017,"title":2018},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2020,"title":2021},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2023,"title":2024},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2026,"title":2027},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2029,"title":2030},"\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":2032,"title":2033},"\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":2035,"title":2036},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2038,"title":2039},"\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":2041,"title":2042},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2044,"title":2045},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2047,"title":2048},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2050,"title":2051},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2053,"title":2054},"\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":2056,"title":2057},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2059,"title":2060},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2062,"title":2063},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2065,"title":2066},"\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":2068,"title":2069},"\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":2071,"title":2072},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2074,"title":2075},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2077,"title":2078},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2080,"title":2081},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2083,"title":2084},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2086,"title":2087},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2089,"title":2090},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2092,"title":2093},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2095,"title":2096},"\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":2098,"title":2099},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2101,"title":2102},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2104,"title":2105},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2107,"title":2108},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2110,"title":2111},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2113,"title":2114},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2116,"title":2117},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2119,"title":2120},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2122,"title":2123},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2125,"title":2126},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2128,"title":2129},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2131,"title":2132},"\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":2134,"title":2135},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2137,"title":2138},"\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":2140,"title":2141},"\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":2143,"title":2144},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2146,"title":2147},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2149,"title":2150},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690029]